Crystalの自作CLIビルダーライブラリを使ってgitコマンド風のCLIツールを作ってみる
この記事は Crystal Advent Calendar 2018 の3日目の記事です。
自作のCrystalのCLIビルダーライブラリをアップデートしたので、AdventCalendarに便乗して紹介します。
githubは以下です。 clim
と言います。簡潔に、直感的にCLIツールを書けることを目指しています。
過去に紹介したブログ記事はこちらです。
この記事が古くなったのでupdate記事です。
目次
gitコマンドっぽいものを作ってみる
実際にgitっぽいCLIツールを作ってみます。
前準備
crystalのバージョンは 0.27.0
です。
$ crystal -v Crystal 0.27.0 (2018-11-04) LLVM: 6.0.1 Default target: x86_64-apple-macosx
crystalのinstallや仕様は、公式のドキュメントを御覧ください。
まずは crystal init
コマンドで雛形を生成します。アプリ名は my_git
とでもしましょう。
$ crystal init app my_git create my_git/.gitignore create my_git/.editorconfig create my_git/LICENSE create my_git/README.md create my_git/.travis.yml create my_git/shard.yml create my_git/src/my_git.cr create my_git/spec/spec_helper.cr create my_git/spec/my_git_spec.cr Initialized empty Git repository in /path/to/src/my_git/.git/ $ cd my_git $ tree -a -I .git . ├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── shard.yml ├── spec │ ├── my_git_spec.cr │ └── spec_helper.cr └── src └── my_git.cr 2 directories, 9 files
次に、 clim
を使用するため、 shard.yml
に以下の記述をします。diffを示します。
diff --git a/shard.yml b/shard.yml index 3b531ee..7eb70b2 100644 --- a/shard.yml +++ b/shard.yml @@ -11,3 +11,8 @@ targets: crystal: 0.27.0 license: MIT + +dependencies: + clim: + github: at-grandpa/clim + version: 0.4.1
記述したら shards install
コマンドでインストールします。
$ shards install Fetching https://github.com/at-grandpa/clim.git Installing clim (0.4.1) $
インストールできました。これで clim
を使用できます。
ではまず、 Hello world!
を表示するCLIツールを作成してみましょう。編集するファイルは src/my_git.cr
です。
src/my_git.cr
require "clim" # ライブラリを require module MyGit # 継承して使う class Cli < Clim VERSION = "0.1.0" # CLIツールのメインのコマンドを定義 main do # run ブロックが実際に実行される run do |opts, args| puts "Hello world!! #{args.join(", ")}!" end end end end MyGit::Cli.start(ARGV)
$ crystal build src/my_git.cr -o mygit $ ./mygit Taro Miko Hello world!! Taro, Miko!
コマンドの引数が args
に入っていることがわかります。 args
の型は Array(String)
です。
さらにもっと拡張していきましょう。
versionを表示する
以下を目指します。
$ ./mygit --version mygit version 0.1.0 $ ./mygit -v mygit version 0.1.0
clim
では version
ディレクティブを使用するだけで実装できます。
require "clim" module MyGit class Cli < Clim VERSION = "0.1.0" main do # runブロックの上にコマンドの定義を書いていく # `--version` で出力する文字列を引数にとる # short: "-v" を書くと `-v` でも出力される version "mygit version #{VERSION}", short: "-v" run do |opts, args| puts "Hello world!! #{args.join(", ")}!" end end end end MyGit::Cli.start(ARGV)
$ crystal build src/my_git.cr -o mygit $ ./mygit --version mygit version 0.1.0 $ ./mygit -v mygit version 0.1.0
--version
オプションを簡単に実装できました。
helpを充実させる
clim
は標準で --help
オプションを搭載しています。いい感じにhelpを表示してくれます。そのための情報を定義しましょう。
require "clim" module MyGit class Cli < Clim VERSION = "0.1.0" main do # `desc` や `usage` でhelpの内容を記述 desc "my git command." usage "mygit [command] [arguments] [options]" version "mygit version #{VERSION}", short: "-v" run do |opts, args| puts "Hello world!! #{args.join(", ")}!" end end end end MyGit::Cli.start(ARGV)
desc
はコマンドの説明、 usage
は使い方を記述します。こうすると --help
で次のように出力されます。
$ ./mygit --help my git command. Usage: mygit [command] [arguments] [options] Options: --help Show this help. -v, --version Show version.
desc
の内容や usage
の内容に加え、オプションの説明も表示してくれます。便利ですね。
オプションを追加する
オプションを定義するには option
ディレクティブを使用します。
require "clim" module MyGit class Cli < Clim VERSION = "0.1.0" main do desc "my git command." usage "mygit [command] [arguments] [options]" version "mygit version #{VERSION}", short: "-v" # オプションを定義 option "-n NAME", "--namespace=NAME", # オプション名 type: String, # 型 desc: "namespace.", # 説明 default: "default_namespace", # デフォルト値 required: false # 必須かどうか # 複数登録もできる option "-e PATH", "--exec-path=PATH", type: String, desc: "exec path.", required: false run do |opts, args| # オプション名でメソッド呼び出しできる unless opts.namespace.nil? puts "namespace is #{opts.namespace}." end unless opts.exec_path.nil? puts "exec path is #{opts.exec_path}." end puts "Hello world!! #{args.join(", ")}!" end end end end MyGit::Cli.start(ARGV)
$ crystal build src/my_git.cr -o mygit $ ./mygit -n my_namespace Taro -e my_exec_path namespace is my_namespace. exec path is my_exec_path. Hello world!! Taro!
オプションの実装も簡単にできました。 run
ブロックに渡される opts
引数を介して値を取得します。 opts
にはオプション名のメソッドが生えるので、それを呼び出します。 もちろん型も決まっています。
--help
にもちゃんとオプションの説明が記載されます。
$ ./mygit --help my git command. Usage: mygit [command] [arguments] [options] Options: -n NAME, --namespace=NAME namespace. [type:String] [default:"default_namespace"] -e PATH, --exec-path=PATH exec path. [type:String] --help Show this help. -v, --version Show version.
サブコマンドを定義する
gitはサブコマンドがたくさんあります。 mygit
にもサブコマンドを実装しましょう。定義は簡単です。 sub "{サブコマンド名}"
ブロックを親コマンドの run
ブロックの直後に置き、メインのコマンドと同じように定義します。例えば mygit log
というサブコマンドを実装しましょう。
require "clim" module MyGit class Cli < Clim main do # ... run do |opts, args| # ... end sub "log" do desc "my git log command." usage "mygit log [arguments] [options]" option "-q", "--quiet", type: Bool, desc: "suppress diff output." run do |opts, args| puts "[quiet mode]" if opts.quiet puts "Hello world!! #{args.join(", ")}!" end end end end end MyGit::Cli.start(ARGV)
$ crystal build src/my_git.cr -o mygit $ ./mygit log --help my git log command. Usage: mygit log [arguments] [options] Options: -q, --quiet suppress diff output. [type:Bool] --help Show this help. $ ./mygit log -q Taro [quiet mode] Hello world!! Taro!
sub "{サブコマンド名}"
のブロック内は、今までのコマンド定義と全く同じです。サブコマンドの run
ブロックの下に、さらに sub "{サブコマンド名}"
ブロックを記述すれば「サブサブコマンド」も実装できます。入れ子に制限はありません。「サブサブサブ...コマンド」も簡単に実装できます。
全コード
最終的に、 mygit log
と mygit branch
を実装しました。また、見やすいように以下のことを行いました。
run
ブロックで実行する処理はMyGit::Command
クラスに委譲- mainのコマンドを実行するとhelpを表示
ディレクトリ構成は以下です。
. ├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── shard.lock ├── shard.yml ├── spec │ ├── my_git_spec.cr │ └── spec_helper.cr └── src ├── my_git │ └── command.cr └── my_git.cr
src/my_git/command.cr
を追加しました。各ファイルは次のようになっています。
src/my_git.cr
require "clim" require "./my_git/*" module MyGit class Cli < Clim VERSION = "0.1.0" main do desc "my git command." usage "mygit [command] [arguments] [options]" version "mygit version #{VERSION}", short: "-v" run do |opts, args| MyGit::Command.main(opts, args) end sub "log" do desc "my git log command." usage "mygit log [arguments] [options]" option "-q", "--quiet", type: Bool, desc: "suppress diff output.", required: false run do |opts, args| MyGit::Command.log(opts, args) end end sub "branch" do desc "my git branch command." usage "mygit branch [arguments] [options]" option "-m NAME", "--move=NAME", type: String, desc: "Move/rename a branch.", required: false run do |opts, args| MyGit::Command.branch(opts, args) end end end end end MyGit::Cli.start(ARGV)
src/my_git/command.cr
module MyGit class Command def self.main(opts, args) # opts.helpでhelpのStringが得られます puts opts.help end def self.log(opts, args) puts "[quiet mode]" if opts.quiet puts "Hello world!! #{args.join(", ")}!" end def self.branch(opts, args) unless opts.move.nil? puts "Move/rename: #{opts.move}" end puts "Hello world!! #{args.join(", ")}!" end end end
実行結果は以下です。各コマンドを叩いてみました。
$ crystal build src/my_git.cr -o mygit $ ./mygit my git command. Usage: mygit [command] [arguments] [options] Options: --help Show this help. -v, --version Show version. Sub Commands: log my git log command. branch my git branch command. $ ./mygit -v mygit version 0.1.0 $ ./mygit log --help my git log command. Usage: mygit log [arguments] [options] Options: -q, --quiet suppress diff output. [type:Bool] --help Show this help. $ ./mygit log --quiet Taro [quiet mode] Hello world!! Taro! $ ./mygit branch --help my git branch command. Usage: mygit branch [arguments] [options] Options: -m NAME, --move=NAME Move/rename a branch. [type:String] --help Show this help. $ ./mygit branch -m new_name Taro Move/rename: new_name Hello world!! Taro!
いい感じですね。
その他の機能
その他、 clim
は以下のような機能があったりします。
help_template
- 自由にhelpの形式をカスタマイズできる
alias
- サブコマンドの定義の部分で
alias "hoge", "fuga"
と書けば、hoge
fuga
でコマンドを実行できる
- サブコマンドの定義の部分で
更に詳しくは、 README をみてください。
たまにupdateします
なにか欲しい機能を思いついたらupdateしていきます。ご意見大歓迎です。Crystalはサクッとかけて、コンパイルできて、バイナリ吐けて、便利ですねー(宣伝)!
というか、みなさんCrystal書いてみてはどうですか!!!!!
触った感想など、AdventCalendarに書いてみてはどうでしょう!!!!!
ぜひに!!!!!(必死