Crystal 言語で CLI ツールを書いてみる

Crystal はつくりかけの言語です.この記事は 0.8.0 を元に書きましたが,今後かなり変更されることが予想されます
Crystal は Ruby に強くインスパイアされたコンパイル言語です.結構前から気になっていて,Crystal のコンパイラが Ruby で書かれていた頃(今はセルフホストしています)から見ていて最近はぼちぼちコントリビュートしたりしてます.インスパイアされているというだけで,色々 Ruby と違うところもあります(例えばいま話題の文字列は Crystal では全て immutable です)
僕は Ruby が好きなので,ちょっとしたツールや書き捨てのスクリプトは特に理由が無い場合は Ruby で書いています.それもあって Crystal でちょっとしたツールを書くのは次のような利点があります.
- Ruby と同じように楽に(そして雑に)書ける
- Ruby とは違い実行速度が速い(まだあまり実感してないですが)
- Ruby とは違いコンパイル時に型チェックが入る(各式が取り得る型の union type で型付けします)
- Ruby とは違いバイナリファイル1つになる(共有ライブラリなどを使わなければ)
コマンドラインツールは Crisp (Crystal で書かれた Lisp 処理系)や crdoc (コマンドラインから Crystal のドキュメントを検索&開くツール)をつくってみたので,今回は自分なりのやり方の手順をまとめてみます.
といっても大体 Ruby と同じですが…
準備
Go 言語における go コマンドのように,Crystal にも crystal コマンドがあります. 公式のインストールページ に従ってインストールします.残念ながらまだ Windows では利用できません(対応はぼちぼち進んでいるようです).
次に開発環境を入れます.お好みに合わせて導入してください.(もし需要があれば開発ツール周りもブログに書きたい.)
- Atom: language-crystal-actual
- Sublime Text: sublime-crystal
- Vim: vim-crystal
- Emacs: emacs-crystal-mode
リポジトリ生成
試しにファイルの中身を表示するだけのコマンド mycat をつくってみます.
まずは crystal init コマンドで初期のリポジトリを生成します.
$ crystal init app mycat
次のようなディレクトリ構成が自動でつくられます.
--- mycat
|--- .gitignore
|--- LICENSE
|--- README.md
|--- .travis.yml
|--- shard.yml
|--- src
| |--- mycat.cr
| |--- mycat/version.cr
|
|--- spec
| |--- spec_helper.cr
| |--- mycat_spec.cr
|
|--- .gitignore
僕はコマンドラインツールを書く時はなるべくライブラリとしても使えるようにつくるので,src/mycat.cr は API だけ書いて,bin/mycat.cr にコマンド実行処理を書きます.そのほうがテストもしやすいです.
最終的にはこんな感じです.
--- mycat
|--- .gitignore
|--- LICENSE
|--- README.md
|--- .travis.yml
|--- shard.yml
|--- bin
| |--- mycat.cr
|
|--- src
| |--- mycat.cr
| |--- mycat/version.cr
|
|--- spec
| |--- spec_helper.cr
| |--- mycat_spec.cr
|
|--- .gitignore
ドキュメントを書く
最初の時点で脳内で決まっている範囲で簡単なドキュメント(intro,コマンドの引数など)を README.md に書きます.生成された README.md はスニペットになっているので,GitHub のユーザ名などを自分で埋めます.
また,(なぜか)LICENSE が MIT で決め打ちになっているので,必要があれば修正します.
shard.yml を書く
npm の package.json と同じように,パッケージ情報を shard.yml に書きます.これは半標準(いずれ本体に取り込まれる?)のパッケージマネージャ shards を使う前提で生成されています.
name: mycat version: 0.0.1 authors: - 名前 <mail@address> license: MIT
依存するパッケージが出てきた時はここに書きます.
スペックを書く
まずは mycat ディレクトリでおもむろに crystal spec と実行してみます.
次のように失敗すると思います.
F
Failures:
1) Mycat works
Failure/Error: false.should eq(true)
expected: true
got: false
# ./spec/mycat_spec.cr:7
Finished in 0.55 milliseconds
1 examples, 1 failures, 0 errors, 0 pending
Failed examples:
crystal spec ./spec/mycat_spec.cr:6 # Mycat works
それも当然です.spec/mycat_spec.cr は次のようになっています.
require "./spec_helper" describe Mycat do # TODO: Write tests it "works" do false.should eq(true) end end
ここで実装を考えながら必要なメソッドのテストを書きます.ファイル名を受け取って IO に出力に出すメソッド meow を持ったクラス Cat をつくることにするとこんな感じでしょうか.test.txt は spec ファイルと同じディレクトリに適当につくります.__DIR__ はそのファイルの親ディレクトリを表す大域定数(みたいなもの)です.
require "./spec_helper" describe Mycat do describe Mycat::Cat do describe "meow" do it "writes the content of file to specified IO" do io = StringIO.new c = Mycat::Cat.new io c.meow "#{__DIR__}/test.txt" io.to_s.should eq("foo\nbar\nbaz\n") end end end end
まだ実装が無いのでもちろんこれも失敗します.
実装を書く
require "./mycat/*" module Mycat # TODO Put your code here end
最初はこうなっていると思います.
require "./mycat/*" module Mycat class Cat def initialize(@mouse) end def meow(file_name) File.open file_name do |f| @mouse << f.read end end end end
できました.受け取った IO にファイルの中身を吐くだけなので簡単ですね.コンストラクタの引数のように,直接インスタンス変数名を指定してその変数に代入することができます.(コンストラクタ以外のメソッドでも使えます)
試しにビルドしてみましょう.crystal build コマンドを使います.
$ crystal build src/mycat.cr
問題が無ければ,先ほどと同じ方法で spec を実行します.今度はテストが通ると思います.
コマンド実行処理をつくる
bin/mycat.cr を実装します.
require "../src/mycat" require "option_parser" begin OptionParser.parse! do |parser| parser.banner = "Usage: mycat file_name" parser.on("-v", "--version", "Show version") { puts Mycat::VERSION; exit 0 } parser.on("-h", "--help", "Show this help") { puts parser; exit 0 } end cat = Mycat::Cat.new STDOUT ARGV.first?.try do |arg| cat.meow arg end rescue e STDERR.puts e exit 1 end
オプションの実装は Ruby でもおなじみの OptionParser が使えます.今回はバージョンとヘルプだけをパースしています.
Enumerable#first? はリストの最初の要素があればそれを返し,無ければ nil を返します.Object#try は Ruby ではお馴染みの,nil で無い値で実行すればブロック部分が実行されるメソッドです.例えばここで間違えて cat.meow ARGV.first? など書くとコンパイラエラーになってくれます.(コンパイラは Cat#meow(f: String) に推論したのに String? な値を渡しているため)
試しに実行してみましょう.crystal コマンドはデフォルトでファイルを直接実行する(実際はコンパイルして一時ファイルを生成しそれを実行する)ので,下記のようにします.crystal コマンドで実行されるプログラム側に引数を与えるには -- オプションの後に置く必要があります.
$ crystal ./bin/mycat.cr -- README.md $ crystal ./bin/mycat.cr -- -v $ crystal ./bin/mycat.cr -- --help
正しく動いていることが確認できたらリリースビルドで最適化されたバイナリを作成します.
$ crystal build --release ./bin/mycat.cr
カレントディレクトに mycat というバイナリファイルができていると思うので,それをパスの通ったところに置くなりします.
Travis での CI
デフォルトで .travis.yml が生成されますが,中身はほぼ空っぽです.
次のようにテストスクリプトを書きます.
Crystal の CI 環境は Crystal コミュニティによって提供されているので,こちらで頑張って用意する必要はありません.
language: crystal script: - crystal spec - crystal build ./bin/mycat.cr
まとめ
プログラミング言語 Crystal でコマンドラインツールを書く自分なりの手順をまとめました.まだ開発途中の言語なのでこれを使ってでかいツールを…とはなかなかいかないかもしれませんが,僕みたいに Ruby 好きな人は書いてみるとハードルも低く新鮮で楽しいかもしれません.