圧倒亭グランパのブログ

30年後の自分にもわかるように書くブログ

1秒でも早くCLIツールを作りたい by Crystal

この記事の内容は古いので、以下の記事でupdateした内容を紹介しています。

at-grandpa.hatenablog.jp

以下、古い記事です↓↓↓↓↓

   

世の中、1秒でも早くCLIツールを作りたいときってありますよね?

そんな方のために、Crystalのライブラリを作成しました。

github.com

 

早く作りたい

早く作りたい人のために、記述量が少なく、しかし、必要な情報は書けるよう、シンプルな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ブロックの中が実行されています。

これらに加え、付加情報としてdescusage、オプションとして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ツールを作りたい」という要望はあるのか
    • それは言わない約束

とにかく楽しかったです。あとはこれをもっと洗練させていって、あわよくばどこかで使いたいなと画策しています。