著者: at_grandpa
この章では Crystal での CLI 開発について書きます。
Crystal で CLI ツールの開発をしていきましょう。Crystal で CLI ツールを書くメリットは次のようなものがあります。
-
コンパイルしてワンバイナリにできる
-
Ruby 風の syntax で雑に書ける
-
実行速度が早い
-
コンパイル時に型チェックが入る
CLI ツールは、欲しい時にサッと書けて継続的に使えるようにしたいですね。そういう意味では、 Crystal という選択肢は良いのではないでしょうか。今回初めて Crystal を触るという方も、まずは CLI ツールをサクッと作ってみることをおすすめします。では、早速作っていきましょう。
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
に失敗するテストが書かれているからです。
link:./projects/myecho/spec/myecho_01_first_spec_failed.cr[role=include]
では、実際のテストから書いていきましょう。myecho
の基本機能は「与えられた引数の文字列をそのまま出力する」です。
link:./projects/myecho/spec/myecho_02_standard_output_spec.cr[role=include]
-
出力する先の
IO
インスタンスを生成します。テスト時はIO::Memory
に出力します。 -
MyEcho::Cli
にio
を渡し、インスタンスを生成します。 -
MyEcho::Cli#run
に、コマンドライン引数のARGV
を模した["foo", "bar"]
を渡して実行します。 -
io
に出力された文字列を検証します。
まだ実装が終わっていないので、このテストは落ちます。実装側も書きましょう。
link:./projects/myecho/src/myecho_02_standard_output.cr[role=include]
-
インスタンス変数
@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 しましょう。
link:./projects/myecho/src/cli.cr[role=include]
これで、モジュールと 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!
さらに CLI ツールらしくしていきましょう。バージョンを表示させる -v
オプションを実装します。オプションがあると一気に CLI ツールらしくなりますね。Crystal には OptionParser
というクラスが用意されています。コマンドラインオプションを扱うのに便利なクラスです。今回は OptionParser
の使い方も解説しつつ実装していきます。まずはテストから書きましょう。
link:./projects/myecho/spec/myecho_03_display_version_v_spec.cr[role=include]
-
出力する先の
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
を導入します。
link:./projects/myecho/src/myecho_03_display_version_v.cr[role=include]
-
標準ライブラリの
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)
(スタックトレースが続く ... )
では対応していきましょう。まずはテストから書きます。
link:./projects/myecho/spec/myecho_04_display_version_version_spec.cr[role=include]
-
--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
を指定します。
link:./projects/myecho/src/myecho_04_display_version_version.cr[role=include]
-
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
は、コマンドラインオプションを柔軟に扱えます。
次にヘルプを表示してみます。オプションの追加はバージョン表示の時と同じです。まずはテストから書きましょう。
link:./projects/myecho/spec/myecho_05_display_help_specified_string_spec_failed.cr[role=include]
-h
を指定した場合、何が表示されるかはまだわかりません。とりあえずこのまま進みましょう。現状だと、 Invalid option: -h
でテストが落ちることは目に見えています。まずは適当に文字列を返しましょう。
link:./projects/myecho/src/myecho_05_display_help_specified_string.cr[role=include]
...
link:./projects/myecho/src/myecho_05_display_help_specified_string.cr[role=include]
当然、テストは失敗します。
$ 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
で、ヘルプメッセージを返してくれます。実装してみましょう。
link:./projects/myecho/src/myecho_06_display_help_formatted.cr[role=include]
...
link:./projects/myecho/src/myecho_06_display_help_formatted.cr[role=include]
...
link:./projects/myecho/src/myecho_06_display_help_formatted.cr[role=include]
テストを回します。
$ 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'
それらしい文字列が返ってきました。テストを修正しましょう。
link:./projects/myecho/spec/myecho_06_display_help_formatted_spec.cr[role=include]
今度はテストが回りました。
$ 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 などがあると良さそうです。テストを書きましょう。
link:./projects/myecho/spec/myecho_07_display_help_banner_spec.cr[role=include]
良い感じのヘルプにしてみました。テストが通るように実装しましょう。OptionParser
には、 #banner=
というメソッドがあり、ヘルプメッセージに加える文字列を定義できます。
link:./projects/myecho/src/myecho_07_display_help_banner.cr[role=include]
これでテストを通すことができました。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
を使えば、このように簡単にヘルプを設定できます。
もっと CLI ツールらしくするために、さらにオプションを加えましょう。各コマンド引数に prefix をつける --prefix
オプションです。期待される動作は次のようになります。
$ ./bin/myecho --prefix pre_ foo bar baz
pre_foo pre_bar pre_baz
--prefix PREFIX
を指定することで、各引数の先頭に PREFIX
を付けます。まずはテストから書きましょう。
link:./projects/myecho/spec/myecho_08_prefix_spec.cr[role=include]
このテストを通すように実装しましょう。
link:./projects/myecho/src/myecho_08_prefix.cr[role=include]
...
link:./projects/myecho/src/myecho_08_prefix.cr[role=include]
...
link:./projects/myecho/src/myecho_08_prefix.cr[role=include]
...
link:./projects/myecho/src/myecho_08_prefix.cr[role=include]
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
のコード全てを記載します。
link:./projects/myecho/src/cli.cr[role=include]
link:./projects/myecho/src/myecho.cr[role=include]
link:./projects/myecho/spec/myecho_spec.cr[role=include]
次は、もう少しかゆいところに手が届く、サードパーティ製の CLI ビルダーライブラリをいくつかご紹介します。
CLI ツールを作成するにあたって、サードパーティ製の CLI ビルダーツールを使うことは得策です。OptionParser
よりもリッチな機能を使うことができます。例えば、 Ruby だと thor などが有名です。Crystal でも CLI ビルダーツールが作成されています。今回は主観で選択したいくつかをご紹介します。題材としては、ここまで作ってきた myecho
を用います。
早い時期から作成されていたライブラリです。百聞は一見に如かず、早速 myecho
を実装しましょう。
link:./projects/myecho_commander/src/myecho_commander.cr[role=include]
サードパーティ製のライブラリは、基本的に次の構成になっています。
-
コマンドの説明(
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
期待通りの動作をしていることがわかります。
最近も開発されているライブラリです。早速コードを見てみましょう。
link:./projects/myecho_admiral/src/myecho_admiral.cr[role=include]
行数が少ないですね。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
こちらも期待通りの動作をしています。
これは私自身が作成したライブラリです。記述量の少なさと直感的な記述を目的に作成しています。コードを見てみましょう。
link:./projects/myecho_clim/src/myecho_clim.cr[role=include]
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 に触れてみてください。