41
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

実践structopt: Rustのコマンドラインパーサを使う

Last updated at Posted at 2019-04-29

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")を呼ぶコードが生成されます。

ただ、このように書くと関数の引数にリテラルしか渡すことができません。&strusizeを要求する関数はいいのですが、例えば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の呼び出しになるのでそちらのドキュメントから必要なものを探します。

オプションの型

オプションの型によってそのオプションが必須かどうかや複数取れるかどうかを示します。
取れる型はboolFromStrを実装した型及びそのVecOptionです。

    #[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,

例えばRegexFromStrを実装しているのでそのまま通ります。

    #[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::ColoredHelpsub1 --helpしたときの色付け設定になります。
メインのstructなしにいきなりenumから書いても大丈夫です。

パース実行

最後にfrom_argsを呼ぶことで、パース結果のstructが得られます。

    let _ = Opt::from_args();  

パースエラーはclap側で(エラーメッセージの表示からアプリの終了まで)ハンドリングされるので、ここでは正常にパースされたstructそのものが返ってきます。

41
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?