日頃の行い

個人的な日頃の行いをつらつら書いてます\\\\ ٩( 'ω' )و ////

Rustのgoogle/arghライブラリで引数をstructにbindする

GWにRustを触り始めたら楽しくなってきました。
新しい言語を触るのはやはり楽しいです。
今回はなんやかんやするコマンドを作ろうと思って、
引数をいい感じにしてくれるライブラリないかなと思って調べてたら、
googleのarghというライブラリがあったのでそれを触った備忘録です。

google/arghはこちら

github.com

基本的に使用するCargo.tomlはinitして、dependenciesにarghを追加した下記のようなのものでした。

Cargo.toml

[package]
name = "argh_opt"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
argh = "0.1.7"

目次

1.1 --hoge fugaのようなオプションを設定する

よくある ./bin/foo --hoge fuga みたいなやつですね。
これを実装してみましょう。

src/main.rs

use argh::FromArgs;

#[derive(FromArgs, Debug)]
/// 
struct FooArg {
    /// hoge
    #[argh(option)]
    hoge: String,
}

fn main() {
    let a: FooArg = argh::from_env();
    println!("{:?}", a);
    println!("{:?}", a.hoge);
}

/// があるのは、descriptionを書くのが必須のようで、エラーになってしまうからこうしました。
argh(description= "...")とも書けるようですが、ドキュメンテーションコメントで書いても拾ってくれるみたいでした。
これを実行してみるとこんな感じになります。

$cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/argh_opt`
Required options not provided:
    --hoge

$cargo run -- --hoge fuga
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/argh_opt --hoge fuga`
FooArg { hoge: "fuga" }
"fuga"

これで--hogeが必須のオプションになりました。
--hogeを必須ではなく任意にしたい場合は下記のように型を Option[String] にするといいようです。

struct FooArg {
    /// hoge
    #[argh(option)]
    hoge: Option[String],
}

1.2 数字やbool値を引数に渡す

次は ./bin/foo --hoge bar -- fuga --piyo 100 みたいなものを目指します。
数字は型に数字系のデータ型を指定し、
boolはoptionではなくswitchを指定することで使えるようでした。

use argh::FromArgs;

#[derive(FromArgs, Debug)]
/// 
struct FooArg {
    /// hoge
    #[argh(option)]
    hoge: String,

    /// fuga
    #[argh(switch)]
    fuga: bool,

    /// piyo
    #[argh(option)]
    piyo: i32,
}

fn main() {
    let a: FooArg = argh::from_env();
    println!("{:?}", a);
    println!("{:?} {:?} {:?}", a.hoge, a.fuga, a.piyo);
}
$cargo run -- --hoge foo --fuga --piyo 10
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/argh_opt --hoge foo --fuga --piyo 10`
FooArg { hoge: "foo", fuga: true, piyo: 10 }
"foo" true 10

# 数字の部分に文字列を渡したらエラーにしてくれました
$cargo run -- --hoge foo --fuga --piyo bar
    Finished dev [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/argh_opt --hoge foo --fuga --piyo bar`
Error parsing option '--piyo' with value 'bar': invalid digit found in string

zsh: exit 1     cargo run -- --hoge foo --fuga --piyo bar

1.3 デフォルト値を設定する

default = "関数名"で指定してあげると良さそうでした。
ダブルクオートの中に文字列で関数を指定するのどことなく不思議な感じがしますね。

use argh::FromArgs;

#[derive(FromArgs, Debug)]
/// 
struct FooArg {
    /// a
    #[argh(option, default = "String::from(\"hoge1\")")]
    hoge1: String,
    /// a
    #[argh(option, default = "default_hoge()")]
    hoge2: String,
    /// a
    #[argh(option, default = "\"hoge\".to_string()")]
    hoge3: String,
}

fn main() {
    let a: FooArg = argh::from_env();
    println!("{:?}", a);
    println!("{:?} {:?} {:}", a.hoge1, a.hoge2, a.hoge3);
}

fn default_hoge() -> String {
    return "hoge2".to_string();
}

なお、関数の型があっていないときはコンパイルエラーになりました。
例えば上記のdefault_hogeをi32が返るように変更し、
コンパイルするとエラーになった様子です。

fn default_hoge() -> i32 {
    return 100;
}
error[E0308]: mismatched types
  --> src/main.rs:10:30
   |
10 |     #[argh(option, default = "default_hoge()")]
   |                              ^^^^^^^^^^^^^^^^- help: try using a conversion method: `.to_string()`
   |                              |
   |                              expected struct `String`, found `i32`

便利ですね〜

1.4 from_str_fnを使って文字列をstructに変換する

