圧倒亭グランパのブログ

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

Crystal言語用のCLIビルダーライブラリを、より使いやすくアップデートしました

Versionは 0.2.0 になります。

github.com

前回紹介した記事はこちらです。

at-grandpa.hatenablog.jp

 

アップデート内容は「オプションの型が一意に決まるようにした」です。

オプションの型が一意に決まるようにした

これを実現するために、構文が変わっています。

 

今までの構文はこちらです。 main_command を宣言した上で、descstringなどのオプションを定義していきました。

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_commandsub_command のブロックの中に、また sub_commandをネストするだけです。descoptionDSLも使用できます。構造上は、一応どこまでも深いサブコマンドを定義できます。

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ツールを作る時は積極的にドッグフーディングしていこうと思います。