Crystal言語用のCLIビルダーライブラリを、より使いやすくアップデートしました
Versionは 0.2.0
になります。
前回紹介した記事はこちらです。
アップデート内容は「オプションの型が一意に決まるようにした」です。
オプションの型が一意に決まるようにした
これを実現するために、構文が変わっています。
今までの構文はこちらです。 main_command
を宣言した上で、desc
やstring
などのオプションを定義していきました。
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| print "#{opts["greeting"].as(String)}, " print "#{opts["name"].as(Array(String)).join(", ")}!" print "\n" end end end Hello::Cli.start(ARGV)
これだと、opts["greeting"]
がUnion型((String | Bool | Array(String) | Nil)
)なので、一度 .as(String)
しないと String型として扱えなかったりします。これはちょっと不便です。
アップデート後はこちらです。
equire "clim" module Hello class Cli < Clim main_command do desc "Hello CLI tool." usage "hello [options] [arguments] ..." version "Version 0.1.0" option "-g WORDS", "--greeting=WORDS", type: String, desc: "Words of greetings.", default: "Hello" option "-n NAME", "--name=NAME", type: Array(String), desc: "Target name.", default: ["Taro"] run do |options, arguments| print "#{options.greeting}, " print "#{options.name.join(", ")}!" print "\n" end end end end Hello::Cli.start(ARGV)
$ crystal build src/hello.cr $ ./hello --help Hello CLI tool. Usage: hello [options] [arguments] ... Options: -g WORDS, --greeting=WORDS Words of greetings. [type:String] [default:"Hello"] -n NAME, --name=NAME Target name. [type:Array(String)] [default:["Taro"]] --help Show this help. --version Show version. $ ./hello -n Ichiro -n Miko -g 'Good night' Good night, Ichiro, Miko!
main_command
がブロックになっています。これは、オプションの型を一意にするためです。結果、 options.greeting
の部分はメソッド呼び出しになっており、型も String型になっています。とてもスッキリし、扱いやすくなりました。
以前の構文では、各DSLはクラスメソッドであり、そこで各コマンドのオブジェクトをひたすらクラス変数に突っ込んでいくという構造でした。これだとどうしても型の定義が難しかったので、大きな変更を余儀なくされました。
新しい構文では、各DSLはマクロです。例えば、main_command
のマクロを展開すると、Commandクラスを継承したクラス定義に展開されます。オプションの定義もマクロで、Optionsクラスの定義に展開されます。この時、プロパティの定義部分に DSLで指定した type: {型}
を当てはめています。結果、オプションのメソッド呼び出しでプロパティを呼び出すことで、型が一意に決まるというわけです。
これらを一言で言うと、「大量のマクロが展開されて、DSLで指定した型に沿った『巨大なクラス』が定義される」という感じです。
サブコマンドも書けます。
require "clim" module FakeCrystalCommand class Cli < Clim main_command do desc "Fake Crystal command." usage "fcrystal [sub_command] [arguments]" run do |options, arguments| puts options.help # => help string. end sub_command "tool" do desc "run a tool" usage "fcrystal tool [tool] [arguments]" run do |options, arguments| puts "Fake Crystal tool!!" end sub_command "format" do desc "format project, directories and/or files" usage "fcrystal tool format [options] [file or directory]" run do |options, arguments| puts "Fake Crystal tool format!!" end end end sub_command "spec" do desc "build and run specs" usage "crystal spec [options] [files]" run do |options, arguments| puts "Fake Crystal spec!!" end end end end end FakeCrystalCommand::Cli.start(ARGV)
main_command
や sub_command
のブロックの中に、また sub_command
をネストするだけです。desc
やoption
のDSLも使用できます。構造上は、一応どこまでも深いサブコマンドを定義できます。
optionの type: {型}
に指定できる型も多く対応しました。追加が簡単になったので一気に追加してしまいました。
- Int8
- Int16
- Int32
- Int64
- UInt8
- UInt16
- UInt32
- UInt64
- Float32
- Float64
- String
- Bool
- Array(Int8)
- Array(Int16)
- Array(Int32)
- Array(Int64)
- Array(UInt8)
- Array(UInt16)
- Array(UInt32)
- Array(UInt64)
- Array(Float32)
- Array(Float64)
- Array(String)
これだけあれば十分だろうという感じですね。
また、
default
が指定されなかったら nilableな型((String | Nil)
のようなもの)default
が指定されたら nil は許可しない (String
型)
という対応もしました。直感的にはこうだろうなと思ったからです。詳しくはREADMEを御覧ください。
直感的に書けて、型のサポートも充実しました。今後、CLIツールを作る時は積極的にドッグフーディングしていこうと思います。