なんらかのフォーマットのデータを入力時に変換してもらおうと思います。
例えば 14:15 のような時間のフォーマットを変換します。
./bin/foo --time 14:15 みたいなコマンドのイメージです。
from_str_fnに、 &strを受け取りResult<T, String>を返す関数を渡すといい感じに変換してくれるみたいでした。

use argh::FromArgs;

#[derive(FromArgs, Debug)]
/// 
struct FooArg {
    /// a
    #[argh(option, from_str_fn(str_to_time))]
    time: Time,
}

#[derive(Debug)]
struct Time {
    _h: u32,
    _m: u32,
}

fn str_to_time(v: &str) -> Result<Time, String> {
    let x: Vec<&str> = v.split(":").collect();
    let h = match x[0].parse::<u32>() {
        Ok(x) => x,
        Err(e) => return Err(e.to_string())
    };
    let m = match x[1].parse::<u32>() {
        Ok(x) => x,
        Err(e) => return Err(e.to_string())
    };

    if h >= 24 {
        Err(format!("{} is too large", h))
    } else if  m >= 60 {
        Err(format!("{} is too large", m))
    } else {
        Ok(Time { _h: h, _m: m })
    }
}

fn main() {
    let a: FooArg = argh::from_env();
    println!("{:?}", a);
    println!("{:?}", a.time);
}

実行してみると

$cargo run -- --time 14:15
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/argh_opt --time '14:15'`
FooArg { time: Time { _h: 14, _m: 15 } }
Time { _h: 14, _m: 15 }

# ResultのErr側に渡したStringをメッセージとして出してくれるようですね
$cargo run -- --time hoge
   Compiling argh_opt v0.1.0 (/Users/arata/Documents/Workspace/rust/argh_opt)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/argh_opt --time hoge`
Error parsing option '--time' with value 'hoge': invalid digit found in string

みたいな感じでした。

2.1 sub commandを使う

次はサブコマンドを実装してみます。
./bin/foo hoge./bin/foo fuga みたいなやつです。
これを実装してみます。
enumでコマンドを作成し、
それぞれarghのマクロにsubcommandと渡してあげれば良さそうでした。  

use argh::FromArgs;

#[derive(FromArgs, Debug)]
/// 
struct FooArg {
    #[argh(subcommand)]
    _command: SubCommand,
}

#[derive(FromArgs, Debug)]
#[argh(subcommand)]
enum SubCommand {
    Hoge(HogeCommand),
    Fuga(FugaCommand),
}

#[derive(FromArgs, Debug)]
/// h
#[argh(subcommand, name = "hoge")]
struct HogeCommand {}

#[derive(FromArgs, Debug)]
/// h
#[argh(subcommand, name = "fuga")]
struct FugaCommand {}


fn main() {
    let a: FooArg = argh::from_env();
    println!("{:?}", a);
}

実行してみるとこんな感じ

$cargo run -- hoge
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/argh_subcommand hoge`
FooArg { _command: Hoge(HogeCommand) }

$cargo run -- fuga
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/argh_subcommand fuga`
FooArg { _command: Fuga(FugaCommand) }

$cargo run -- piyo
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/argh_subcommand piyo`
Unrecognized argument: piyo

2.2 sub command内でオプションを使う

サブコマンドにオプションをつけるのは、
サブコマンドのstructに、1.xでやったようにオプションを追加するだけでよかったです。
試しにhogeコマンドにpiyoというオプションを追加してみます。

use argh::FromArgs;

#[derive(FromArgs, Debug)]
/// 
struct FooArg {
    #[argh(subcommand)]
    command: SubCommand,
}

#[derive(FromArgs, Debug)]
#[argh(subcommand)]
enum SubCommand {
    Hoge(HogeCommand),
    Fuga(FugaCommand),
}

#[derive(FromArgs, Debug)]
/// h
#[argh(subcommand, name = "hoge")]
struct HogeCommand {
    /// piyo
    #[argh(option)]
    piyo: String,
}

#[derive(FromArgs, Debug)]
/// h
#[argh(subcommand, name = "fuga")]
struct FugaCommand {}

fn main() {
    let a: FooArg = argh::from_env();
    println!("{:?}", a);
    match a.command  {
        SubCommand::Hoge(x) => println!("{}", x.piyo),
        SubCommand::Fuga(_) => (),
    }
}

実行してみるとこんな感じ

cargo run -- hoge --piyo fooooo
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/argh_subcommand hoge --piyo fooooo`
FooArg { command: Hoge(HogeCommand { piyo: "fooooo" }) }
fooooo

感想

  • とりあえずこれ使っておけばやりたいことはできそうだなーと思った
  • google製だから安心かなと思ったけどREADMEの最後に↓って書いてあって少し不安になったw
    • NOTE: This is not an officially supported Google product.

  • Rust楽しい