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

  • 37
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

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

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 はコマンドラインツールのインタフェースを記述するための言語です。docoptでコマンドを定義することで自動でパーサを生成します。
今回はdocoptのRustの実装である docopt.rs を使ってコマンドを書いてみたので、docoptの使い方を紹介します。

作成したコマンドの仕様

cref という英語ネイティブではない人のためのコミットメッセージを検索するツールを作りました。初めてRustで書いたツールなのでコードはあまり綺麗でないかもしれません。GitHubコミットメッセージの英語の書き方の文例が検索できるサービス作った に触発されて作りました。

コミットログの検索

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

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

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

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

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

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

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

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

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

docoptで記述してみる

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

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で使ってみます。

[dependencies]
docopt = "*"
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の定義を省略することもできます。

[dependencies]
docopt = "*"
docopt_macros = "*"
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を出力するとこのようになっています。

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 とその実装の docopt.rs を紹介しました。
docoptはRustのパッケージマネージャの cargo でも使われています。ヒューマンリーダブルなUSAGEを変更するだけでコマンドやオプションを追加できるので今のところ気に入っています。