はじめに
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](rust-lang/cargo https://github.com/rust-lang/cargo) でも使われています。ヒューマンリーダブルなUSAGEを変更するだけでコマンドやオプションを追加できるので今のところ気に入っています。