Rust で structopt で簡単 CLI ツール

この記事は [ユニークビジョン株式会社 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/uniquevision) の 11 日目の記事です。

# はじめに

Rust 勉強中です。（初心者です）
Rust で CLI ツールを作ろうとしたときに [structopt](https://github.com/TeXitoi/structopt) が便利だったのでご紹介します。


... ちなみに、[clap](https://github.com/clap-rs/clap) というクレートでも同じ書き方でコマンドを定義できるようです。
https://github.com/clap-rs/clap#using-derive-macros

>  The first example shows the simplest way to use clap, by defining a struct. If you're familiar with the structopt crate you're in luck, it's the same! (In fact it's the exact same code running under the covers!)

（structopt は clap を使って実装されているので、clap を使った例のほうがよかったのかもしれません）

# 準備

プロジェクトを用意します。

```
cargo new my-cli
```

`Cargo.toml` の `dependencies` に以下を追加します。

```
structopt = "0.3.21"
```

# 引数として受け取る

引数を受け取って、そのまま返す簡単なコマンドを作成します。

-   構造体の定義の上のところに `#[derive(StructOpt)]` を記述します。
-   `from_args()` を呼び出すと、実行時に引数から受け取った値を構造体として得ることができます。

```rust
use structopt::StructOpt;

#[derive(StructOpt)]
struct Opt {
    name: String,
}

fn main() {
    let opt = Opt::from_args();
    println!("{}", opt.name);
}
```

実行すると以下のようになります。

```sh
$ cargo run hoge
hoge
```

# オプションとして受け取る

オプションとして使いたいメンバには `#[structopt(short, long)]` を記述します。

-   デフォルトではメンバ変数の名前がオプションの名前として使用されます。
-   `short = "g"` とすると、そのオプションを `-g` で呼び出せるようになります。

```rust
use structopt::StructOpt;

#[derive(StructOpt)]
struct Opt {
    name: String,
    #[structopt(short = "g", long)]
    greet: bool,
}

fn main() {
    let opt = Opt::from_args();

    if opt.greet {
        println!("Hello, {}!", opt.name);
    } else {
        println!("{}", opt.name);
    }
}
```

```sh
$ cargo run hoge
hoge

$ cargo run hoge --greet
Hello, hoge!

$ cargo run hoge -g
Hello, hoge!
```

`greet` 変数の型を `Option<String>` にすれば文字列を渡せます。

（もしここで `Option<String>` ではなく `String` にすれば `--greet` は必須のオプションになります。）

```rust
use structopt::StructOpt;

#[derive(StructOpt)]
struct Opt {
    name: String,
    #[structopt(short = "g", long)]
    greet: Option<String>,
}

fn main() {
    let opt = Opt::from_args();

    match opt.greet {
        Some(greet) => println!("{}, {}!", greet, opt.name),
        None => println!("{}", opt.name),
    }
}
```

```sh
$ cargo run hoge -g Hi
Hi, hoge!
```

# その他

## 環境変数から値を受け取る

個人的にとても使いやすそうだと思いました。

```rust
use structopt::StructOpt;

#[derive(StructOpt)]
struct Opt {
    name: String,
    #[structopt(short = "g", default_value = "Hello", env = "GREET")]
    greet: String,
}

fn main() {
    let opt = Opt::from_args();
    println!("{}, {}!", opt.greet, opt.name);
}
```

```
$ cargo run hoge
Hello, hoge!

$ cargo run hoge -g Hi
Hi, hoge!

$ GREET=Hi cargo run hoge
Hi, hoge!
```

## `Vec<String>`として値を受け取る

たとえば `Vec<String>` として引数の値を受け取ることができます。

-   `min_values = 2` とすると、引数が 1 つしか与えられない場合にエラーになってくれます。

```rust
use structopt::StructOpt;

#[derive(StructOpt)]
struct Opt {
    #[structopt(required = true, min_values = 2)]
    names: Vec<String>,
}

fn main() {
    let opt = Opt::from_args();
    println!("Hello! {}!", opt.names.join(", "));
}
```

```sh
$ cargo run hoge fuga
Hello! hoge, fuga!
```

## サブコマンドを作成する

```rust
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
/// help comment here!!!
enum Opt {
    /// greet someone
    Greet { name: String },
    /// say goodbye to someone
    Bye {
        name: String,
        #[structopt(short = "f", long)]
        forever: bool,
    },
}

fn main() {
    match Opt::from_args() {
        Opt::Greet { name } => println!("{} さん、こんにちは。", name),
        Opt::Bye { name, forever } => {
            if forever {
                println!("{} さん、永遠にさようなら。", name)
            } else {
                println!("{} さん、さようなら。", name)
            }
        }
    };
}
```

実行例 :

```sh
cargo run help
my-cli 0.1.0
help comment here!!!

USAGE:
    my-cli <SUBCOMMAND>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

SUBCOMMANDS:
    bye      say goodbye to someone
    greet    greet someone
    help     Prints this message or the help of the given subcommand(s)
```

```sh
$ cargo run greet hoge
hoge さん、こんにちは。
```

```sh
$ cargo run bye fuga -f
fuga さん、永遠にさようなら。
```

# おわりに

structopt を使用すれば Rust のプログラムを簡単に CLI にできます。ぜひ使ってみてください。

簡単ですが以上です。ありがとうございました。