DEV Community

kojix2
kojix2

Posted on

12 Things I Learned Writing CLI Tools in Crystal

I love the Crystal programming language. For the past two or three years, I have been building command-line tools with it. During this time, I often compared it with Ruby, and I encountered many differences, discoveries, and obstacles. In this article, I will share them.

1. Similarity to Ruby

Crystal looks very similar to Ruby. Many common Ruby idioms also work in Crystal. Crystal is statically typed, but most of the time you do not need to write types explicitly. Type inference will do the work for you.

2. Use DeepWiki

DeepWiki is very useful for learning Crystal. For a niche language, it is one of the best resources. You can even ask questions in your native language.

3. Arrays and Hashes cannot mix types

In Crystal, you cannot freely mix different types in an Array or Hash. Ruby allows this, but Crystal does not. You can use union types, but usually it is better to avoid them. Instead, consider one of these options:

At first this may feel inconvenient, but you get used to it.

# Array(Int32 | String | Symbol) - not recommended
arr = [1, "two", :three]

# OK: Union is written explicitly
arr : Array(Int32 | String | Symbol) = [1, "two", :three]

# OK: Tuple for fixed positions
t = {1, "two", :three}

# OK: record for structured data
record Item, id : Int32, name : String, tag : Symbol
items = [
  Item.new(1, "apple",  :fruit),
  Item.new(2, "orange", :fruit),
]
Enter fullscreen mode Exit fullscreen mode

4. No eval

Crystal does not have eval. This is one big difference from Ruby.
If you really need dynamic evaluation, you should use Ruby. Another choice is to embed mruby or use a library like Anyolite. Crystal itself has an interpreter, but it is not practical and slower than Ruby or mruby.

# Ruby
code = "1 + 2"
puts eval(code) # => 3
Enter fullscreen mode Exit fullscreen mode
# Crystal has no eval
# You must design differently
Enter fullscreen mode Exit fullscreen mode

5. Method overloading

In Ruby, it is common to branch on the argument type inside one method.
In Crystal, it is more natural to use method overloading. This makes the code clearer.

def square(x : Int32) : Int32
  x * x
end

def square(x : String) : Int32
  square(x.to_i)
end

puts square(12)     # => 144
puts square("12")   # => 144
Enter fullscreen mode Exit fullscreen mode

6. Return types should be consistent

In Ruby, a method can return values of different types. In Crystal, if the return type is not clear, you will run into trouble. If you want to return different types, you should split the method. You can use a union type, but it is not recommended.

# not recommended
def maybe_value(flag : Bool) : Int32 | String
  flag ? 42 : "forty-two"
end
Enter fullscreen mode Exit fullscreen mode
def value_int : Int32
  42
end

def value_str : String
  "forty-two"
end
Enter fullscreen mode Exit fullscreen mode

7. Handling Nil

Pay attention to whether a variable can be Nil.
If it can, you need to handle it with not_nil!, if val = maybe_val, or the safe navigation operator.

name : String? = nil

if n = name
  puts n.upcase
else
  raise "name is nil"
end
Enter fullscreen mode Exit fullscreen mode

8. Garbage collection

Crystal uses LLVM and relies on an external GC (libgc).
Performance is often close to Rust or Nim, but memory profiling and tuning can be difficult. Also, the timing of GC is not predictable, so Crystal may not be suitable for real-time systems.

9. Asynchronous I/O

Asynchronous I/O is available by default. Some developers feel it is easier to use than in Rust.

10. Linking when distributing

Crystal programs are usually linked with libgc and other libraries such as libpcre2. Be careful when distributing binaries.

  • Linux: You can build statically linked binaries with GitHub Actions + Docker + musl
  • macOS: You can prepare a Homebrew Tap, or build portable binaries with static linking for libgc, libpcre2, and others

See also: github actions workflow in lolcat.cr

11. Windows support

Crystal now works on Windows (MSVC / MinGW64) more stably than before. Parallel execution also works. However, solving C library dependencies can still be painful. If you are not familiar with Windows, you may need to ask AI for help.

12. Limitations of OptionParser

The standard OptionParser does not support combined short options.
So ls -l -h works, but ls -lh does not.
I plan to create a pull request to fix this in the future.

Conclusion

Writing command-line tools in Crystal is sometimes painful. But at the same time, you learn a lot. I believe the “best days” of the Crystal language are not in the past or present, but in the future.


This post was originally based on my reply to a thread on Reddit, then expanded into a Japanese article on Qiita, and now translated into English with the help of ChatGPT.

Top comments (0)