はじめに
rustでCLIツールを作ろうとした際に最初に候補にあがるライブラリがclapだろう。実際、Rust CLI working groupがまとめているリポジトリでは、Useful cratesの章でclapが紹介されている。
ただこの記事を執筆している時点での最新バージョンは3.0.5なのに対し、ググって出てくる記事で紹介されているコードがバージョン2.xのものだったりする。3.0がリリースされたのはこの記事を書いた数日前でした。clapは2.xから3.0に上がるタイミングでいくつかのマクロが廃止された他、deprecatedになった実装が多数存在する。また新機能として構造体を定義するとパーサーを生成してくれる機能が追加され、これがめっぽう使い易いのでぜひ使って頂きたいのもある。
ネット上のコードをとりあえずコピペしても動作しない場合があるため、「とりあえず使ってみたい」という人にとって現状は大変不便な状況である。本記事ではこの状況をふまえ、最新版で動作するサンプルコードを提示したい。
clapとはどんなライブラリなのか?
clapはコマンドラインから渡された引数を解析するためのライブラリ。使用感はNode.jsにおけるyargsに近く、メソッドチェーンによりツールのインターフェースを定義することができる。だがNode.jsと違い、Rustではclap::Parser
トレイトを継承したstruct
を定義することで宣言的にオプション引数などを指定することもできるようになっている。筆者的にはこちらの書き方の方が記述がすっきりするので好き。本記事では両方の書き方を紹介する。
執筆時点の環境
- OS: Ubuntu 20.04.3 LTS
- rustc: 1.56.1 (59eed8a2a 2021-11-01)
- cargo: 1.56.0 (4ed5d137b 2021-10-04)
- clap: 3.0.5
- clap_derive: 3.0.5
まずはいちばん単純な定義方法から
Cargo.tomlの指定には特に変わったところはない。
# ... 前略 ...
[dependencies]
clap = "3.0.5"
このとき、main.rsの記述は下記の通りとなる。
use clap::{App, Arg};
fn main() {
// 任意のオプション(デフォルト値の指定も可能)
let optional_opt: Arg = Arg::new("name") // 入力値を取得する際の識別子を指定
.short('n') // オプション(短)に使用する文字を指定
.long("name") // オプション(長)に使用する文字列を指定
.default_value("Alice"); // デフォルト値を指定(※暗黙的にtakes_value(true)が指定される)
// 必須のオプション
let required_opt: Arg = Arg::new("count")
.short('c')
.long("count")
.takes_value(true) // オプションが値を要求するかどうかを設定
.required(true);
// 位置引数
let positional_arg: Arg = Arg::new("message")
.required(true);
// 上記で定義したオプションや引数は下記のようにAppに渡す
let app: App = App::new("My Application")
.author("Author's name")
.version("v1.0.0")
.about("Application short description.")
.arg(optional_opt)
.arg(required_opt)
.arg(positional_arg);
// Appを設定し終わったら下記のようにしてコマンドラインからのデータを解析することができる
// ArgMatchesを取得する関数には"try_"で始まるものとそうでないものがある。
// "try_"が付いていない方は渡されたデータが不正だった場合にプロセスが終了するため、
// "try_"系を使った方が安全ではある。
// ただほとんどのCLIツールは引数指定が間違っていた場合はエラーとヘルプを出力すると思うが、
// そういった挙動はclap側で実装されているため、あまり神経質になる必要はなさそう。
// とりあえずこのサンプルでは簡単なエラーハンドリングを例示しておく。
match app.try_get_matches() {
Ok(m) => {
let name = m.value_of("name").unwrap();
let count = m.value_of("count").unwrap().parse::<i32>().unwrap();
let message = m.value_of("message").unwrap();
for _ in 0..count {
println!("{}: {}", name, message);
}
},
Err(e) => {
println!("{}", e);
}
}
}
derive featureを利用して見通しを良くする
上記でわかる通り、基本的にclapはArg
とApp
を利用してインターフェースを定義していく。すこしオプションの数を増やすと一気にコードの見通しが悪くなってしまう点には注意が必要だ。そこでfeatureとして定義されているclap_deriveを使用する。まずはCargo.tomlを以下の通り変更する。
[dependencies]
- clap = "3.0.5"
+ clap = { version = "3.0.5", features = ["derive"] }
これでclap::Parser
トレイトが使用できるようになる。Parser
を使用すると上記のコードは下記のように変更できる。コードで記述していたものが宣言的にかけるようになり、メインの処理がかなり見通しがよくなったことがわかるだろう。また整数が渡される前提だったパラメーターcount
の文字列→整数への変換処理が必要なくなったことにも注目して頂きたい。
use clap::Parser;
// Parserを継承した構造体はArgの代わりに使用することが可能。
#[derive(Parser)]
#[clap(
name = "My Application",
author = "Author's name",
version = "v1.0.0",
about = "Application short description."
)]
struct AppArg {
// 任意のオプション
#[clap(short, long)]
name: Option<String>,
// 必須のオプション
#[clap(short = 'c', long = "count")]
count: i32,
// 位置引数
message: String,
}
fn main() {
let arg: AppArg = AppArg::parse();
for _ in 0..arg.count {
println!(
"{}: {}",
arg.name.clone().unwrap_or(String::from("Alice")),
arg.message
);
}
}
ちなみに、上記コードでは「任意項目にしたい場合はOptionで型を指定すればよい」と示すためにわざとデフォルト値を宣言しなかったが、当然これも可能である。
struct AppArg {
// 任意のオプション
- #[clap(short, long)]
- name: Option<String>,
+ #[clap(short, long, default_value = "Alice")]
+ name: String,
サブコマンドを定義する
サブコマンドの定義方法は、App::new()
を使用するパターンとParser
を使うパターンのどちらにも用意されている。
use clap::{App, Arg};
fn main() {
let subcommand1 = App::new("echo").arg(Arg::new("message").required(true));
let subcommand2 = App::new("greet").arg(Arg::new("name").required(true));
let app = App::new("Application")
.subcommand(subcommand1)
.subcommand(subcommand2);
match app.get_matches().subcommand() {
Some(("echo", sub_m)) => {
println!("{}", sub_m.value_of("message").unwrap());
}
Some(("greet", sub_m)) => {
println!("hello, {}", sub_m.value_of("name").unwrap());
}
_ => {}
}
}
// ここでuseしているのはSubcommandトレイトであって、SubCommand構造体ではない点に注意。
// 名前の"C"が大文字かどうかしか違いがなく、大変間違えやすい。
// SubCommand構造体はバージョン3.0からdeprecatedされており、今後廃止予定である。
use clap::{Parser, Subcommand};
#[derive(Parser)]
struct AppArg {
#[clap(subcommand)]
action: Action,
}
#[derive(Subcommand)]
enum Action {
Echo { message: String },
Greet { name: String },
}
fn main() {
let cli = AppArg::parse();
match cli.action {
Action::Echo { message } => {
println!("{}", message);
},
Action::Greet { name } => {
println!("hello, {}", name);
}
}
}
AppとParserのどちらを使うべきか
個人的には処理とインターフェースの定義を分離しやすいParser
を使うパターンの方が好みだが、細かい制御はApp::new()
の方が得意である。例えば、2つのオプションの内一方のみを指定してもらいたい場合の制御はParser
パターンの方が記述量が増えてしまうため不利である。