この記事は 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"と書けば、hogefugaでコマンドを実行できる
- サブコマンドの定義の部分で
更に詳しくは、 README をみてください。
たまにupdateします
なにか欲しい機能を思いついたらupdateしていきます。ご意見大歓迎です。Crystalはサクッとかけて、コンパイルできて、バイナリ吐けて、便利ですねー(宣伝)!
というか、みなさんCrystal書いてみてはどうですか!!!!!
触った感想など、AdventCalendarに書いてみてはどうでしょう!!!!!
ぜひに!!!!!(必死