Elixirでいい感じのCLIをパッと作ってサッと共有(できるようになる。そう、Elixir 1.3ならね)


ElixirでCLIを書く

[翻訳]Elixirでコマンドラインアプリケーションを書くで紹介されている通り、Erlang/Elixirにはescriptという便利なツールがあり、CLIを簡単に作れる。

今回自分でも作ってみたので、いくつか気づいたポイントを紹介。

基礎知識

  • mix.exsで以下のように定義しておく
  def project do
    [
      ...

      escript: [
        main_module: Your.CLI,
        name: "yourcli",
        path: Path.expand(Path.join(["~", ".mix", "escripts", "yourcli"])), # 後述
      ],

      ...
     ]
  end
  • Your.CLI.main/1の中身を実装する。その名の通りCLIのメインエントリポイントで、コマンドライン引数が配列として渡る
    • 最初はIO.puts("Hello world!")とかで
  • mix deps.get
  • mix escript.build
  • (追記)Elixir 1.3以降では:pathは不要。後段の追記参照

バイナリを置く場所

いきなりこの記事のポイントに入る。

mix escript.buildはデフォルト(前節で:pathキーを設定しない状態)だと、カレントディレクトリにyourcliという名前の実行可能バイナリをポンと設置してくれる。従って、

$ mix escript.build
  (snip)
$ chmod +x yourcli
$ ./yourcli
Hello world!

こうなるわけだ。簡単でよろしい。パッと作れそうだ。

さて、近々来る予定のElixir 1.3では~/.mix/escriptsというパスがデフォルトのescriptバイナリ置き場として制定され、新設のmix escript.installコマンドがビルドしたバイナリはここに置かれる。1

つまり今から~/.mix/escriptsPATHを通しておき、バイナリをそこに吐き出すようにしておけばスムースに移行できる。前節の設定はそういう意味。

ちなみにElixir 1.3ではmix escript.install <URL>一発でリモートのプロジェクトをフェッチしてインストールできる。「サッと共有」ができるというのはそういうわけ。2

ただし、escriptはコンパイル済みバイナリによる提供であればErlangさえあれば動くが、mixでインストールするにはElixirが必要になる。というかmixはElixirについてくる。

(追記)というわけで、Elixir 1.3が出てしばらく立ち、もう1.6になった今現在は、:pathは指定せずにおいて、mix escript.install <url>などとするのがよい。まだリリースはしておらず、ローカルディレクトリからインストールしたい場合もmix escript.install~/.mix/escriptsに配置されるし、重複チェックやescript.uninstallなどもある。

また、1.3ローンチ直後はできなかったが、最近は選択肢が増えて、

  • mix escript.install hex <package>
  • mix escript.install github <user>/<repo>

などとできるようになった。今はこちらをinstall instructionとすべき。

OptionParser超便利

冒頭でリンクした記事でも紹介されているOptionParserはElixirのビルトインモジュールで超便利である。

コマンドライン引数は配列としてmain/1に渡ってくるわけだが、OptionParserはその配列をよろしく解釈してくれる。

OptionParser.parse(~W(-v --hoge --foo Foo Bar Baz))
# {[hoge: true, foo: "Foo"], ["Bar", "Baz"], [{"-v", nil}]}

返り値は{opts, args, errors}で、--つきのオプションであれば自動でoptsとして解釈される。true, false以外の文字列や数値の値がなければ真偽値を、あればその値をオプションに紐付ける。

-つきのオプションは短縮形扱いなので、基本形が定義されていないとerrorsに入ってしまうが、その設定もaliasesを指定するだけでいい。

OptionParser.parse(~W(-v --hoge --foo Foo Bar Baz),
  aliases: [v: :verbose])
# {[verbose: true, hoge: true, foo: "Foo"], ["Bar", "Baz"], []}

switchesstrictといったその他のオプションもある。switchesはオプションの取る値のTypeを指定できて、

OptionParser.parse(~W(-v --hoge --foo Foo Bar Baz),
  aliases: [v: :verbose], 
  switches: [foo: :integer])
# {[verbose: true, hoge: true], ["Bar", "Baz"], [{"--foo", "Foo"}]}

このようにvalidateできる。strictは読んで字のごとく定義したモノ以外受け付けなくする。

OptionParser.parse(~W(-v --hoge --foo Foo Bar Baz), 
  aliases: [v: :verbose], 
  strict: [verbose: :boolean])
# {[verbose: true], ["Foo", "Bar", "Baz"], [{"--hoge", nil}, {"--foo", nil}]}

システムコマンドの使用

System.cmd/3を使うと別のコマンドをElixirコードから呼び出して結果を取得できる。世の中にあるElixirモジュールだけではやりづらい処理を補うことができる。

ただし、System.cmd/3は単一のコマンドに引数を渡して実行させることしかできない。例えば以下のようなことをしたい場合に困る。

$ echo "some string" | pbcopy

Macだとよくやるクリップボードコピーだ。他にもjqにパイプしてJSONをPrettyPrintしたいなんてこともあるかもしれない。3

上記Docにも下の方にちょろっと書いてあるが、こういったパイプやリダイレクトを利用したい場合、Erlangの:os.cmd/1が使える。

contents = "some string"
:os.cmd('echo #{contents} | pbcopy')

渡すのはchar listになる点が要注意。Interpolationは使えるので、Elixirコード内で生成された文字列等をクリップボードに渡す処理を書くことができる。Windowsの場合もclipで同じことができる。

コマンドの終了ステータス

何かメッセージを出してコマンドを異常終了させたい場合があると思う。

単にraise(message)してもいいのだが、例外になってしまうので標準出力にスタックトレースが吐き出されてカッコ悪い。
エラーハンドリングした上で終了状態を明示して止めたいのであれば、こう書ける。

IO.puts(:stderr, message)
exit({:shutdown, 1})

Kernel.exit/1docの下の方、"CLI exits"の節に説明が書いてある。1を別のステータスコードにしてもいい。リンクされた全てのOTPプロセスには全てpolitely-shutdownするよう通知される。
ちなみにraise等の正常でないケースでは、OSプロセスとしては全てステータス1で終了するとのこと。

ghpr

他にもある気がするけど、とりあえず思いついたのはここまで。

以上の知見を活かしつつ、ghprというCLIを作ったので使ってみてください。Elixirで書いてるよという主張のためにレポジトリはymtszw/ex_ghprです

内容としてはGitHub Pull Requestをコマンド一発でオープンできるシロモノです。
github/hubもあるのですが、複数アカウントの取り扱いが面倒なのを解消しつつ、自分のチームのワークフローをCLIで補助しています。
オプションでいろいろとできるようになっている+今後も拡張する予定なので、よろしくお願いします。

(追記)筆者は今も変わらず日常的に使ってますが、現状機能に満足して2年位放置していたら、hubのほうがちょこちょこ機能追加してて、普通にhubで十分なケースが増えた気もする。。。


  1. masterなので既にバージョンは繰り上がっているが、こちらのリンク参照 

  2. Elixir 1.3の新機能等は@tuvistavieさんがtokyo.ex #1で発表していたスライドにまとまっています。 

  3. Poison.encode/2prettyオプションがあるけど。