2019/10/23追記
structopt v0.3にてbreaking changeが入ったため、それに合わせてソースコードを更新しました。
はじめに
structoptはRustのコマンドラインパーサです。コマンドラインパーサとしてはclapが有名ですが、structoptはstruct定義からclapのコードを自動生成するものです。
本家のドキュメントを含めてあまり細かい例が書かれた記事がなかったので、ここではそれなりに実践的な例を示します。
ちなみに現在計画中のclap v3ではstructopt相当の機能が取り込まれるようです。記法が変わる可能性はありますが、似たような書き方がclapでも使えるようになりそうです。
ドキュメント
まず最初に参照すべきドキュメントについて書きます。先に述べた通り、structoptはclapのコードを生成します。そのため細かい設定をしようとするとstructoptよりclapのドキュメントを読むことになります。structoptのドキュメントもあまり詳細な具体例が載っていないのですが、詳細を書いていくとclapの説明になってしまうためやむを得ない感じもします。
サンプル
以下のソースを使って解説していきます。
use lazy_static::lazy_static;
use regex::Regex;
use std::path::PathBuf;
use structopt::clap::arg_enum;
use structopt::{clap, StructOpt};
#[derive(Debug, StructOpt)]
#[structopt(name = "App name")]
#[structopt(long_version(option_env!("LONG_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))))]
#[structopt(setting(clap::AppSettings::ColoredHelp))]
pub struct Opt {
#[structopt(name = "WORDS")]
pub words: Vec<String>,
#[structopt(short = "n")]
pub count: Option<usize>,
// before Rust 1.32
//#[structopt(short = "p", long = "path", parse(from_os_str))]
//pub path: PathBuf,
#[structopt(short = "p", long = "path")]
pub path: PathBuf,
#[structopt(short = "r", long = "regex")]
pub regex: Regex,
#[structopt(
short = "t",
long = "threads",
default_value(&THREADS),
value_name = "NUM"
)]
pub threads: usize,
#[structopt(
short = "m",
long = "method",
possible_values(&Method::variants())
)]
pub method: Option<Method>,
#[structopt(short = "a", conflicts_with_all(&["b", "c"]))]
pub a: bool,
#[structopt(short = "b", conflicts_with_all(&["a", "c"]))]
pub b: bool,
#[structopt(short = "c", conflicts_with_all(&["a", "b"]))]
pub c: bool,
#[structopt(short = "v", long = "verbose")]
pub verbose: bool,
#[structopt(subcommand)]
pub sub: Sub,
}
#[derive(Debug, StructOpt)]
pub enum Sub {
#[structopt(name = "sub1", about = "sub command1")]
#[structopt(setting(clap::AppSettings::ColoredHelp))]
Sub1,
}
arg_enum! {
#[derive(Debug)]
pub enum Method {
A,
B,
C,
}
}
lazy_static! {
static ref THREADS: String = format!("{}", num_cpus::get());
}
fn main() {
let _ = Opt::from_args();
}
これは以下のようなコマンドラインオプションを生成します。
実践といいつつ名前が適当な感じですいません…。
$ structopt-sample --help
App name 0.1.0
dalance@gmail.com
USAGE:
structopt-sample [FLAGS] [OPTIONS] --path <path> --regex <regex> [WORDS]... <SUBCOMMAND>
FLAGS:
-a
-b
-c
-h, --help Prints help information
-V, --version Prints version information
-v, --verbose
OPTIONS:
-n <count>
-m, --method <method> [possible values: A, B, C]
-p, --path <path>
-r, --regex <regex>
-t, --threads <NUM> [default: 32]
ARGS:
<WORDS>...
SUBCOMMANDS:
help Prints this message or the help of the given subcommand(s)
sub1 sub command1
この表示は白黒ですが、実際にはclapがいい感じに色付けして表示してくれます。
clapのインポート
structoptはclapをエクスポートしています。clap以下を参照することはよくあるのでuseしておくと便利です。
use structopt::{clap, StructOpt};
パーサ全体の設定
まず#[derive(StructOpt)]
でこのstructにstructoptを適用することを示します。
#[derive(Debug, StructOpt)]
pub struct Opt {
}
structへのderiveでパーサの設定を渡せます。これはclap::Appの呼び出しに相当するのでclap側のドキュメントを見ながら必要な関数を呼んでいきましょう。
#[structopt(name = "App name")]
関数名 = 引数
でその引数を使った関数呼び出しができるので、この例だとname("App name")
を呼ぶコードが生成されます。
ただ、このように書くと関数の引数にリテラルしか渡すことができません。&str
やusize
を要求する関数はいいのですが、例えばenumを渡したい場合や何らかのコードの実行結果を渡したい場合はrawで囲みます。こうすると引数部分の文字列リテラルをRustのコードとして評価した結果を渡すことができます。
structopt v0.3でリテラルでない任意のコードを渡すことができるようになり、rawは不要になりました。
#[structopt(setting(clap::AppSettings::ColoredHelp))]
この例ではhelpを色付けして表示する設定をしています。
long_versionにgitリビジョンを入れる
開発中のバイナリを部分的に配布したりするときなど、gitのリビジョン情報を埋め込んでおきたいことがあります。
#[structopt(long_version(option_env!("LONG_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))))]
このようにすることでコンパイル時に環境変数LONG_VERSION
が設定されていればlong_version
がその値に、なければCargo.toml
のバージョン(CARGO_PKG_VERSION
)が使われます。
すると以下のようにgitリビジョンを埋め込むことができます。
$ LONG_VERSION=`git rev-parse HEAD` cargo build
各オプションの設定
structのメンバへのderiveでオプション毎の設定を書くことができます。こちらはclap::Argの呼び出しになるのでそちらのドキュメントから必要なものを探します。
オプションの型
オプションの型によってそのオプションが必須かどうかや複数取れるかどうかを示します。
取れる型はbool
とFromStr
を実装した型及びそのVec
とOption
です。
#[structopt(name = "WORDS")]
pub words: Vec<String>,
#[structopt(short = "n")]
pub count: Option<usize>,
#[structopt(short = "v", long = "verbose")]
pub verbose: bool,
デフォルト値を動的に変える
raw
を使えばdefault_value
の値を動的に変えることができます。ただ、deriveにすべて書くのは可読性が悪いので、lazy_static!
と組み合わせるといいでしょう。
#[structopt(
short = "t",
long = "threads",
default_value(&THREADS),
value_name = "NUM"
)]
pub threads: usize,
ここではlazy_static!
を使ってCPU数を取得してthreads
オプションのデフォルト値としています。
lazy_static! {
static ref THREADS: String = format!("{}", num_cpus::get());
}
列挙型
オプション引数に特定のパターンしか許可しない場合は、直接列挙型にすることができます。
列挙型定義をarg_enum!
で囲んで
arg_enum! {
#[derive(Debug)]
pub enum Method {
A,
B,
C,
}
}
possible_values
に渡します。
#[structopt(
short = "m",
long = "method",
possible_values(&Method::variants())
)]
pub method: Option<Method>,
配列を渡す
conflicts_with
など文字列リテラルの配列を渡すケースがありますが、これも素のderiveでは書けないのでraw
にします。
この例では-a
、-b
、-c
はどれか一つしか指定できません。
#[structopt(short = "a", conflicts_with_all(&["b", "c"]))]
pub a: bool,
#[structopt(short = "b", conflicts_with_all(&["a", "c"]))]
pub b: bool,
#[structopt(short = "c", conflicts_with_all(&["a", "b"]))]
pub c: bool,
型変換
FromStr
が実装されている型はそのまま受け取れますがそうでない場合何らかの変換が必要です。
例えばPathBuf
を受けとりたい場合は以下でした。
#[structopt(short = "p", long = "path", parse(from_os_str))]
pub path: PathBuf,
この変換は頻出なのでstructoptでfrom_os_str
が定義されていてOsStr
経由で変換できるとドキュメントにも書いてありますが、実はRust1.32からPathBuf
にもFromStr
が実装されたので、今はこれでも通ります。
#[structopt(short = "p", long = "path")]
pub path: PathBuf,
例えばRegex
もFromStr
を実装しているのでそのまま通ります。
#[structopt(short = "r", long = "regex")]
pub regex: Regex,
サブコマンド
サブコマンドはメインのstructにenumを入れて#[structopt(subcommand)]
で指定します。
#[structopt(subcommand)]
pub sub: Sub,
#[derive(Debug, StructOpt)]
pub enum Sub {
#[structopt(name = "sub1", about = "sub command1")]
#[structopt(setting(clap::AppSettings::ColoredHelp))]
Sub1,
}
clap::Appの設定はサブコマンド毎に独立なのでそれぞれ設定する必要があります。
ここでのclap::AppSettings::ColoredHelp
はsub1 --help
したときの色付け設定になります。
メインのstructなしにいきなりenumから書いても大丈夫です。
パース実行
最後にfrom_args
を呼ぶことで、パース結果のstructが得られます。
let _ = Opt::from_args();
パースエラーはclap側で(エラーメッセージの表示からアプリの終了まで)ハンドリングされるので、ここでは正常にパースされたstructそのものが返ってきます。