1秒でも早くCLIツールを作りたい by Crystal
この記事の内容は古いので、以下の記事でupdateした内容を紹介しています。
以下、古い記事です↓↓↓↓↓
世の中、1秒でも早くCLIツールを作りたいときってありますよね?
そんな方のために、Crystalのライブラリを作成しました。
早く作りたい
早く作りたい人のために、記述量が少なく、しかし、必要な情報は書けるよう、シンプルなDSLを目指しました。
最小のサンプルは以下です。
hello01.cr
require "clim" module Hello class Cli < Clim main_command run do |opts, args| puts "Hello, #{args.first?}!" end end end Hello::Cli.start(ARGV)
$ crystal build src/hello01.cr $ ./hello01 Taro Hello, Taro!
引数を取って、run
ブロックの中が実行されています。
これらに加え、付加情報としてdesc
やusage
、オプションとしてstring
bool
array
が使えます。
hello02.cr
require "clim" module Hello class Cli < Clim main_command desc "Hello CLI tool." usage "hello [options] [arguments] ..." array "-n NAME", "--name=NAME", desc: "Target name.", default: [] of String string "-g WORDS", "--greeting=WORDS", desc: "Words of greetings.", default: "Hello" run do |opts, args| puts "#{opts.s["greeting"]}," puts "#{opts.a["name"].join(", ")}!" end end end Hello::Cli.start(ARGV)
$ crystal build src/hello02.cr $ ./hello02 -n Taro -n Miko -g 'Good night' Good night, Taro, Miko!
array
はオプションを並べることで全てを配列として受け取ることができます。
helpは以下のとおりです。
$ ./hello02 -h Hello CLI tool. Usage: hello [options] [arguments] ... Options: -h, --help Show this help. -n NAME, --name=NAME Target name. [default:[]] -g WORDS, --greeting=WORDS Words of greetings. [default:Hello]
サブコマンドは以下のように sub do ... end
ブロック内に書きます。
hello03.cr
require "clim" module Hello class Cli < Clim main_command desc "Hello CLI tool." usage "hello [options] ..." string "-n NAME", "--name=NAME" run do |opts, args| # Put your main command code here. end sub do command "sub_command1" desc "Sub command1." usage "hello sub_command1 [options] ..." string "-n NAME", "--name=NAME" run do |opts, args| # Put your sub command1 code here. end command "sub_command2" desc "Sub command2." usage "hello sub_command2 [options] ..." string "-n NAME", "--name=NAME" run do |opts, args| # Put your sub command2 code here. end end end end Hello::Cli.start(ARGV)
$ crystal build src/hello03.cr $ ./hello03 -h Hello CLI tool. Usage: hello [options] ... Options: -h, --help Show this help. -n NAME, --name=NAME Sub Commands: sub_command1 Sub command1. sub_command2 Sub command2.
さらに深くネストすることも可能です。
ざっくりとしたDSLの説明はこのくらいです。(これで全部説明したようなものです)
このくらいの記述量ならスピーディーにコーディングできるのではないでしょうか。ライブラリに気を遣うことなく、メインの処理に集中できます。
もっと早く作りたい
もっと早く作りたいですか?このDSLを書くのも億劫ですか?そんなあなたには clim init
コマンドがあります。
climには CLI tool もあります。
https://github.com/at-grandpa/clim-toolsgithub.com
手動でbuildしてPathの通っているところに配置してください。以下は一例です。
$ git clone https://github.com/at-grandpa/clim-tools.git $ cd clim-tools $ crystal build src/tools.cr -o /usr/local/bin/clim
clim
コマンドが使えるようになりました。
$ clim -h Clim command line interface tools. Usage: clim [sub-command] [options] ... Options: -h, --help Show this help. Sub Commands: init Creates CLI tool skeleton. direct Directly build the crystal code.
clim init
を見てみます。
$ clim init -h Creates CLI tool skeleton. Usage: clim init command-name [options] ... Options: -h, --help Show this help. -e CODE, --eval=CODE Code to insert into the run block. [default:puts opts.help] -s NAME:DESC, --string=NAME:DESC Add "string" option. [default:[]] -b NAME:DESC, --bool=NAME:DESC Add "bool" option. [default:[]] -a NAME:DESC, --array=NAME:DESC Add "array" option. [default:[]]
なにやらごちゃごちゃ書いてありますが、要は、
- CLIツールを作成するための雛形を生成する
--string=NAME:DESC
などを指定すれば、オプションも雛形に記述してくれる-e
オプションで指定したコードがrun do ... end
ブロックの中に記述される
というものです。やってみましょう。
$ clim init hello04 -a name:'Target name.' -s greeting:'Words of greetings.' -e 'puts "#{opts.s["greeting"]},\n#{opts.a["name"].join(", ")}!"' create hello04/.gitignore create hello04/LICENSE create hello04/README.md create hello04/.travis.yml create hello04/shard.yml create hello04/src/hello04.cr create hello04/src/hello04/version.cr create hello04/spec/spec_helper.cr create hello04/spec/hello04_spec.cr Initialized empty Git repository in /Users/y-tsuchida/lt36/hello04/.git/ clim update /path/to/hello04/shard.yml clim update /path/to/hello04/src/hello04.cr $ cd hello04 $ crystal dep Updating https://github.com/at-grandpa/clim.git Installing clim (master) $ crystal build src/hello04.cr $ ./hello04 -h Command Line Interface. Usage: hello04 [options] [arguments] Options: -h, --help Show this help. -g VALUE, --greeting=VALUE Words of greetings. -n VALUE, --name=VALUE Target name. [default:[]] $ ./hello04 -n Taro -n Miko -g 'Good night' Good night, Taro, Miko!
雛形が生成され、buildも通り、helpも表示され、実行もできていることがわかります。
早い。エディタ開いてない。
もっともっと早く作りたい
もっともっと早く作りたいですか?雛形生成とか面倒くさいですか?そんなあなたには clim direct
コマンドがあります。
$ clim direct -h Directly build the crystal code. Usage: clim direct [command-name] [options] ... Options: -h, --help Show this help. -o FILE, --output=FILE Output filename. [default:/tmp/crystal.out] -e CODE, --eval=CODE Crystal code to evaluation. [default:puts "Hello, world!!"] -r, --release Compile in release mode. [default:false] -c, --clim Use clim library. [default:false] -s NAME:DESC, --string=NAME:DESC Add "string" option. (with "-c") [default:[]] -b NAME:DESC, --bool=NAME:DESC Add "bool" option. (with "-c") [default:[]] -a NAME:DESC, --array=NAME:DESC Add "array" option. (with "-c") [default:[]]
さらにごちゃごちゃしてますが、要は、
- 雛形とかもういいから、直接バイナリ作るよ
というものです。やってみましょう。
フィボナッチのバイナリをつくりますか。
$ clim direct -o ./fib -e 'def fib(n); return n if n < 2; fib(n - 2) + fib(n - 1); end; puts fib(ARGV[0].to_i32)' ./fib $ ./fib 10 55
早い。一発。
しかし、今まではちゃんと「helpなどが充実したCLIツール」を作ってきました。clim direct
でもリッチなCLIツールを作りたいですよね?
そんなあなたには --clim
オプションがあります。
先程のフィボナッチ生成ではclimライブラリは使っていませんでしたが、--clim
オプションを付ければ使用できます。clim init
のようなオプションの指定もできます。
./hello05
のバイナリを作ってみましょう。
$ clim direct hello05 -a name:'Target name.' -s greeting:'Words of greetings.' -e 'puts "#{opts.s["greeting"]},\n#{opts.a["name"].join(", ")}!"' --clim -o ./hello05 ./hello05 $ ./hello05 -h Command Line Interface. Usage: hello05_tmp [options] [arguments] Options: -h, --help Show this help. -g VALUE, --greeting=VALUE Words of greetings. -n VALUE, --name=VALUE Target name. [default:[]] $ ./hello05 -n Taro -n Miko -g 'Good night' Good night, Taro, Miko!
一発でバイナリを生成でき、かつ、climの機能もあります。
早い。
まとめ
今回は「自分にとって気持ちいいツール」を目指しました。ざっと振り返ってみます。
- 気持ちのいいDSLを始めに決め、それを実現するためにコードを書くというアプローチは良かった
- 目的があるのでモチベーションを保てる
- 「パッとコマンドを追加したい」ときにすぐに追加できるのは気持ちいい
- それでいて、helpも整備されるので気持ちいい
- 当初、サブコマンドは複雑になるからやめようと思っていたが、実装したほうがコードがシンプルになりそうだったので実装した
- 余分だなと思った機能は省きまくった
- コマンドのaliasやIntのサポートなど
- ツールは出来る限りシンプルにしたい
- もっとできるはず
- crystalのシンタックスはrubyと似ているので、記述量を減らしやすいと思う
- かつ、エラーをコンパイル時点で弾けるのは開発しやすかった
- 始め、「DSLならcrystalのmacroだろー!」と思ってウキウキしていたが、蓋を開けてみればmacroはほとんど使わなかった
- 「使いたいだけ」だと余計に複雑になってしまいそう
- 「使いドコロ」があるのでその知識は身につけたい
- crystalは標準ライブラリも普通に充実しているので、いろいろ使えた
- 今回はひと言で言うと「
OptionParser
のwrapper」
- 今回はひと言で言うと「
- CLIツールはドッグフーディングしやすいので良い
- 今回も自分のライブラリを使ってツールを作った
- バグ発見のためにドッグフーディングはホント良い
- 他の言語でも雛形生成ライブラリは存在するが、さらに早いものを作ってみたかった
- 自己満足のアレが強い
- 他の言語でもこれくらい早く作れるライブラリがあるのか探してみる
- もっとシンプルに楽にできないかなー
- そもそも「秒速でCLIツールを作りたい」という要望はあるのか
- それは言わない約束
とにかく楽しかったです。あとはこれをもっと洗練させていって、あわよくばどこかで使いたいなと画策しています。