Rustのコマンドラインパーサーであるclapを使う

https://qiita.com/advent-calendar/2023/ne

こんにちは。この記事はNE Advent Calendar2日目の記事です。一年は早いですね。今年も色んなことがありました。ベースを再始動したり、ピアノを買ったり、比較的登山とかもできた気がしますが、何一つ身になっていない気もします。

とはいえ、食生活を変えたり、運動をし始めたりという良い習慣の流れはつくられ始めたので、また来年も続けられるといいなぁ、健康のトピックが家族親族で増えてきたのはやはり加齢のせいかもなぁと淡々と感じています。

さて、Rustのコマンドオプション解析のCrateである [Clap](https://github.com/clap-rs/clap) の設定方法ががよく分からなくなるのでチップス的にアトリビュートの情報を書いておきます。

## clapとは

https://github.com/clap-rs/clap

Rustで作られたコマンドもよく使われるようになったかと思いますが、必ず必要なのに引数のオプション解析が面倒くさいです。その解析部分の面倒をよく見てくれるCrateです。デフォルトでは `std::env::args` 、同じようなCrateに `getopts` などがあります。

clapにはメソッドチェーン形式でオプションの定義を書いていく `Builder形式` （[リファレンス](https://docs.rs/clap/latest/clap/index.html)） と、マクロで宣言的に書いていく `Derive形式` （[リファレンス](https://docs.rs/clap/latest/clap/_derive/index.html)） が使えます。Derive形式の方が冗長にならなくていいのかな、という所感で、本記事でもそちらしか書いていません。

clapの最新は2023/12/02時点の最新、4.4.10を使っています。

## 準備

`cargo new clap_test` などで適当なプロジェクトを作り、 `cargo add clap --features derive` でderive形式も含めて使えるようにしておきます。さらにこの [example](https://docs.rs/clap/latest/clap/#example) を `src/main.rc` に書いて、色々変えていきながら表示を試します。

```rust
use clap::Parser;

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Name of the person to greet
    #[arg(short, long)]
    name: String,

    /// Number of times to greet
    #[arg(short, long, default_value_t = 1)]
    count: u8,
}

fn main() {
    let args = Args::parse();

    for _ in 0..args.count {
        println!("Hello {}!", args.name)
    }
}
```

これをcargo runするともう現時点でオプションのフォーマットを整えて出力してくれます。

```shell
$ cargo run -- --help
   Compiling clap_test v0.1.0 (/Users/masakuni/Desktop/work/tmp/clap_test)
    Finished dev [unoptimized + debuginfo] target(s) in 3.59s
     Running `target/debug/clap_test --help`
Simple program to greet a person

Usage: clap_test [OPTIONS] --name <NAME>

Options:
  -n, --name <NAME>    Name of the person to greet
  -c, --count <COUNT>  Number of times to greet [default: 1]
  -h, --help           Print help
  -V, --version        Print version
```

多幸感ある。

## #[derive(Parser)]

Args構造体に clap の [Parserトレイト](https://docs.rs/clap/latest/clap/trait.Parser.html) を実装します。これによってこの構造体が引数の定義としておおむね扱えるようになります。

```rust
let args = Args::parse();
```

先の例では実装されたParserを実行し、ここで `name` オプションと `count` オプションを引数に取れる構造体を解析した、ということ。

## #[command(author)]

このコマンドのauthorを設定します。

```rust
#[command(author = "Masakuni Ito")]
```

とも書けるし、書かなかった場合はCargo.tomlの `authors` を自動的に参照してくれるようです。ただ、このauthorは現状、基本的に出力されない模様で、テンプレート変更時などに利用できるようです。

https://github.com/clap-rs/clap/discussions/4316

## #[command(version)]

このコマンドのバージョンを指定します。値を指定しなければCargo.tomlの値が参照されます。

```rust
#[command(version = "0.0.100")]
```

上記のように値を書くこともできて、この場合、Cargo.tomlより優先されますが、まぁ、あんまりここで指定することはないんじゃないかなぁ、という感想。

```shell
masakuni@mi-air % cargo run -- --version
    Blocking waiting for file lock on build directory
   Compiling clap_test v0.1.0 (/Users/masakuni/Desktop/work/tmp/clap_test)
    Finished dev [unoptimized + debuginfo] target(s) in 3.67s
     Running `target/debug/clap_test --version`
clap_test 0.0.100
```

## #[command(about)]

このコマンドの概要を書きます。マクロ、Cargo.toml、ドキュメントコメントで指定することができて、それぞれ下記の優先度のようです。Cargo.tomlは `about` ではなく、 `description` の値が使われます。

```rust
# マクロで直接指定する
#[command(about = "一番優先されるぽい")]

# Cargo.tomlのdescription
description = "二番目に優先されるぽい"

# src/main.rc上部に書かれていたドキュメントコメントのサマリ
/// 三番目に優先されるぽい
```

```shell
$ cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/clap_test --help`
一番優先されるぽい

Usage: clap_test [OPTIONS] --name <NAME>

Options:
  -n, --name <NAME>    Name of the person to greet
  -c, --count <COUNT>  Number of times to greet [default: 1]
  -h, --help           Print help
  -V, --version        Print version
```

じゃあ全部書かれてなかった場合はどうなるんだというと、何も記載されなくなります。ドキュメントコメントまで読んでくれるあたりがとても賢い。


## #[command(long_about = None)]

コマンドのヘルプが長めに書けるようになります。 `--help` を指定したとき、オプションの説明がさらに丁寧なものになります。これもドキュメントコメントまで読んでくれます。

```rust
#[command(long_about = "ここに長めのコマンドの説明を書くことができます。\n様々な情熱に動かされながらこのコマンドを作ったストーリーを語ってもよいでしょう。")]
```

というように書くと

```shell
cargo run -- --help
   Compiling clap_test v0.1.0 (/Users/masakuni/Desktop/work/tmp/clap_test)
    Finished dev [unoptimized + debuginfo] target(s) in 2.44s
     Running `target/debug/clap_test --help`
ここに長めのコマンドの説明を書くことができます。
様々な情熱に動かされながらこのコマンドを作ったストーリーを語ってもよいでしょう。

Usage: clap_test [OPTIONS] --name <NAME>

Options:
  -n, --name <NAME>
          Name of the person to greet

  -c, --count <COUNT>
          Number of times to greet

          [default: 1]

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version
```

というようなオプションになります。

## #[arg(short)]

短縮されたオプションを自動的に受け入れるようになります。冒頭のスクリプトのオプションだと `name` と `count` のオプションが自動的に `-n` や `-c` で受け取れるようになります。

他にもArgsの構造体を

```rust
struct Args {
    #[arg(short, long)]
    first: bool,
    #[arg(short, long)]
    second: bool,
    #[arg(short, long)]
    third: bool,
}
```

のようにboolのオプションを受け取るようなものだった場合、

```
cargo run -- -fst
```

一文字だけの連続でオプションを設定可能にできたりします。

## #[arg(long)]

正式なオプション名でオプションを受け取れるようにします。つまり `--` （ハイフン2つ）の形式でオプションを受け取ります。ちなみにこれがなかった場合、ハイフン2つでのオプションを受け取ることができません。へー。

```shell
$ cargo run -- --count 1 --name masa
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/clap_test --count 1 --name masa`
error: unexpected argument '--count' found

Usage: clap_test [OPTIONS] -n <NAME>

For more information, try '--help'.
```

## #[arg(default_value_t = 1)]

オプションのデフォルト値を設定しておきます。この場合は1。 `_t` は何なんだ？　というと型指定されているという意味らしく、例えばこの場合は `count` の `u8` が指定されるようです。[この辺](https://github.com/clap-rs/clap/discussions/3640)に言及があります。

要するに

```rust
    /// Number of times to greet
    #[arg(short, long, default_value_t = "abcd")]
    count: u8,
```

こういうことをやると

```shell
$ cargo run -- --count 1 --name masa
   Compiling clap_test v0.1.0 (/Users/masakuni/Desktop/work/tmp/clap_test)
error[E0308]: mismatched types
  --> src/main.rs:17:42
   |
17 |     #[arg(short, long, default_value_t = "abcd")]
   |                                          ^^^^^^ expected `u8`, found `&str`
18 |     count: u8,
   |            -- expected due to this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `clap_test` (bin "clap_test") due to 2 previous errors
```

ビルドエラーにしてくれます。しかし、

```rust
    /// Number of times to greet
    #[arg(short, long, default_value = "abcd")]
    count: u8,
```

`default_value` のアトリビュートを使うと、型まできっちり見てくれず、この場合でもビルドは通ってしまいます。このため `default_value_t` の方が推奨、とのことです。

## まだ指定できるものがありすぎるが

宣言的に書かれたオプションの構造体を `Parser::Parse()` で解析することで自在に取得できるclapすごい、というところでおしまい。ドキュメントコメントまで読んでくれるところがとても良いですね。