docoptでドキュメントとともにコマンドラインツールを作る

# はじめに

Rustでコマンドライン引数を受け取るのにrust-langは [getopts](https://github.com/rust-lang/getopts) というcrateを提供しています。getoptsはオプションとフラグを定義して、与えられたコマンドライン引数がマッチするかどうかで処理をするというシンプルなものです。

```rust
fn main() {
    let args: Vec<String> = env::args().collect();
    let program = args[0].clone();

    let mut opts = Options::new();
    opts.optopt("o", "", "set output file name", "NAME");
    opts.optflag("h", "help", "print this help menu");
    let matches = match opts.parse(&args[1..]) {
        Ok(m) => { m }
        Err(f) => { panic!(f.to_string()) }
    };
    if matches.opt_present("h") {
        print_usage(&program, opts);
        return;
    }
    ...
}
```

getoptsはシンプルですがサブコマンドを実装したり少し複雑なことをしようとしたら大変でした。issueを見ていたら同じようにサブコマンドの実装で困っている人がいて、docoptを使えと言われていたので、docoptについて調べてみました。

# docoptでコマンドで定義する

[docopt](http://docopt.org/) はコマンドラインツールのインタフェースを記述するための言語です。docoptでコマンドを定義することで自動でパーサを生成します。
今回はdocoptのRustの実装である [docopt.rs](https://github.com/docopt/docopt.rs) を使ってコマンドを書いてみたので、docoptの使い方を紹介します。

## 作成したコマンドの仕様

[cref](https://github.com/rejasupotaro/cref) という英語ネイティブではない人のためのコミットメッセージを検索するツールを作りました。初めてRustで書いたツールなのでコードはあまり綺麗でないかもしれません。[GitHubコミットメッセージの英語の書き方の文例が検索できるサービス作った](http://d.hatena.ne.jp/minamijoyo/20150220/p1) に触発されて作りました。

### コミットログの検索

`$ cref` で検索画面が起動して、インタラクティブにコミットログの絞り込みを行うことができます。

![](https://raw.githubusercontent.com/rejasupotaro/cref/master/images/search.gif)

### コミットログのインポート

`$ cref import` に続けて `rails/rails` のようにリポジトリを指定してコミットログをローカルに保存します。リポジトリは複数指定することもできます。

### インポートされているリポジトリのリスト

`$ cref list` で今インポートされているリポジトリ一覧を出力します。

### インポートされているリポジトリの更新

`$ cref update` でリポジトリの最新のコミットを取得します。`cref update` に続けて1つ以上のリポジトリを指定することもできます。指定しなければ全リポジトリを更新します。

### インポートされているリポジトリの削除

`$ cref delete` に続けて `rails/rails` のようなリポジトリ名を入れるとリポジトリに紐づくコミットログをローカルから削除します。

## docoptで記述してみる

これらの仕様をdocoptで記述すると以下のようになります。

```rust
Usage:
  cref
  cref import <repo>...
  cref list
  cref update [<repo>...]
  cref delete <repo>
  cref (--help | --version)

Options:
  -h, --help     Show this screen
  -v, --version  Show version
```

文法を見ていきます。

# docoptの文法

### `<argument>`

引数の順番を持った引数の定義に使います。たとえば以下のように使うことができます。

``` 
Usage: my_program <host> <port>
```

また `...` を付けることで可変長であることを定義できます。

```
cref import <repo>...
```

### `[optional elements]`

省略可能な要素を定義します。`$ cref update` のリポジトリは省略可能なので、そこで使っています。

```
cref update [<repo>...]
```

### `(required elements)`

`[]` で囲まれていない要素はデフォルトで省略不可能ですが `()` を使うことで省略不可能であることを明示することができます。

```
Usage: my_program (--either-this <and-that> | <or-this>)
```

このコマンドは `--either-this` の後に `<and-that>` か `<or-this>` が必要であることを表しています。

```
Usage: my_program [(<one-argument> <another-argument>)]
```

このコマンドは引数が0個か、2個かのいずれかであることを表しています。

### `-o` `--option`

ショートオプションはスタックすることができます。つまり `-abc` は `-a -b -c` と等しく解釈されます。`-f FILE` あるいは `-fFILE` で引数を指定することができます。
ロングオプションはその後ろに ` ` か `=` で引数を持つことができます。つまり `--input=ARG` は `--input ARG` と等しく解釈されます。

### `command`

`<arguments>` か `--option` に属さない要素はサブコマンドとして解釈されます。

# docopt.rsの使い方

文法がおおまかに分かったところで、実際にRustで使ってみます。

```toml
[dependencies]
docopt = "*"
```

```rust
static USAGE: &'static str = "
Usage:
  cref
  cref import <import-repo>...
  cref list
  cref update [<update-repo>...]
  cref delete <delete-repo>
  cref (--help | --version)

Options:
  -h, --help     Show this screen
  -v, --version  Show version
";

#[derive(RustcDecodable, Debug)]
pub struct Args {
    cmd_import: bool,
    arg_import_repo: Vec<String>,
    cmd_list: bool,
    cmd_update: bool,
    arg_update_repo: Vec<String>,
    cmd_delete: bool,
    arg_delete_repo: String,
    flag_help: bool,
    flag_version: bool
}

fn main() {
    let args: Args = Docopt::new(USAGE)
            .and_then(|d| d.decode())
            .unwrap_or_else(|e| e.exit());
}
```

このようにstrでUSAGEとそれに対応するArgsを定義して、Docoptのコンストラクタに渡します。macroが提供されていて、structの定義を省略することもできます。

```toml
[dependencies]
docopt = "*"
docopt_macros = "*"
```

```rust
docopt!(Args derive Debug, "
Usage:
  cref
  cref import <import-repo>...
  cref list
  cref update [<update-repo>...]
  cref delete <delete-repo>
  cref (--help | --version)

Options:
  -h, --help     Show this screen
  -v, --version  Show version
");

fn main() {
    let args: Args = Args::docopt()
            .decode()
            .unwrap_or_else(|e| e.exit());
}
```

Argsを出力するとこのようになっています。

```rust
println!("{:?}", args);
// => Args { cmd_list: false, cmd_update: false, arg_import_repo: ["square/okio"], arg_delete_repo: "", cmd_delete: false, flag_version: false, arg_update_repo: [], cmd_import: true, flag_help: false }
```

`cmd_*` にboolが入って `arg_*` に `String` あるいは `Vec<String>` が入るので、それで処理を分岐したりします。ヘルプを出力すると以下のようになります。

```
$ cref --help
Usage:
  cref
  cref import <import-repo>...
  cref list
  cref update [<update-repo>...]
  cref delete <delete-repo>
  cref (--help | --version)

Options:
  -h, --help     Show this screen
  -v, --version  Show version
```

# まとめ

コマンドラインツールのインタフェース記述言語の [docopt](http://docopt.org/) とその実装の [docopt.rs](https://github.com/docopt/docopt.rs) を紹介しました。
docoptはRustのパッケージマネージャの [cargo](rust-lang/cargo https://github.com/rust-lang/cargo) でも使われています。ヒューマンリーダブルなUSAGEを変更するだけでコマンドやオプションを追加できるので今のところ気に入っています。