Clapとは
コマンドラインの引数解析を行うRust用のライブラリです。
位置引数、フラグ、オプション、サブコマンドなどの解析が可能で、それぞれ細かい設定も指定することができる多機能さが特徴です。
- Webサイト(https://clap.rs/)
- Github(https://github.com/kbknapp/clap-rs)
- ドキュメント(https://docs.rs/clap)
この記事について
Clapについてざっと確認したので個人的な備忘的意味合いも含め、導入から簡単な使い方、各機能を解説します。
また、使用している各メソッドについては一覧としてこちらにもまとめていますので参考までに。
確認した環境やバージョンは以下の通りです。
- MacOSX 10.11.6 (El Capitan)
- rust 1.5.1
- clap 2.20.3
Quick start
導入手順から簡単なサンプル実装までを紹介します。
Note: rustの環境(rustcやCargoなど)は整っているものとします。
1. プロジェクトを作成してcargo.tomlを編集
まず任意の場所でcargo new clapex
を実行してプロジェクトの雛形を作成します。
次にClapをCargo経由で取得する為の設定をCargo.toml
に追加します。
[package]
name = "clapex"
version = "0.1.0"
authors = ["myname <myname@mail.com>"]
description = "Clap Example CLI"
[dependencies]
clap = "2.20.3"
descriptionなどは必須ではありませんが、記述しておくと自動的に内容をヘルプに記載してくれるClapのmacroが使えたりします(後述)。
2. main.rsを作ってcrateとuseを追加
プロジェクトトップのsrcディレクトリにmain.rs
ファイルを作成し、Clapを使うためにextern crate
します。
また、この後の実装でApp
、Arg
、SubCommand
を使用するので、簡潔のためにuse
を追記します。
extern crate clap;
use clap::{App, Arg, SubCommand}
3. main関数に簡単なアプリケーションを定義
main関数に簡単なサンプルCLIアプリケーションを定義してみます。
fn main() {
let app = App::new("clapex")
.version("0.1.0") // バージョン情報
.author("myname <myname@mail.com>") // 作者情報
.about("Clap Example CLI") // このアプリについて
.arg(Arg::with_name("pa") // 位置引数を定義
.help("sample positional argument") // ヘルプメッセージ
.required(true) // この引数は必須であることを定義
)
.arg(Arg::with_name("flg") // フラグを定義
.help("sample flag") // ヘルプメッセージ
.short("f") // ショートコマンド
.long("flag") // ロングコマンド
)
.arg(Arg::with_name("opt") // オプションを定義
.help("sample option") // ヘルプメッセージ
.short("o") // ショートコマンド
.long("opt") // ロングコマンド
.takes_value(true) // 値を持つことを定義
)
.subcommand(SubCommand::with_name("sub")// サブコマンドを定義
.about("sample subcommand") // このサブコマンドについて
.arg(Arg::with_name("subflg") // フラグを定義
.help("sample flag by sub") // ヘルプメッセージ
.short("f") // ショートコマンド
.long("flag") // ロングコマンド
)
);
}
App::new
メソッドで"clapex"という名前のAppインスタンスを作成し、メソッドチェーンでヘルプ情報や引数などを定義していきます。
4. 解析部分を実装する
次に解析部分を実装します。サンプルなので入力を標準出力するだけの簡単なものを実装してみます。
fn main() {
let app = App::new("clapex")
...(省略)
// 引数を解析
let matches = app.get_matches();
// paが指定されていれば値を表示
if let Some(o) = matches.value_of("pa") {
println!("Value for pa: {}", o);
}
// optが指定されていれば値を表示
if let Some(o) = matches.value_of("opt") {
println!("Value for opt: {}", o);
}
// flgのON/OFFで表示するメッセージを切り替え
println!("flg is {}", if matches.is_present("flg") {"ON"} else {"OFF"});
// subサブコマンドの解析結果を取得
if let Some(ref matches) = matches.subcommand_matches("sub") {
println!("used sub"); // subが指定されていればメッセージを表示
// subflgのON/OFFで表示するメッセージを切り替え
println!("subflg is {}", if matches.is_present("subflg") {"ON"} else {"OFF"});
}
}
get_macthes
メソッドを呼び出すことで与えられたコマンドラインの解析を行い、正常に解析できればArgMatches
という結果が返ります。コマンドラインの指定にエラーがあった場合(必須引数が指定されていないなど)、エラーメッセージを標準出力に出力し、強制的に終了コード1でメインプロセスを終了します。
引数に指定された内容はArgMatches
が持つメソッドを利用して取得可能です。
value_of
メソッドは引数に渡した引数名に該当する引数に指定された値を取得します。値がない場合はNone
が返ります。
is_present
メソッドは引数に渡した引数名に該当する引数が指定されたかどうかを判定します。bool値が返ります。
subcommand_matches
メソッドは引数に渡したサブコマンド名に該当するサブコマンドの解析結果としてArgMatches
が返ります。
Tips
エラーを検出した場合にプロセスを終了させたくない場合はget_matches_safe
メソッドを使うことで可能です。これはエラー時にはエラーを返すだけでメインプロセスは終了させません。エラー時に別途実行したい処理がある場合などはこちらを使ったほうがいいでしょう。
main.rs
の実装は一旦これで完了です。
5. ビルドして実行してみる
プロジェクトディレクトリ配下でcargo build
を実行します。
無事にビルドが終われば{プロジェクトルート}/target/debug/clapex
という実行ファイルが出力されているはずです。
では、実行してみましょう。
まずはヘルプメッセージを表示させてみます。./clapex -h
を実行します。
$ ./clapex -h
clapex 0.1.0
myname <myname@mail.com>
Clap Example CLI
USAGE:
clapex [FLAGS] [OPTIONS] <pa> [SUBCOMMAND]
FLAGS:
-f, --flag sample flag
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-o, --opt <opt> sample option
ARGS:
<pa> sample positional argument
SUBCOMMANDS:
help Prints this message or the help of the given subcommand(s)
sub sample subcommand
このように、定義した情報が自動的によくあるヘルプ形式で整形されて表示されます。
明示的に定義していないバージョン(-V
/--version
)とヘルプ(-h
/--help
)はClapが自動的に作成します(作成させないようにすることも可能です)。
このバージョンとヘルプの自動生成はサブコマンドにも適用されています。./clapex sub -h
を実行します。
$ ./clapex sub -h
clapex-sub
sample subcommand
USAGE:
clapex sub [FLAGS]
FLAGS:
-h, --help Prints help information
-f, --flag sample flag by sub
-V, --version Prints version information
サブコマンドに限定したヘルプメッセージが出力されます。
次に引数を指定して実行してみます。./clapex pavalue -f -o optvalue sub -f
を実行します。
$ ./clapex pavalue -f -o optvalue sub -f
Value for pa: pavalue
Value for opt: optvalue
flg is on
used sub
subflg is on
解析も正しく行われています。
必須の引数である"pa"を指定しなかったらどうなるでしょうか。./clapex -f
を実行します。
$ ./clapex -f
error: The following required arguments were not provided:
<pa>
USAGE:
clapex <pa> --flag
For more information try --help
エラーになりました。エラーメッセージにはエラーの理由とUSAGE、さらに--help
への導線も表示されています。
このように、Clapを使うと比較的簡単にコマンドラインを定義&解析することができます。
この他にも引数を複数指定可能にしたり、いくつかの引数をグループ可してそのうち1つだけ指定可能にしたり、ヘルプメッセージの出力方法を変更したりと様々な機能がClapには備わっています。
いくつかの機能をAdvancedとして紹介します。
Advanced
基本的な実装方法はQuick startで説明した通りですが、Clapにはまだまだ便利な機能がたくさんあるのでその一部を解説します。
crateマクロでCargo.tomlを参照して自動設定する
アプリ名や作成者、バージョンなどを手動で定義するのではなく、Cargo.toml
を参照して自動的に設定させることができます。
Quick startのコードを修正してみます。
#[macro_use] // macroを使うのでmacro_useを追記
extern crate clap;
...(省略)
fn main() {
let app = App::new(crate_name!()) // Cargo.tomlのnameを参照する
.version(crate_version!()) // Cargo.tomlのversionを参照する
.author(crate_authors!()) // Cargo.tomlのauthorsを参照する
.about(crate_description!()) // Cargo.tomlのdescriptionを参照する
.arg(Arg::with_name("pa")
...(省略)
}
それぞれハードコーディングからmacroを使うように変更したので、Cargo.toml
の内容を参照するようになりました。
試しにCargo.toml
を変更してみます。
[package]
name = "clapex"
version = "1.1.1"
authors = ["myname <myname@mail.com>"]
description = "Clap Example CLI ref toml"
...(省略)
ヘルプメッセージを表示すると、
$ ./crapex -h
clapex 1.1.1
myname <myname@mail.com>
Clap Example CLI ref toml
このように変更が反映されます。
これはapp_from_crate
マクロを使うことでさらに簡略化することが可能です。
main.rs
を次のように修正します。
fn main() {
let app = app_from_crate!()
.arg(Arg::with_name("pa")
...(省略)
}
だいぶ簡略化されましたが、これでも同じ結果を得ることができます。
シンタックスライトにArgを定義する
Quick startではArgを定義する際、以下のように行っていました。
...(省略)
fn main() {
let app = app_from_crate!()
...(省略)
.arg(Arg::with_name("opt")
.help("sample option")
.short("o")
.long("opt")
.takes_value(true)
)
...(省略)
}
少し冗長に感じる方もいるかと思います。
しかしこれはfrom_usage
メソッドを使ってある程度シンタックスライトに定義することが可能です。
from_usage
を使った書き方に変更します。
...(省略)
fn main() {
let app = App::new(crate_name!())
...(省略)
.arg(Arg::from_usage("-o --opt [OPT] 'sample option'"))
...(省略)
}
定義が1行に収まり、すっきりと記述することができました。可読性もこちらの方が良いですね。
from_usage
メソッドはいくつかのシンタックスルールに則って記述することである程度の設定を一度に行うことができます。
ある程度と表現したのは、from_usage
メソッドだけでは定義しきれない設定も多数あるからです。
from_usage
メソッドで定義できない設定はこれに続けてメソッドチェーンで繋げれば問題なく設定可能です。
Tips
Appにも似たようなarg_from_usage
というメソッドがあります。これを使うとAppのインスタンスから. arg_from_usage("...")
と書き始める事ができます。ただし、この方法の場合はシンタックスで定義できるものしか設定することができません。
シンタックスルールは以下のようになります。
[explicit name] [short] [long] [values] [help]
- explicit name: 明示的な名称(省略可能)。省略された場合はlong > short > valuesの順を優先順として、指定された名称を引数の名称とします。
[]
で囲うと任意、<>
で囲うと必須であることを意味します。後ろに...
を付けると複数指定可能であることを意味します。 - short: ショートコマンド。
-
の後に1文字付けることで設定されます。後ろに...
を付けると複数指定可能であることを意味します。設定の最後に,
を付けることもできますが、機能には影響しません。 - long: ロングコマンド。
--
の後にスペースなしの文字列を付けることで設定されます。後ろに...
を付けると複数指定可能であることを意味します。 - values: 引数の値。
[]
または<>
で囲った文字列を1つ以上記述することで設定されます。[]
で囲うと任意、<>
で囲うと必須であることを意味します。後ろに...
を付けると複数指定可能であることを意味します。1つの引数に複数の値を持たせることもできます。 - help: ヘルプメッセージ。
''
で囲った文字列を記述することで設定されます。
Quick startの定義を全てfrom_usage
メソッドを使うと以下のようになります。
...(省略)
fn main() {
let app = App::new(crate_name!())
.arg(Arg::from_usage("<pa> 'sample positional argument'"))
.arg(Arg::from_usage("[flg] -f --flag 'sample flag'"))
.arg(Arg::from_usage("-o --opt [OPT] 'sample option'"))
.subcommand(SubCommand::with_name("sub")
.about("sample subcommand")
.arg(Arg::from_usage("[subflg] -f --flag 'sample flag by sub'"))
);
...(省略)
}
かなりすっきり設定できますね。
複数の引数をグループ化する
Clapには複数の引数をグループ化する機能があります。引数をグループ化することにより、グループのうちどれか1つしか指定できないようになります(グループから複数指定させることも可能です)。
引数をグループ化するにはArgGroup
を使います。
main.rs
に幾つか引数を追加してそれらをグループ化してみます。
#[macro_use]
extern crate clap;
use clap::{App, Arg, SubCommand, ArgGroup}; // ArgGroupを追加
fn main() {
let app = App::new(crate_name!())
...(省略)
// 定義部分
// levelグループに所属させる引数を定義
.args_from_usage("--verb 'verbose mode: level group'
--debug 'debug mode: level group'
--info 'info mode: level group'")
// ArgGroupを定義
.group(ArgGroup::with_name("level") // グループ名
.args(&["verb", "debug", "info"]) // グループに所属させる引数を設定
)
...(省略)
// 解析部分
// levelグループのいずれかが指定されていれば内容を表示
if matches.is_present("level") {
let (verb, debug, _) = (matches.is_present("verb"),
matches.is_present("debug"),
matches.is_present("info"));
println!("level is {}", if verb {"verb"} else if debug {"debug"} else {"info"});
}
}
ArgGroup::with_name
メソッドで"level"という名前のグループを作成し、args
メソッドで所属させる引数そ設定しています。それをgroup
メソッドでApp
に設定します。
解析部分ではグループのいずれかが指定されたことをis_present
メソッドで判定しています。
Tips
args_from_usage
メソッドは複数の引数の定義を1つ1行としてシンタックスライトに一度に定義することができます。シンタックスルールはfrom_usage
メソッドと同じです。ただし、この方法の場合はシンタックスで定義できるものしか設定することができません。
それでは、これを実行してみます。./clapex pavalue --verb
を実行します。
$ ./clapex pavalue --verb
Value for pa: pavalue
flg is OFF
level is verb
期待通り"level is verb"が出力されました。
グループ化しているので同時にverbとinfoは指定できないはずです。./clapex pavalue --verb --info
を実行してみましょう。
$ ./clapex pavalue --verb --info
error: The argument '--verb' cannot be used with one or more of the other specified arguments
USAGE:
clapex <pa> <--verb|--debug|--info>
For more information try --help
こちらも期待通りエラーになりました。
ArgGroup
はこの他にもグループ単位で必須にしたり、他の引数が指定されいる場合に限り指定可能にするなど様々な設定が可能です。
ヘルプに表示される引数の順序を定義順にする
ここで一度ヘルプを表示してみます。
$ ./clapex -h
clapex 1.1.1
myname <myname@mail.com>
Clap Example CLI ref toml
USAGE:
clapex [FLAGS] [OPTIONS] <pa> [SUBCOMMAND]
FLAGS:
--debug debug mode: level group
-f, --flag sample flag
-h, --help Prints help information
--info info mode: level group
-V, --version Prints version information
--verb verbose mode: level group
OPTIONS:
-o, --opt <OPT> sample option
ARGS:
<pa> sample positional argument
SUBCOMMANDS:
sub sample subcommand
help Prints this message or the help of the given subcommand(s)
先程定義したlevelグループがバラバラで見づらくなってしまっています。これはClapではデフォルトで引数の並び順はアルファベットの降順になるためです。
では次はこの順序を変更してみます。引数ひとつひとつに順序を付けるArg::display_order
というメソッドもありますが、それぞれの引数に設定しないといけないのと運用にも若干手間がかかるので、ここではもっとシンプルに、定義順で表示するAppSettings::DeriveDisplayOrder
という設定を有効にします。
main.rs
を以下の様に変更します。
#[macro_use]
extern crate clap;
use clap::{App, AppSettings, Arg, ArgGroup, SubCommand}; // AppSettingsを追加
fn main() {
let app = app_from_crate!()
.setting(AppSettings::DeriveDisplayOrder) // この1行を追加
...(省略)
}
これだけです。
setting
メソッドはAppSettings
の設定を有効にするメソッドで、AppSettings::DeriveDisplayOrder
はヘルプに表示する引数の順序を定義順にする設定です。
では、再度ヘルプを表示してみます。
$ ./clapex -h
clapex 1.1.1
myname <myname@mail.com>
Clap Example CLI ref toml
USAGE:
clapex [FLAGS] [OPTIONS] <pa> [SUBCOMMAND]
FLAGS:
-f, --flag sample flag
--verb verbose mode: level group
--debug debug mode: level group
--info info mode: level group
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-o, --opt <OPT> sample option
ARGS:
<pa> sample positional argument
SUBCOMMANDS:
sub sample subcommand
help Prints this message or the help of the given subcommand(s)
無事に順序が定義順に表示されるようになりました。
指定可能な値を限定する & デフォルト値を設定する
Clapでは指定可能な値をあらかじめ設定しておくことができます。また、引数が指定されなかった場合に取るデフォルト値を設定することもできます。
main.rs
を以下のように追記します。
...(省略)
fn main() {
let app = app_from_crate!()
...(省略)
// 以下の定義を追加
.arg(Arg::from_usage("-m --mode [MODE] 'sample mode'")
.possible_values(&["fast", "normal", "slow"]) // 指定可能な値を設定
.default_value("normal") // デフォルト値を設定
)
...(省略)
// 以下の解析部分を追加
// modeが指定されていれば値を表示
if let Some(o) = matches.value_of("mode") {
println!("Value for mode: {}", o);
}
...(省略)
}
新たにmodeという引数を追加し、possible_values
メソッドで指定可能な値を列挙しています。また、default_value
メソッドで引数が指定されなかった場合のデフォルト値を設定しています。
ヘルプを確認してみます。
$ ./clapex -h
...(省略)
OPTIONS:
-o, --opt <OPT> sample option
-m, --mode <MODE> sample mode [default: normal] [values: fast, normal, slow]
...(省略)
modeが追加され、デフォルト値と指定可能値が明示されています。
では、実際にコマンドライン実行してみます。
$ ./clapex pa -m fast
Value for pa: pa
flg is OFF
Value for mode: fast
指定可能値なので期待通りに処理が正常終了しています。
modeを指定しない場合はどうでしょうか。
$ ./clapex pa
Value for pa: pa
flg is OFF
Value for mode: normal
こちらも期待通りにデフォルト値が設定されています。
最後に指定可能値以外の値を指定してみましょう。
$ ./clapex pa -m veryfast
error: 'veryfast' isn't a valid value for '--mode <MODE>'
[values: fast, normal, slow]
USAGE:
clapex <pa> --mode <MODE>
For more information try --help
エラーになりました。
エラーメッセージからも"veryfast"は無効な値で、"fast"/"normal"/"slow"が指定可能であることが分かります。
カスタムバリデーションを実装する
Clapは引数のバリデーションに独自のバリデータを設定することができます。
main.rs
を以下のように変更します。
...(省略)
// 値に'@'が含まれてないことをチェックするカスタムバリデータ
fn not_at(v: String) -> Result<(), String> {
if v.contains("@") {
return Err(String::from("The value can not contain the '@' character"));
}
Ok(())
}
fn main() {
let app = app_from_crate!()
...(省略)
.arg(Arg::from_usage("<pa> 'sample positional argument [value: without '@']'") // '@'を含んではいけないことを明記
.validator(not_at) // '@'をチェックするカスタムバリデータを設定
)
...(省略)
}
validator
メソッドを使うことで引数のバリデーションに独自のバリデータを設定できます。引数に渡すのは特定のシグネチャFn(String) -> Result<(), String>
を満たすstaticな関数です。
ここではサンプルとして'@'文字がふくまれていないことをチェックするバリデータを実装しています。エラー時の文言はそのまま出力されるので分かりやすい文言を設定した方がいいでしょう。
また、該当の引数のヘルプメッセージにもバリデーションのことを明記しておく方が親切ですので、ここでもその旨を追記しています。
では実際にバリデーションエラーにしてみます。
$ ./clapex pa@
error: Invalid value for '<pa>': The value can not contain the '@' character
カスタムバリデーションが動作してエラーになりました。
メッセージ前半部分(":"より前)はClapが自動的に出力する文言で、どの引数でエラーになったのかが出力されます。メッセージ後半部分(":"より後)がカスタムバリデータで設定したエラーメッセージになります。
シェルの補完ファイルを生成する
Clapはシェルの補完ファイル(bash-completionなど)を生成する機能も持っています。
この機能はAppインスタンスが持つgen_completions_to
メソッドを使って生成することができますが、これまでの実行ファイルとは別に動作する方が良いでしょう。その手順はClapのドキュメントに記載されているので、それに従ってやってみます。
まずはAppの定義部分を別モジュールに切り分けます。src/cli.rs
ファイルを作成し、そこにAppの定義を移します。
use clap::{App, AppSettings, Arg, ArgGroup, SubCommand};
pub fn build_cli() -> App<'static, 'static> {
app_from_crate!()
.setting(AppSettings::DeriveDisplayOrder)
.arg(Arg::from_usage("<pa> 'sample positional argument [value: without '@']'")
.validator(not_at)
)
.arg(Arg::from_usage("[flg] -f --flag 'sample flag'"))
.arg(Arg::from_usage("-o --opt [OPT] 'sample option'"))
.arg(Arg::from_usage("-m --mode [MODE] 'sample mode'")
.possible_values(&["fast", "normal", "slow"])
.default_value("normal")
)
.args_from_usage("--verb 'verbose mode: level group'
--debug 'debug mode: level group'
--info 'info mode: level group'")
.group(ArgGroup::with_name("level")
.args(&["verb", "debug", "info"])
)
.subcommand(SubCommand::with_name("sub")
.about("sample subcommand")
.arg(Arg::from_usage("[subflg] -f --flag 'sample flag by sub'"))
)
}
fn not_at(v: String) -> Result<(), String> {
if v.contains("@") {
return Err(String::from("The value can not contain the '@' character"));
}
Ok(())
}
今までmain.rs
のmain関数内で行っていたAppの定義をbuild_cli
というパブリックな関数に移しました。定義内容は特に変えていません。
これによってmain.rs
のApp定義部分も以下のように変更できます。
#[macro_use]
extern crate clap;
// clap::Appなどはここでは使わなくなったのuseは削除
mod cli; //代わりにcliモジュールをインポート
fn main() {
// Appを定義する関数を呼び出し、解析する
let matches = cli::build_cli().get_matches();
...(省略)
}
App定義部分は別関数からの呼び出しにリファクタリングします。
次に、補完ファイル作成用にbuild.rs
をプロジェクトトップに作成し・実装します。
#[macro_use]
extern crate clap;
use clap::{Shell};
include!("src/cli.rs");
fn main() {
let mut app = build_cli();
app.gen_completions("clapex", Shell::Bash, "./");
}
cli::build_cli
を呼び出してApp定義を取得した後、gen_completions
メソッドで補完ファイルを生成します。gen_completions
メソッドの第1引数にはCLIアプリのバイナリ名、第2引数には対象のシェル、第3引数には補完ファイルを書き出すディレクトリを指定します。
対象にできるシェルは現状以下の4つです。
- Bash
- Fish
- Zsh
- PowerShell
ここではBashを生成していますが、自身の環境に合わせて変えて下さい。
最後にビルド時に補完ファイルを生成させるようにCargo.toml
を修正します。
[package]
...(省略)
build = "build.rs"
[build-dependencies]
clap = "2.20.3"
packageにbuild = "build.rs"
を指定してビルド時にbuild.rs
を実行するように設定します。また、build.rs
でもclapを使うのでbuild-dependenciesを追加してClapを解決できるようにします。
これで準備が整いました。cargo build
するとプロジェクトトップにclapex.bash-completion
ファイル(選択したシェルによって変わるのでBash以外を指定した場合は読み替えて下さい)が生成されます。
あとは自身の環境のcompletionにこのファイルを適用すればコマンド補完されるようになります。
まとめ
簡単な導入からいくつかの機能を紹介しましたが、Clapにはここでは紹介しきれないほどまだまだ多くの設定があり、それらを駆使すればより柔軟なコマンドラインアプリケーションを作成することができます。
各種設定の詳細については本家ドキュメントを参照して頂くのが一番良いと思いますが、こちらにも簡単にではありますがほぼ全ての設定についてまとめてありますので参考にしてもらえればと思います。