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

# ElixirでCLIを書く

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

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

# 基礎知識

- `mix.exs`で以下のように定義しておく

```ex
  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`という名前の実行可能バイナリをポンと設置してくれる。従って、

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

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

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

[^doc]: masterなので既にバージョンは繰り上がっているが、[こちらのリンク](https://hexdocs.pm/mix/master/Mix.Tasks.Escript.Install.html)参照

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

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

[^elixir1_3]: Elixir 1.3の新機能等は@tuvistavieさんがtokyo.ex #1で発表していた[スライド](http://tuvistavie.com/slides/elixir-1-3/#/)にまとまっています。

ただし、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](http://elixir-lang.org/docs/stable/elixir/OptionParser.html)超便利

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

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

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

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

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

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

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

```ex
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`は読んで字のごとく定義したモノ以外受け付けなくする。

```ex
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`](http://elixir-lang.org/docs/stable/elixir/System.html#cmd/3)を使うと別のコマンドをElixirコードから呼び出して結果を取得できる。世の中にあるElixirモジュールだけではやりづらい処理を補うことができる。

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

```bash
$ echo "some string" | pbcopy
```

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

[^poison]: `Poison.encode/2`の`pretty`オプションがあるけど。

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

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

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

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

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

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

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

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

# [ghpr](https://github.com/ymtszw/ex_ghpr)

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

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

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

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