07. CLI 開発
この章では Crystal での CLI 開発について書きます。
Crystal で CLI ツール
Crystal で CLI ツールの開発をしていきましょう。Crystal で CLI ツールを書くメリットは次のようなものがあります。
-
コンパイルしてワンバイナリにできる
-
Ruby 風の syntax で雑に書ける
-
実行速度が早い
-
コンパイル時に型チェックが入る
CLI ツールは、欲しい時にサッと書けて継続的に使えるようにしたいですね。そういう意味では、 Crystal という選択肢は良いのではないでしょうか。今回初めて Crystal を触るという方も、まずは CLI ツールをサクッと作ってみることをおすすめします。では、早速作っていきましょう。
自作の echo コマンド「 myecho 」を作る
echo
コマンドを模倣した myecho
コマンドを作っていきましょう。まずは crystal init app myecho
を実行してひな形を作ります。
$ crystal init app myecho
create myecho/.gitignore
create myecho/.editorconfig
create myecho/LICENSE
create myecho/README.md
create myecho/.travis.yml
create myecho/shard.yml
create myecho/src/myecho.cr
create myecho/src/myecho/version.cr
create myecho/spec/spec_helper.cr
create myecho/spec/myecho_spec.cr
Initialized empty Git repository in /path/to/myecho/.git/
次のようなファイルが作成されました。
$ tree
.
├── LICENSE
├── README.md
├── shard.yml
├── spec
│ ├── myecho_spec.cr
│ └── spec_helper.cr
└── src
├── myecho
│ └── version.cr
└── myecho.cr
3 directories, 7 files
これで準備は整いました。まずはテストを回してみましょう。
$ crystal spec
F
Failures:
1) MyEcho works
Failure/Error: false.should eq(true)
Expected: true
got: false
# spec/myecho_spec.cr:7
Finished in 73 microseconds
1 examples, 1 failures, 0 errors, 0 pending
Failed examples:
crystal spec spec/myecho_spec.cr:6 # MyEcho works
テストは落ちます。ひな形生成時、 spec/myecho_spec.cr
に失敗するテストが書かれているからです。
require "./spec_helper"
require "./../src/myecho.cr"
describe MyEcho do
# TODO: Write tests
it "works" do
false.should eq(true)
end
end
では、実際のテストから書いていきましょう。myecho
の基本機能は「与えられた引数の文字列をそのまま出力する」です。
describe MyEcho do
describe MyEcho::Cli do
describe "run" do
it "writes the content of args to specified IO" do
io = IO::Memory.new (1)
myecho = MyEcho::Cli.new(io) (2)
myecho.run(["foo", "bar"]) (3)
io.to_s.should eq "foo bar\n" (4)
end
end
end
end
-
出力する先の
IO
インスタンスを生成します。テスト時はIO::Memory
に出力します。 -
MyEcho::Cli
にio
を渡し、インスタンスを生成します。 -
MyEcho::Cli#run
に、コマンドライン引数のARGV
を模した["foo", "bar"]
を渡して実行します。 -
io
に出力された文字列を検証します。
まだ実装が終わっていないので、このテストは落ちます。実装側も書きましょう。
require "./myecho/*"
module MyEcho
class Cli
def initialize(@io : IO = STDOUT) (1)
end
def run(args) (2)
@io.print args.join(" ") + "\n" (3)
end
end
end
-
インスタンス変数
@io
を定義します。初期値はSTDOUT
です。 -
#run
を定義します。 -
@io
に引数args
を出力します。
書けたらテストを回しましょう。
$ crystal spec
.
Finished in 66 microseconds
1 examples, 0 failures, 0 errors, 0 pending
通りました。これで、受け取った引数をそのまま出力するメソッド #run
を実装できました。次は build してバイナリを作りましょう。
まずは build 対象のファイルを作ります。src/myecho.cr
で #run
を呼び出して直接 build してもよいですが、そうするとテスト時にも #run
が実行されてしまいます。それを避けるために cli.cr
ファイルを別途作成し、 myecho.cr
を require しましょう。
require "./myecho"
MyEcho::Cli.new.run(ARGV)
これで、モジュールと build ファイルを分離できました。早速 build してみましょう。
$ mkdir bin
$ crystal build -o ./bin/myecho ./src/cli.cr
./bin/
ディレクトリを作成し、その中に myecho
という名前でバイナリを出力しています。myecho
を実行してみましょう。
$ ./bin/myecho Hello!! World!!
Hello!! World!!
出力されました!CLI ツールの完成です!いろいろ出力して遊んでみてください。
$ ./bin/myecho HAHAHA!
HAHAHA!
バージョン表示のオプション -v
を実装する
さらに CLI ツールらしくしていきましょう。バージョンを表示させる -v
オプションを実装します。オプションがあると一気に CLI ツールらしくなりますね。Crystal には OptionParser
というクラスが用意されています。コマンドラインオプションを扱うのに便利なクラスです。今回は OptionParser
の使い方も解説しつつ実装していきます。まずはテストから書きましょう。
describe "writes the version to specified IO" do
it "with '-v'" do
io = IO::Memory.new (1)
myecho = MyEcho::Cli.new(io) (2)
myecho.run(["-v"]) (3)
io.to_s.should eq MyEcho::VERSION + "\n" (4)
end
end
-
出力する先の
IO
インスタンスを生成します。テスト時はIO::Memory
に出力します。 -
MyEcho::Cli
にio
を渡し、インスタンスを生成します。 -
version を表示するコマンドライン引数
["-v"]
を渡して実行します。 -
MyEcho::VERSION
が表示されていることを検証します。
テストを回しましょう。
$ crystal spec
..F
Failures:
1) MyEcho MyEcho::Cli run writes the version to specified IO with '-v'
Failure/Error: io.to_s.should eq MyEcho::VERSION + "\n"
Expected: "0.1.0\n"
got: "-v\n"
# spec/myecho_spec.cr:18
Finished in 117 microseconds
3 examples, 1 failures, 0 errors, 0 pending
Failed examples:
crystal spec spec/myecho_spec.cr:14 # MyEcho MyEcho::Cli run writes the version to specified IO with '-v'
0.1.0
が期待されていますが -v
が出力されていますね。これは期待通りの落ち方です。では実装に入りましょう。単純に「 -v
が入力されたら MyEcho::VERSION
を出力する」でもよいのですが、先程も宣言した通り OptionParser
を導入します。
require "./myecho/*"
require "option_parser" (1)
module MyEcho
class Cli
def initialize(@io : IO = STDOUT)
end
class Options
property display_version : Bool = false
property args : Array(String) = [] of String
end
def run(args)
# オプション設定を格納する
options = Options.new
# OptionParserを用いたオプションの設定
OptionParser.parse(args) do |parser| (2)
parser.on("-v", "show version") do (3)
options.display_version = true
end
parser.unknown_args do |unknown_args| (4)
options.args = unknown_args
end
end
# バージョンの表示
if options.display_version
@io.print MyEcho::VERSION + "\n"
return
end
# 引数の表示
@io.print options.args.join(" ") + "\n"
end
end
end
-
標準ライブラリの
OptionParser
を require します。 -
OptionParser#parse
にargs
を渡し、ブロック内でオプションを定義していきます。 -
OptionParser#on
の引数に-v
とその説明文を、ブロックには実行したい処理を書きます。 -
オプション以外の引数は、配列になって
OptionParser#unknown_args
のブロック引数となります。
テストを回しましょう。
$ crystal spec
...
Finished in 102 microseconds
3 examples, 0 failures, 0 errors, 0 pending
通りました。バイナリも作りましょう。そして、実際にバージョンを表示してみましょう。
$ crystal build -o ./bin/myecho ./src/cli.cr
$ ./bin/myecho foo
foo
$ ./bin/myecho -v
0.1.0
バージョン表示ができました!CLI ツールらしくなってきました。ここまで来ると、 --version
を指定してもバージョンを表示させたいですよね。現状だと例外が発生してしまいます。
$ ./bin/myecho --version
--version
Invalid option: --version (OptionParser::InvalidOption)
(スタックトレースが続く ... )
では対応していきましょう。まずはテストから書きます。
describe "writes the version to specified IO" do
it "with '-v'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["-v"])
io.to_s.should eq MyEcho::VERSION + "\n"
end
# 新しいテストケース
it "with '--version'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["--version"]) (1)
io.to_s.should eq MyEcho::VERSION + "\n"
end
end
-
--version
を指定した場合でも、バージョンが表示されることを検証しています。
テストを回すと、例外が発生しテストが落ちます。
$ crystal spec
.....E
Failures:
1) MyEcho MyEcho::Cli run writes the version to specified IO with '--version'
Invalid option: --version
Error running at_exit handler: Index out of bounds
対応するには、 OptionParser#on
の第二引数に long_flag
を指定します。
parser.on("-v", "--version", "show version") do (1)
options.display_version = true
end
-
OptionParser#on
の第二引数に--version
を指定しています。
これでテストが通ります。
$ crystal spec
......
Finished in 189 microseconds
6 examples, 0 failures, 0 errors, 0 pending
実際にバージョンを表示してみましょう。
$ crystal build -o ./bin/myecho ./src/cli.cr
$ ./bin/myecho foo
foo
$ ./bin/myecho -v
0.1.0
$ ./bin/myecho --version
0.1.0
--version
でもバージョンを表示できました。このように、 OptionParser
は、コマンドラインオプションを柔軟に扱えます。
ヘルプ表示のオプション -h
--help
を実装する
次にヘルプを表示してみます。オプションの追加はバージョン表示の時と同じです。まずはテストから書きましょう。
describe "writes the help to specified IO" do
it "with '-h'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["-h"])
io.to_s.should eq "helpには何が表示される?"
end
end
-h
を指定した場合、何が表示されるかはまだわかりません。とりあえずこのまま進みましょう。現状だと、 Invalid option: -h
でテストが落ちることは目に見えています。まずは適当に文字列を返しましょう。
parser.on("-h", "--help", "show help") do
options.display_help = true
end
...
# ヘルプの表示
if options.display_help
@io.print "helpです。" + "\n"
return
end
当然、テストは失敗します。
$ crystal spec
.........F
Failures:
1) MyEcho MyEcho::Cli run writes the help to specified IO with '-h'
Failure/Error: io.to_s.should eq "helpには何が表示される?"
Expected: "helpには何が表示される?"
got: "helpです。\n"
# spec/myecho_spec.cr:35
Finished in 246 microseconds
10 examples, 1 failures, 0 errors, 0 pending
Failed examples:
crystal spec spec/myecho_spec.cr:31 # MyEcho MyEcho::Cli run writes the help to specified IO with '-h'
ここで、 OptionParser
の便利機能を使います。OptionParser#to_s
で、ヘルプメッセージを返してくれます。実装してみましょう。
# OptionParserを用いたオプションの設定
OptionParser.parse(args) do |parser|
...
# ヘルプメッセージの格納
# すべての設定を行った後で格納する
options.help_message = parser.to_s + "\n"
end
...
# ヘルプの表示
if options.display_help
@io.print options.help_message
return
end
テストを回します。
$ crystal spec
...F
Failures:
1) MyEcho MyEcho::Cli run writes the help to specified IO with '-h'
Failure/Error: io.to_s.should eq "helpには何が表示される?"
Expected: "helpには何が表示される?"
got: " -h, --help show help\n -v, --version show version\n"
# spec/myecho_spec.cr:35
Finished in 174 microseconds
4 examples, 1 failures, 0 errors, 0 pending
Failed examples:
crystal spec spec/myecho_spec.cr:31 # MyEcho MyEcho::Cli run writes the help to specified IO with '-h'
それらしい文字列が返ってきました。テストを修正しましょう。
describe "writes the help to specified IO" do
it "with '-h'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["-h"])
io.to_s.should eq <<-HELP_MESSAGE
-h, --help show help
-v, --version show version
HELP_MESSAGE
end
end
今度はテストが回りました。
$ crystal spec
..........
Finished in 294 microseconds
10 examples, 0 failures, 0 errors, 0 pending
ヘルプが表示されるかを build して確かめます。
$ crystal build -o ./bin/myecho ./src/cli.cr
$ ./bin/myecho foo
foo
$ ./bin/myecho -h
-h, --help show help
-v, --version show version
$ ./bin/myecho --help
-h, --help show help
-v, --version show version
ヘルプが表示されました。しかし、もう少し体裁の整ったヘルプがよいですね。例えば、コマンドの説明や Usage などがあると良さそうです。テストを書きましょう。
describe "writes the help to specified IO" do
it "with '-h'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["-h"])
io.to_s.should eq <<-HELP_MESSAGE
My echo.
Usage: myecho [options] [arguments]
-h, --help show help
-v, --version show version
HELP_MESSAGE
end
end
良い感じのヘルプにしてみました。テストが通るように実装しましょう。OptionParser
には、 #banner=
というメソッドがあり、ヘルプメッセージに加える文字列を定義できます。
parser.banner = <<-BANNER
My echo.
Usage: myecho [options] [arguments]
BANNER
これでテストを通すことができました。build して動作を確かめましょう。
$ crystal build -o ./bin/myecho ./src/cli.cr
$ ./bin/myecho --help
My echo.
Usage: myecho [options] [arguments]
-h, --help show help
-v, --version show version
より、ヘルプらしくなりました。OptionParser
を使えば、このように簡単にヘルプを設定できます。
prefix を付ける --prefix
を実装する
もっと CLI ツールらしくするために、さらにオプションを加えましょう。各コマンド引数に prefix をつける --prefix
オプションです。期待される動作は次のようになります。
$ ./bin/myecho --prefix pre_ foo bar baz
pre_foo pre_bar pre_baz
--prefix PREFIX
を指定することで、各引数の先頭に PREFIX
を付けます。まずはテストから書きましょう。
describe "prefix specified string to each arguments" do
it "with '--prefix PREFIX'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["--prefix", "pre_", "foo", "bar", "baz"])
io.to_s.should eq "pre_foo pre_bar pre_baz\n"
end
end
このテストを通すように実装しましょう。
# OptionParserを用いたオプションの設定
OptionParser.parse(args) do |parser|
...
parser.on("--prefix PREFIX", "prefix to each arguments") do |prefix|
options.prefix = prefix
end
...
end
...
prefix = options.prefix
unless prefix.nil?
@io.print options.args.map { |arg| prefix + arg }.join(" ") + "\n"
return
end
OptionParser#on
は、 long_flag
の名前だけでも指定可能です。--prefix PREFIX
のように、オプション引数( PREFIX
)を書くと、オプション引数が必須になります。また、今回の --prefix
定義のままで、コマンドラインから --prefix=PREFIX
のように =
を用いた指定も可能です。いい感じに取り扱ってくれます。
これで --prefix
の実装も終わりました。build して動作を確認しましょう。
$ crystal build -o ./bin/myecho ./src/cli.cr
$ ./bin/myecho --prefix pre_ foo bar baz
pre_foo pre_bar pre_baz
うまく動作しているようです。
いかがでしたでしょうか。OptionParser
を使っての CLI ツールの作成方法が大体つかめたでしょうか。
以下に、ここまでで紹介した myecho
のコード全てを記載します。
require "./myecho"
MyEcho::Cli.new.run(ARGV)
require "./myecho/*"
require "option_parser"
module MyEcho
class Cli
def initialize(@io : IO = STDOUT)
end
class Options
property display_version : Bool = false
property args : Array(String) = [] of String
property display_help : Bool = false
property help_message : String = ""
property prefix : String? = nil
end
def run(args)
# オプション設定を格納する
options = Options.new
# OptionParserを用いたオプションの設定
OptionParser.parse(args) do |parser|
parser.banner = <<-BANNER
My echo.
Usage: myecho [options] [arguments]
BANNER
parser.on("--prefix PREFIX", "prefix to each arguments") do |prefix|
options.prefix = prefix
end
parser.on("-h", "--help", "show help") do
options.display_help = true
end
parser.on("-v", "--version", "show version") do
options.display_version = true
end
parser.unknown_args do |unknown_args|
options.args = unknown_args
end
# ヘルプメッセージの格納
options.help_message = parser.to_s + "\n"
end
# ヘルプの表示
if options.display_help
@io.print options.help_message
return
end
# バージョンの表示
if options.display_version
@io.print MyEcho::VERSION + "\n"
return
end
# prefixをつける
prefix = options.prefix
unless prefix.nil?
@io.print options.args.map { |arg| prefix + arg }.join(" ") + "\n"
return
end
# 引数の表示
@io.print options.args.join(" ") + "\n"
end
end
end
require "./spec_helper"
require "./../src/myecho.cr"
describe MyEcho do
describe MyEcho::Cli do
describe "run" do
it "writes the content of args to specified IO with args" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["foo", "bar"])
io.to_s.should eq "foo bar\n"
end
describe "writes the version to specified IO" do
it "with '-v'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["-v"])
io.to_s.should eq MyEcho::VERSION + "\n"
end
it "with '--version'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["--version"]) (1)
io.to_s.should eq MyEcho::VERSION + "\n"
end
end
describe "writes the help to specified IO" do
it "with '-h'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["-h"])
io.to_s.should eq <<-HELP_MESSAGE
My echo.
Usage: myecho [options] [arguments]
--prefix PREFIX prefix to each arguments
-h, --help show help
-v, --version show version
HELP_MESSAGE
end
end
describe "prefix specified string to each arguments" do
it "with '--prefix PREFIX'" do
io = IO::Memory.new
myecho = MyEcho::Cli.new(io)
myecho.run(["--prefix", "pre_", "foo", "bar", "baz"])
io.to_s.should eq "pre_foo pre_bar pre_baz\n"
end
end
end
end
end
次は、もう少しかゆいところに手が届く、サードパーティ製の CLI ビルダーライブラリをいくつかご紹介します。
サードパーティ製のライブラリを使う
CLI ツールを作成するにあたって、サードパーティ製の CLI ビルダーツールを使うことは得策です。OptionParser
よりもリッチな機能を使うことができます。例えば、 Ruby だと thor などが有名です。Crystal でも CLI ビルダーツールが作成されています。今回は主観で選択したいくつかをご紹介します。題材としては、ここまで作ってきた myecho
を用います。
mrrooijen/commander
早い時期から作成されていたライブラリです。百聞は一見に如かず、早速 myecho
を実装しましょう。
require "./myecho_commander/*"
require "commander"
cli = Commander::Command.new do |cmd|
cmd.use = "myecho"
cmd.long = "My echo."
cmd.flags.add do |flag|
flag.name = "prefix"
flag.long = "--prefix"
flag.default = ""
flag.description = "prefix to each arguments."
end
cmd.flags.add do |flag|
flag.name = "version"
flag.long = "--version"
flag.short = "-v"
flag.default = false
flag.description = "show version."
end
cmd.run do |options, arguments|
# バージョンの表示
if options.bool["version"]
puts MyechoCommander::VERSION
next
end
# prefixをつける
prefix = options.string["prefix"]
unless prefix.empty?
puts arguments.map { |arg| prefix + arg }.join(" ")
next
end
# 引数の表示
puts arguments.join(" ")
end
end
Commander.run(cli, ARGV)
サードパーティ製のライブラリは、基本的に次の構成になっています。
-
コマンドの説明(
Description
やUsage
など) -
Options
やFlags
の定義 -
実際に実行される箇所
-
run
メソッドやrun
ブロックなど -
ここで
Options
やFlags
の入力値を受け取れる -
ここで 入力された引数を受け取れる
-
これらを独自の定義方法で書いていきます。OotionParser
を用いたときよりも見やすくなっていると思います。また、 help
や version
など、 CLI ツールに必須のオプションはデフォルトでついている場合もあります。mrrooijen/commander
の場合は help
がデフォルトで設定されているので、コード上には現れていません。また、サブコマンドも実装可能です。詳しくは公式マニュアルを御覧ください。コードを実際に実行すると次のようになります。
$ crystal run src/myecho_commander.cr -- foo bar baz
foo bar baz
$ crystal run src/myecho_commander.cr -- --version
0.1.0
$ crystal run src/myecho_commander.cr -- --help
myecho - My echo.
Usage:
myecho [flags] [arguments]
Commands:
help [command] # Help about any command.
Flags:
-h, --help # Help for this command. default: 'false'.
--prefix # prefix to each arguments. default: ''.
-v, --version # show version. default: 'false'.
$ crystal run src/myecho_commander.cr -- --prefix pre_ foo bar baz
pre_foo pre_bar pre_baz
期待通りの動作をしていることがわかります。
jwaldrip/admiral.cr
最近も開発されているライブラリです。早速コードを見てみましょう。
require "./myecho_admiral/*"
require "admiral"
module MyechoAdmiral
class Cli < Admiral::Command
define_version MyechoAdmiral::VERSION, short: v
define_help description: "My echo.", short: h
define_flag prefix : String?,
description: "prefix to each arguments.",
long: prefix
def run
# prefixをつける
prefix = flags.prefix
unless prefix.nil?
puts arguments.map { |arg| prefix + arg }.join(" ")
return
end
# 引数の表示
puts arguments.join(" ")
end
end
end
MyechoAdmiral::Cli.run
行数が少ないですね。help
と version
はマクロで1行で書かれています。こういった DSL を提供しやすいのもマクロの強みです。構造もわかりやすく、読みやすいと思います。もちろん、サブコマンドも対応しています。
実際に実行すると次のようになります。
$ crystal run src/myecho_admiral.cr -- foo bar baz
foo bar baz
$ crystal run src/myecho_admiral.cr -- --version
0.1.0
$ crystal run src/myecho_admiral.cr -- --help
Usage:
myecho [flags...] [arg...]
My echo.
Flags:
--help, -h (default: false) # Displays help for the current command.
--prefix # prefix to each arguments.
--version, -v (default: false)
$ crystal run src/myecho_admiral.cr -- --prefix pre_ foo bar baz
pre_foo pre_bar pre_baz
こちらも期待通りの動作をしています。
at-grandpa/clim
これは私自身が作成したライブラリです。記述量の少なさと直感的な記述を目的に作成しています。コードを見てみましょう。
require "./myecho_clim/*"
require "clim"
module MyechoClim
class Cli < Clim
main do
desc "My echo."
usage "myecho [options] [arguments] ..."
option "--prefix PREFIX", type: String, desc: "prefix to each arguments.", default: ""
version MyechoClim::VERSION, short: "-v"
run do |options, arguments|
# prefixをつける
prefix = options.prefix
unless prefix.empty?
puts arguments.all_args.map { |argument| prefix + argument }.join(" ")
next
end
# 引数の表示
puts arguments.all_args.join(" ")
end
end
end
end
MyechoClim::Cli.start(ARGV)
desc
や option
などの定義が1行で書けます。version
マクロも用意しました。コマンドの実行箇所は、 run
ブロックになります。サブコマンドも対応しており、直感的に記述できます。詳しくはマニュアルを御覧ください。
実際に実行すると次のようになります。
$ crystal run src/myecho_clim.cr -- foo bar baz
foo bar baz
$ crystal run src/myecho_clim.cr -- --version
0.1.0
$ crystal run src/myecho_clim.cr -- --help
My echo.
Usage:
myecho [options] [arguments] ...
Options:
--prefix PREFIX prefix to each arguments. [type:String] [default:""]
--help Show this help.
-v, --version Show version.
$ crystal run src/myecho_clim.cr -- --prefix pre_ foo bar baz
pre_foo pre_bar pre_baz
こちらも期待通りの動作をしています。
サードパーティ製のライブラリはそれぞれに特徴がありますが、デフォルトの OptionParser
よりはリッチな機能を提供してくれます。個人で使う小さな CLI ツールだと OptionParser
で十分かもしれません。しかし、ツールとして公開したりメンテナンスが必要になってくるケースでは、サードパーティ製ライブラリを使うほうが良いかと思います。ぜひ一度試してみてください。
まとめ
この章では Crystal で CLI ツールの開発を行うことについて説明しました。冒頭に説明した通り、利点は次のようになるでしょう。
-
コンパイルしてワンバイナリにできる
-
Ruby 風の syntax で雑に書ける
-
実行速度が早い
-
コンパイル時に型チェックが入る
ものすごく雑に小さい CLI ツールを作成する場合は、コンパイルがある分、 Crystal よりも Ruby の方が速いでしょう。しかし、中規模程度でコード量が多くてメンテナンスが必要になってくるケースだと、 Crystal の利点は大きくなってくるのではないでしょうか。大規模なバッチ処理などでも、処理スピードやメモリ使用量などで Crysal の力が発揮されると思います。
みなさんも一度、 敷居の低い CLI ツールを通して Crystal に触れてみてください。