はじめに
clapとはRustのCLI引数をパースするためのクレートです。Pythonで言うところのargparse、Goで言うところのcobra/spf13のような立ち位置のクレートで、CLIツールの実装をする場合お世話になることが多いでしょう。
docs.rs
自分が実装時にclapでこれはどういうやり方をするのか、などを調べても日本語では網羅的な情報があまり出てきませんでした。自分のためのまとめを書いていたらそれなりの文量になったので公開することにしました。
実際の実装は以下のリポジトリにあります。
github.com
インストール
cargo を使っているという前提で、Cargo.tomlに以下の行を足します。
[dependencies]
clap = { version = "4.0.11", features = ["derive"] }
使い方
基本形は以下の通りです。ここでは基本的にderive API*1を使ったやり方のみを紹介し、ビルダーパターンの方法は紹介しません。
use clap::Parser;
#[derive(Debug, Parse)]
struct Args {
#[arg(help = "位置引数の説明")]
pos_arg: String,
#[arg(short, long, help = "オプション引数の説明")]
opt_arg: String,
}
fn main() {
let args = Args::parse();
println!("opt_arg: {}", args.pos_arg);
println!("opt_arg: {}", args.opt_arg);
}
structの各フィールドに書いたマクロでその引数の挙動を調整します。 arg
の引数は key = value
の形で渡すのですが、この key
にはclapの Arg
strutに実装されているものを指定できるので、詳しくは以下を見ると良いでしょう。
docs.rs
位置引数
arg
マクロを使わなくとも、何も指定しない場合はデフォルトで位置引数になります。とはいえ、最低限ヘルプメッセージを実装しておくのが親切なので以下の様にします。
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[arg(help = "位置引数の説明")]
pos_arg: String,
}
この場合以下の様なヘルプメッセージが出力されます。
❯ cargo run -q -- --help
Usage: positional-arg <POS_ARG>
Arguments:
<POS_ARG> 位置引数の説明
Options:
-h, --help Print help information
<POS_ARG>
という表記が気に入らない場合、以下の様に任意の名前を指定できます。
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[arg(value_name = "FILE", help = "位置引数の説明")]
pos_arg: String,
}
❯ cargo run -q -- --help
Usage: positional-arg <FILE>
Arguments:
<FILE> 位置引数の説明
Options:
-h, --help Print help information
エスケープされた位置引数
例えばcargoでは cargo run
するとき、runした先に対して引数を渡せるように --
で区切って指定することができます。clapを使えば、これと同じ機能を自分のCLIでも実装できます。
使われ方は色々考えられますが、例えば cargo run
のように他のコマンドのラッパーを書いた時に指定された引数をそのままラップしているコマンドに渡す、などを実装できます。
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[arg(value_name = "FILE", help = "位置引数の説明")]
pos_arg: String,
#[arg(last = true, help = "ここはパースされない")]
last_args: Vec<String>,
}
Usage: positional-arg <FILE> [-- <LAST_ARGS>...]
Arguments:
<FILE> 位置引数の説明
[LAST_ARGS]... ここはパースされない
Options:
-h, --help Print help information
last = true
を設定すると、 --
以降の引数のみを消費するため、 --
を指定しない場合に該当する位置引数が無ければエラーになります。単に Vec<T>
を指定した普通の位置引数は --
があってもなくても引数を消費します。
positional-arg FILE --hoge --fuga
positional-arg FILE Hoge Fuga
positional-arg FILE -- Hoge Fuga
positional-arg FILE -- --hoge --fuga
positional-arg FILE Hoge Fuga
positional-arg FILE -- Hoge Fuga
positional-arg FILE -- --hoge --fuga
引数を取るオプション
例えばファイルパスやユーザ名のような文字列、リトライ回数のような整数などを取るオプションを作る場合は以下の様にします。
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[arg(short = 'n', long = "name", help = "your name")]
name: String,
#[arg(short, long, help = "a 32bit integer")]
count: i32,
}
これらのパラメータが必須かどうか、複数取れるかどうかなどはフィールドの型で決まります。↑の例ではいずれも必須かつそれぞれ一つしか取れません。以下のリンクに対応表があります。
https://docs.rs/clap/latest/clap/_derive/index.html#arg-types
よくあるオプショナルな引数を作るには Option<T>
、複数の値を取れるものは Vec<T>
を使えば良いです。
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[arg(short, long, help = "optional value")]
opt: Option<String>,
#[arg(short, long, help = "multiple inputs")]
inputs: Vec<String>,
}
複数取る場合、これは必須では無くなる(0以上の要素が許容される)ので注意が必要です。必須にしたい場合は明示的に required = true
が必要になります。
引数を取らないオプション
例えば verbose
フラグのような、ある機能の有効無効を決定するオプションを実装する場合に使います。単に型を bool
にすればフラグになります。
#[derive(Debug, Parser)]
struct Args {
#[arg(short, long)]
verbose: bool,
}
bool
なオプションはデフォルトで required = false
になり、デフォルト値が false
になります。
デフォルトを true
、指定したら false
になるフラグを作りたい場合は明示的に action
を指定する必要があります。
#[derive(Debug, Parser)]
struct Args {
#[arg(short, long, action = clap::ArgAction::SetFalse)]
non_verbose: bool,
}
env
featureを有効にすると使えるようになります。
https://docs.rs/clap/latest/clap/builder/struct.Arg.html#method.env
特にコンテナなどでコマンドラインフラグではなく環境変数で挙動を変えたいケースは多々あります。引数に対して環境変数を紐付け、自動で取得させることが出来ます。
#[derive(Debug, Parser)]
struct Args {
#[arg(short, long, env, help = "Value from env")]
from_env: String,
}
デフォルトではフィールドのアッパースネークケースがそのまま環境変数として使われますが、自分で指定することもでき、その場合は env = "ENV_VAR_NAME"
とします。
ヘルプメッセージにどういう環境変数が使えるかが表示されるのが結構良いですね。
❯ cargo run -q -- --help
Usage: env-var --from-env <FROM_ENV>
Options:
-f, --from-env <FROM_ENV> Value from env [env: FROM_ENV=]
-h, --help Print help information
値を入れるとhelpメッセージにもそれが表示されます。
❯ FROM_ENV=value cargo run -q -- --help
Usage: env-var --from-env <FROM_ENV>
Options:
-f, --from-env <FROM_ENV> Value from env [env: FROM_ENV=value]
-h, --help Print help information
ただしなんらかのクレデンシャルなどを環境変数経由で渡したいときに、そのまま出てくると困るケースがあります。こういう場合は以下の様に値が隠れるようにオプションを指定すると良いでしょう。
#[derive(Debug, Parser)]
struct Args {
#[arg(short, long, env, hide_env_values = true, help = "Any credential")]
credential: String,
}
❯ FROM_ENV=value CREDENTIAL=password cargo run -q -- --help
Usage: env-var --from-env <FROM_ENV> --credential <CREDENTIAL>
Options:
-f, --from-env <FROM_ENV> Value from env [env: FROM_ENV=value]
-c, --credential <CREDENTIAL> Any credential [env: CREDENTIAL]
-h, --help Print help information
そもそも環境変数で指定できることを隠したいケース(そういうケースあるの……?)では hide_env = true
とすれば [env: XXX]
のような表示も消すことができます。
一つのフラグで複数の値を取る
どういう事かというと、カンマ区切りの値を受け取ってそれをカンマでsplitして Vec<T>
として扱います。
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[arg(
short,
long,
env,
value_delimiter = ',',
help = "comma separated values are allowed"
)]
multi: Vec<String>,
}
fn main() {
let args = Args::parse();
println!("{:?}", args.multi);
}
❯ cargo run -q -- --multi hoge,fuga
["hoge", "fuga"]
MULTI=hoge,fuga
のように環境変数経由でも Vec<T>
にできます。
ただしこれはナイーブに ,
でsplitしているだけなので、クォートなどを解釈してくれるわけではありません。例えば hoge, fuga
と piyo
を渡したくなって以下の様にしても期待する動作にはなりません。
❯ cargo run -q -- --multi '"hoge, fuga",piyo'
["\"hoge", " fuga\"", "piyo"]
一応自前で引数のパース処理を書くことが出来るので、TypedValueParserを実装することでこうしたケースをカバーすることも可能なはずです(未検証)。
https://docs.rs/clap/latest/clap/builder/struct.Arg.html#method.value_parser
https://docs.rs/clap/latest/clap/builder/trait.TypedValueParser.html
排他オプション
オプションが排他であるとは、option Aが指定されたとき、別のoption Bは指定できなくなる(指定するとエラー)というような関係を指します。これは例えばオプション引数で入力を受け付ける --input INPUT
と、指定されると標準入力から入力を受け付ける --input-from-stdin
フラグのような同じリソースを指定する2つの異なるオプションや、同時に指定すると矛盾が発生するオプションなどに適用されます。
clapでは2つの方法を使ってこれを実装できます。
- 各argに対して
confricts_with
や confricts_with_all
、 exclusive
を設定して他のコマンドとの排他性を宣言する
ArgGroup
を設定してそのうちの一つのみを許可するようにする
前者のやり方はどちらのオプションにこれを設定するかなどが問題になることから、基本的には後者でこれを行うこととします。 ArgGroup
のようなtraitはないためやり方がよくわかっていませんでしたが、tutorialに書かれていました。
docs.rs
use clap::{ArgGroup, Parser};
#[derive(Parser)]
#[command(group(ArgGroup::new("how_to_input").required(true).args(["input", "input_from_stdin"])))]
struct Args {
#[arg(long, help = "input from arg")]
input: Option<String>,
#[arg(long, help = "input from stdin")]
input_from_stdin: bool,
}
fn main() {
let args = Args::parse();
println!(
"input: {}, input_from_stdin: {}",
args.input.unwrap_or("not specified".to_string()),
args.input_from_stdin
);
}
❯ cargo run -q -- --help
Usage: exclusive-option <--input <INPUT>|--input-from-stdin>
Options:
--input <INPUT> input from arg
--input-from-stdin input from stdin
-h, --help Print help information
❯ cargo run -q -- --input-from-stdin
input: not specified, input_from_stdin: true
❯ cargo run -q -- --input hoge
input: hoge, input_from_stdin: false
❯ cargo run -q -- --input hoge --input-from-stdin
error: The argument '--input <INPUT>' cannot be used with '--input-from-stdin'
Usage: exclusive-option <--input <INPUT>|--input-from-stdin>
For more information try '--help'
ここで勘違いしてはいけないのは、ArgGroupへ設定した required
と各 arg の required
は両立してしまうということです。 bool
なフラグはデフォルトで false
が指定されるため気付きづらいのですが、それ以外の型でデフォルト値なしもしくは Option
なしでargを設定すると、それは rquired = true
になってしまいます。今回の例では --input
を Option<String>
ではなく String
にすると、 —-input-from-stdin
のみを指定したときに error: The following required argument was not provided: input
というエラーを見ることになるでしょう。
もう一つ気をつけなくてはいけないのは、デフォルトでは各 arg
がそれぞれ ArgGroup
を作ってしまう点です。通常同じstruct内では同じ名前のfieldを宣言できないのでこれらが重複することはありませんが、自分で ArgGroup
を作ると同じ名前で作ることができてしまいます。これはエラーになるので、異なる名前になるようにしなければいけません。
候補から選択するオプション
予め取り得る値がいくつか決まっており、そのうち一つまたは一つ以上を選択して指定するオプション。Pythonのargparseだと choices
とかで指定できたものを指します。
よくある例としてはログレベルです。いくつか定義されたレベルがあり、そのうち一つを選びます。以下はデフォルト値を設定していますが、 default_value_t
を消せば指定が必須のパラメータになります。もちろん Option<LogLevel>
にすればオプショナルにもできます(が、ログレベルではデフォルト値の方が普通の挙動でしょう)。
use clap::{Parser, ValueEnum};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug, Parser)]
struct Args {
#[arg(long, value_enum, default_value_t = LogLevel::Info, help = "Log level")]
log_level: LogLevel,
}
#[derive(Debug, Clone, ValueEnum)]
enum LogLevel {
Debug,
Info,
Warn,
Error,
Critical,
}
impl Display for LogLevel {
fn fmt(&self, f: &mut Formatter) -> Result {
let raw = format!("{:?}", self);
write!(f, "{}", raw.to_uppercase())
}
}
fn main() {
let args = Args::parse();
println!("log_level: {}", args.log_level);
}
N個の選択肢からM個を選ぶ場合は以下の様に Vec<T>
を使えば良いです。
use clap::{Parser, ValueEnum};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug, Parser)]
struct Args {
#[arg(long, value_enum, value_delimiter = ',', help = "Fruits")]
fruits: Vec<Fruit>,
}
#[derive(Debug, Clone, ValueEnum)]
enum Fruit {
Apple,
Orange,
Banana,
}
impl Display for Fruit {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{:?}", self)
}
}
fn main() {
let args = Args::parse();
println!("fruits: {:?}", args.fruits);
}
ヘルプメッセージにはちゃんとデフォルト値や取り得る値が表示されます。
❯ cargo run -q -- --help
Usage: choices [OPTIONS]
Options:
--fruits <FRUITS> Fruits [possible values: apple, orange, banana]
-h, --help Print help information
サブコマンド
CLIツールを作る場合には基本的にサブコマンドも作ることが多いでしょう。clapではサブコマンドもderiveで実装出来ます。基本的に以下の様に実装するようです。
- ルートコマンドの引数のパーサーを作る(
#[derive(Parser)]
)
- サブコマンドのenumを作る(
#[derive(Subcommand)]
)
- ルートコマンドに2のenumを埋め込む(
#[command(subcommand)]
)
実際のサブコマンドの実行は、2で作ったenumでmatchさせることで処理を分岐するようです。
use clap::{Args, Parser, Subcommand};
#[derive(Debug, Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
#[command(about = "help for hoge")]
Hoge {
#[arg(short, long)]
opt: String,
},
#[command(about = "help for fuga")]
Fuga(FugaArgs),
}
#[derive(Debug, Args)]
struct FugaArgs {
#[arg(short, long)]
opt: String,
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Hoge { opt } => {
println!("hoge {}", opt);
}
Commands::Fuga(fuga) => {
println!("fuga {}", fuga.opt);
}
}
}
❯ cargo run -q -- --help
Usage: subcommand <COMMAND>
Commands:
hoge help for hoge
fuga help for fuga
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help information
❯ cargo run -q -- hoge --opt 1
hoge 1
グローバルなオプション
verbose
フラグだったりログレベルのようなオプションはどのコマンドにも付けたいケースがあります。全てのサブコマンドにこれを毎回実装するのは手間ですが、ルートコマンドの直後でしか verbose
を許容できないのも使い勝手が悪く、全コマンドに --verbose
を付けられるようにしたいです。
そこで global
というフラグを使うと、ルートコマンドのオプションをそのサブコマンドでも使えるようになります。
use clap::{Args, Parser, Subcommand};
#[derive(Debug, Parser)]
struct Cli {
#[arg(short, long, global = true, help = "Global flag")]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
#[command(about = "help for hoge")]
Hoge {
#[arg(short, long)]
opt: String,
},
#[command(about = "help for fuga")]
Fuga(FugaArgs),
}
#[derive(Debug, Args)]
struct FugaArgs {
#[arg(short, long)]
opt: String,
#[arg(short, long, global = true, help = "global option for fuga")]
global: bool,
#[command(subcommand)]
command: Option<SubCommands>,
}
#[derive(Debug, Subcommand)]
enum SubCommands {
#[command(about = "help for nested")]
Nested {
#[arg(short, long, help="opt for nested")]
opt: String,
}
}
fn main() {
let cli = Cli::parse();
println!("verbose: {}", cli.verbose);
match cli.command {
Commands::Hoge { opt } => {
println!("hoge {}", opt);
}
Commands::Fuga(fuga) => {
println!("fuga {}", fuga.opt);
println!("global {}", fuga.global);
match fuga.command {
Some(c) => {
match c {
SubCommands::Nested { opt } => {
println!("nested {}", opt);
}
}
}
None => {
println!("sub command is not specified");
}
}
}
}
}
単に arg(global = true)
にするだけで、そのオプションはそのコマンド以下全てのサブコマンドで有効になります。ただし、サブコマンドの Args
から急にそのパラメータが生えてくるわけではなく、あくまで global = true
を設定した struct に値は入るため、ログレベルの変更などはそこで行わなければなりません。
❯ cargo run -q -- --verbose hoge --opt 1
verbose: true
hoge 1
❯ cargo run -q -- hoge --opt 1 --verbose
verbose: true
hoge 1
❯ cargo run -q -- fuga --opt 1 --verbose
verbose: true
fuga 1
global false
sub command is not specified
「そのサブコマンド以下全て」であり、全体ではないのでこの例のように fuga
サブコマンドで global
なオプションを実装しても、それは hoge
サブコマンドでは使えません。
❯ cargo run -q -- fuga --opt 1 --global
verbose: false
fuga 1
global true
sub command is not specified
❯ cargo run -q -- hoge --opt 1 --global
error: Found argument '--global' which wasn't expected, or isn't valid in this context
If you tried to supply '--global' as a value rather than a flag, use '-- --global'
Usage: subcommand hoge <--opt <OPT>>
For more information try '--help'
テスト
ここでは、単に引数のパースに関するテストのみを主眼とし、ツール全体の試験については考えないこととします。今回の実装では雑に src/main.rs
にユニットテストを記述していますが、 src/lib.rs
ないしその他ライブラリクレートに突っ込む方が普通のやり方だと思います。
Argsに実装されている try_parse_from
を使うことで任意の引数を処理させることができます。そのパース結果はResultで返ってくるので、エラーになる場合のテストなども書くことが出来ます。
例えば一つの位置引数を取る場合のテストを書いてみます。
#[cfg(test)]
mod test {
use super::*;
#[test]
fn one_arg() {
let args = Args::try_parse_from(["", "hoge"]);
assert!(args.is_ok());
assert_eq!(args.unwrap().pos_arg, "hoge".to_string());
}
}
こんな感じで、引数を入力して出力をチェックする普通のユニットテストとして記述できます。
おわりに
一応一通り自分で実装して動かした物ですが、非効率な書き方をしているかもしれません。改善点などあれば指摘ください。
公式のtutorialやcoolbookにはこれ以外の例や、より詳細な実装もあるのでそちらも参照してみてください。
docs.rs
docs.rs