【Rust】✨CLIアプリ向けクレート4選✨ ~ clap, dialoguer, indicatif, console ~

本記事では、RustでCLIアプリケーションを作成する際に便利なクレートを4つ紹介いたします！ :crab: :qiitan:

筆者は大のRust好きですが、それを抜きにしても **CLIアプリ制作はRustが一番楽** と言い切ってしまえるぐらいのポテンシャルがこのクレート達(特にclap)にはあります！筆者にとって使用頻度が高い順に彼らを紹介していきたいと思います。

| クレート名 | 概要 | 項目へ |
|:--------|:----|:-:|
| [clap](https://docs.rs/clap/latest/clap/) | コマンドライン引数パーサー | [ :fast_forward: ](#%E3%81%8A%E6%89%8B%E8%BB%BD%E3%83%91%E3%83%BC%E3%82%B5%E3%83%BC-clap) |
| [dialoguer](https://docs.rs/dialoguer/latest/dialoguer/) | ユーザー入力ダイアログ | [ :fast_forward: ](#%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96-dialoguer) |
| [indicatif](https://docs.rs/indicatif/latest/indicatif/) | プログレスバー表示 | [ :fast_forward: ](#%E9%80%B2%E6%8D%97%E3%81%A9%E3%81%86%E3%81%A7%E3%81%99%E3%81%8B-indicatif) |
| [console](https://docs.rs/console/latest/console/) | 装飾・ANSIエスケープ | [ :fast_forward: ](#%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E8%89%B2%E4%BB%98%E3%81%91%E8%81%B7%E4%BA%BA-console) |
| その他 | [serde](https://docs.rs/serde/latest/serde/), [log](https://docs.rs/log/latest/log/), etc... | [ :fast_forward: ](#%E3%81%9D%E3%81%AE%E4%BB%96%E4%BE%BF%E5%88%A9%E3%81%AA%E3%82%AF%E3%83%AC%E3%83%BC%E3%83%88) |

:::note
とにかく **[clap](https://docs.rs/clap/latest/clap/)クレートが便利** です！
clapを使うだけで一瞬でCLIアプリケーションの体裁が整います :clap:

他3つは全て[console-rs](https://github.com/console-rs)が提供するユーティリティクレートで、かゆいところに手が届くイメージです :truck:
:::

4つのクレートを全て活用すると次のデモのようなCLIアプリケーションも簡単(※当社比)に作れます！

![langage_monster_6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/27ba3bfc-4df8-db22-2147-0feadb8cbb07.gif)

<details><summary>asciinema</summary>

<script src="https://asciinema.org/a/658022.js" id="asciicast-658022" async="true"></script>
</details>

リポジトリ: https://github.com/anotherhollow1125/lang_monster

Gif中の次のシーンにて本記事で紹介するクレートが使われています。

- `./lang_monster --help`
    - [clap](https://docs.rs/clap/latest/clap/)クレートによりヘルプオプションが追加されています
- アセットダウンロードプログレスバー
    - [indicatif](https://docs.rs/indicatif/latest/indicatif/)クレートを使って表示しています
- 左上始点でセリフ表示・色付き文字
    - [console](https://docs.rs/console/latest/console/)クレートにてANSIエスケープを出力することで実現しています
- 主人公名ライバル名の選択・入力
    - [dialoguer](https://docs.rs/dialoguer/latest/dialoguer/)クレートを使って実現しています
- オプション入力 (`./lang_monster -t auto -s fast -u Rust -r Go`)
    - [clap](https://docs.rs/clap/latest/clap/)クレートでパースしています

RustでCLIアプリケーションを作成するためのチュートリアルは色々あります。特に[Rust CLI WG](https://github.com/rust-cli)がまとめている[Command Line Applications in Rust](https://rust-cli.github.io/book/index.html)は各種トピックがバランスよく散りばめられている感じで良かったりするので、今さら普通にまとめてもn番煎じかもしれません。

https://rust-cli.github.io/book/index.html

そこで本記事のオリジナリティとして、CLI "らしさ" を備えたクレートをおすすめ順に、豊富なサンプルコードとともに紹介していければなと思います！

<details><summary>環境・前提</summary>

Rustの環境構築に関しては省略させていただきます。

ちなみにインストールはほぼ一瞬です！: [はじめに - Rustプログラミング言語](https://www.rust-lang.org/ja/learn/get-started)

```bash
$ cargo --version
cargo 1.77.2 (e52e36006 2024-03-26)
$ cargo new project
```
</details>

では、
ゆめと　ぼうけんと！
Rust CLIアプリ制作　のせかいへ！
レッツゴー！

# お手軽パーサー [clap](https://docs.rs/clap/latest/clap/)

https://docs.rs/clap/latest/clap/

[clap](https://docs.rs/clap/latest/clap/)はCLIアプリの要であろう、コマンドライン引数をパースするためのライブラリです！Command Line Argument Parser略してCLAP (多分)

```bash
cargo add clap --features derive
```

とコマンドを打ってクレートを追加し、構造体に少しだけ細工をするだけで一気にCLIアプリケーションもどきが作れる、それがclapです！ :clap: :clap_tone1: :clap_tone2: お手軽すぎる

:::note
`cargo add`での追加と`Cargo.toml`に直接追記する追加どちらでも変わらないため、次節以降は`Cargo.toml`ファイルを提示するに留めます
:::

## `--help` オプションと `--version` オプション

例えば、`clap::Parser`を構造体にderiveして`parse`メソッドを呼び出すだけでヘルプオプションが生えます。

```rust:main.rs
use clap::Parser;

#[derive(Parser)]
struct Args;

fn main() {
    let _ = Args::parse();
}
```

実行・出力例

```bash
$ cargo run -q -- --help
Usage: project

Options:
  -h, --help  Print help
```

:::note warn
`cargo run`による実行時、`cargo`ではなくプログラムへ引数を渡すには、間に`--`が必要です。なお、`cargo`コマンドに渡している`-q`は`cargo`コマンド自体の出力(コンパイル情報等)を抑制するものです。
:::

「一瞬でヘルプオプションが生えただと...？貴様何をした...？！」

「何って...トレイトをderiveしただけですが...また俺何かやっちゃいましたか...？ :sweat_smile: 」

`Cargo.toml` に書いた `description` をヘルプコマンドで表示することも可能です！ついでに `--version` オプションも生やしてみます。

```diff_toml:Cargo.toml
[package]
name = "project"
+ version = "3.3.4"
edition = "2021"
+ description = "clapクレートデモ用プロジェクト"

[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
```

```diff_rust:main.rs
use clap::Parser;

#[derive(Parser)]
+ #[command(version, about)]
struct Args;

fn main() {
    let _ = Args::parse();
}
```

実行・出力例

```bash
$ cargo run -q -- --help   
clapクレートデモ用プロジェクト

Usage: project

Options:
  -h, --help     Print help
  -V, --version  Print version
$ cargo run -q -- --version
project 3.3.4
```

何の機能もないアプリなのにこれだけでそれっぽいです！

## ポジショナル引数

コマンドライン引数を追加していき、さらにそれっぽくしてみます。

```rust:main.rs
use clap::Parser;

#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    /// ポジショナル引数
    quote: String,
}

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

    println!("{:?}", args);
}
```

実行・出力例

```bash
$ cargo run -q -- おひたし
Args { quote: "おひたし" }
```

下記どちらかの方法で省略可能にもできます。

- `#[arg(default_value = "~")]`や`#[arg(default_value_t = ~)]`によってデフォルト値を付与
- `Option`型にする

```rust:Rust
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    /// ポジショナル引数
    quote: String,

    /// 省略可能ポジショナル引数
    /// デフォルト値付き
    #[arg(default_value_t = 1)]
    repeat_num: usize,

    /// 省略可能ポジショナル引数
    /// Option型
    lines_num: Option<usize>,
}
```

<details><summary>実装コード全体例</summary>

```rust:main.rs
use clap::Parser;

#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    /// ポジショナル引数
    quote: String,

    /// 省略可能ポジショナル引数
    /// デフォルト値付き
    #[arg(default_value_t = 1)]
    repeat_num: usize,

    /// 省略可能ポジショナル引数
    /// Option型
    lines_num: Option<usize>,
}

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

    println!("{:?}", args);

    let Args {
        quote,
        repeat_num,
        lines_num,
    } = args;

    for _ in 0..lines_num.unwrap_or(1) {
        println!("{}", quote.repeat(repeat_num));
    }
}
```
</details>

:::note
`///`で始まる[ドキュメンテーションコメント](https://doc.rust-jp.rs/rust-by-example-ja/meta/doc.html)をフィールドにつけると、`--help`でコメントの内容を表示してくれます！
:::

実行・出力例

```bash
$ cargo run -q -- --help
clapクレートデモ用プロジェクト

Usage: project <QUOTE> [REPEAT_NUM] [LINES_NUM]

Arguments:
  <QUOTE>       ポジショナル引数
  [REPEAT_NUM]  省略可能ポジショナル引数 デフォルト値付き [default: 1]
  [LINES_NUM]   省略可能ポジショナル引数 Option型

Options:
  -h, --help     Print help
  -V, --version  Print version
$ cargo run -q -- おひたし 2
Args { quote: "おひたし", repeat_num: 2, lines_num: None }
おひたしおひたし
$ cargo run -q -- おひたし 3 2
Args { quote: "おひたし", repeat_num: 3, lines_num: Some(2) }
おひたしおひたしおひたし
おひたしおひたしおひたし
```

また`Vec`型を使用することで複数の値を受け取れるようにできます！

```rust:Rust
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    quotes: Vec<String>,
}
```

<details><summary>実装コード全体例</summary>

```rust:main.rs
use clap::Parser;

#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    quotes: Vec<String>,
}

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

    println!("{:?}", args);
}
```
</details>


実行・出力例

```bash
$ cargo run -q -- --help
clapクレートデモ用プロジェクト

Usage: project [QUOTES]...

Arguments:
  [QUOTES]...  

Options:
  -h, --help     Print help
  -V, --version  Print version
$ cargo run -q -- おひたし アジフライ とんかつ
Args { quotes: ["おひたし", "アジフライ", "とんかつ"] }
```

`Option`型や`Vec`型がコマンドライン引数の与えられ方の実態と一致しているのがとても良いですね、直感的でわかりやすいです！

## フラグ引数

さっきの例ではポジション引数であるがゆえに(順番を入れ替えない場合)複数引数とその他の引数の共存が不可能です...

フラグ引数にすることでこの問題は回避できます！フラグ引数にするには`#[arg(short)]`や`#[arg(long)]`を指定します。また、`long = "〇〇"`とすれば変数名とは異なる引数名を設定することも可能です。

```rust:Rust
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    /// 複数値ポジショナル引数
    quotes: Vec<String>,

    /// 省略不可能フラグ引数 -f
    #[arg(short)]
    flag: bool,

    /// 省略可能フラグ引数 -r, --repeat_num
    /// デフォルト値付き
    #[arg(short, long, default_value_t = 1)]
    repeat_num: usize,

    /// 省略可能フラグ引数 -l, --lines
    /// Option型
    #[arg(short, long = "lines")]
    lines_num: Option<usize>,
}
```

<details><summary>実装コード全体例</summary>

```rust:main.rs
use clap::Parser;

#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    /// 複数値ポジショナル引数
    quotes: Vec<String>,

    /// 省略不可能フラグ引数 -f
    #[arg(short)]
    flag: bool,

    /// 省略可能フラグ引数 -r, --repeat_num
    /// デフォルト値付き
    #[arg(short, long, default_value_t = 1)]
    repeat_num: usize,

    /// 省略可能フラグ引数 -l, --lines
    /// Option型
    #[arg(short, long = "lines")]
    lines_num: Option<usize>,
}

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

    println!("{:?}", args);

    let Args {
        quotes,
        flag: _,
        repeat_num,
        lines_num,
    } = args;

    for _ in 0..lines_num.unwrap_or(1) {
        for quote in quotes.iter() {
            print!("{} ", quote.repeat(repeat_num));
        }
        println!();
    }
}
```
</details>

実行・出力例

```bash
$ cargo run -q -- --help
clapクレートデモ用プロジェクト

Usage: project [OPTIONS] [QUOTES]...

Arguments:
  [QUOTES]...  複数値ポジショナル引数

Options:
  -f                             省略不可能フラグ引数 -f
  -r, --repeat-num <REPEAT_NUM>  省略可能フラグ引数 -r, --repeat_num デフォルト値付き [default: 1]
  -l, --lines <LINES_NUM>        省略可能フラグ引数 -l, --lines Option型
  -h, --help                     Print help
  -V, --version                  Print version
$ cargo run -q -- おひたし アジフライ とんかつ -r 2 --lines 3 -f
Args { quotes: ["おひたし", "アジフライ", "とんかつ"], flag: true, repeat_num: 2, lines_num: Some(3) }
おひたしおひたし アジフライアジフライ とんかつとんかつ 
おひたしおひたし アジフライアジフライ とんかつとんかつ 
おひたしおひたし アジフライアジフライ とんかつとんかつ 
```

複雑なフラグ引数も構造体で簡単に管理できています！

## バリデーション

バリデーション、すなわち引数が想定した値であるかの検証もお手軽に追加できます！

### 列挙型で規定値のみ許可

取りうる値も型にしたい...そんなお望み、叶えます！列挙型に`clap::ValueEnum`を実装することで、バリアントがそのまま候補値になります。また、実質的に文字列引数のバリデーションがかかることになります。

```rust:Rust
#[derive(ValueEnum, Debug, Clone)]
enum LogLevel {
    Error,
    Warn,
    Info,
    Debug,
}

#[derive(Parser, Debug)]
struct Args {
    #[arg(value_enum, short = 'L', long, default_value_t = LogLevel::Error)]
    log_level: LogLevel,
}
```

<details><summary>実装コード全体例</summary>

```rust:main.rs
use clap::{Parser, ValueEnum};

#[derive(ValueEnum, Debug, Clone)]
enum LogLevel {
    Error,
    Warn,
    Info,
    Debug,
}

#[derive(Parser, Debug)]
struct Args {
    #[arg(value_enum, short = 'L', long, default_value_t = LogLevel::Error)]
    log_level: LogLevel,
}

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

    println!("{:?}", args);
}
```
</details>

実行・出力例

```bash
$ cargo run -q -- --help
Usage: project [OPTIONS]

Options:
  -L, --log-level <LOG_LEVEL>  [default: error] [possible values: error, warn, info, debug]
  -h, --help                   Print help
$ cargo run -q -- -L debug
Args { log_level: Debug }
$ cargo run -q -- -L critical
error: invalid value 'critical' for '--log-level <LOG_LEVEL>'
  [possible values: error, warn, info, debug]

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

:::note warn
コマンドライン引数指定側の属性に `value_enum` を付け忘れないよう注意しましょう！

```rust:Rust
#[arg(
    value_enum, // <- 忘れがち
    default_value_t = LogLevel::Error
)]
log_level: LogLevel,
```
:::

### カスタムバリデーション

列挙型にできないような値のバリデーションも属性風マクロ `value_parser` を介して指定することが可能です！clap自体が用意しているパーサーの他、トレイト`Fn(&str) -> Result<T, E>`が実装されている関数やクロージャを指定することができます。以下の例では`Regex::new`がちょうど適していたためそのまま渡しています。

```rust:Rust
#[derive(Parser, Debug)]
struct Args {
    #[arg(
        short, long, default_value_t = 1,
        value_parser = clap::value_parser!(u64).range(1..))
    ]
    natural_number: u64,

    #[arg(short, long, value_parser = Regex::new)]
    regex: Option<Regex>,
}
```

<details><summary>実装コード全体例</summary>

```rust:main.rs
use clap::Parser;
use regex::Regex;

#[derive(Parser, Debug)]
struct Args {
    #[arg(
        short, long, default_value_t = 1,
        value_parser = clap::value_parser!(u64).range(1..))
    ]
    natural_number: u64,

    #[arg(short, long, value_parser = Regex::new)]
    regex: Option<Regex>,
}

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

    println!("{:?}", args);
}
```
</details>

実行・出力例

```bash
$ cargo run -q -- --help
Usage: project [OPTIONS]

Options:
  -n, --natural-number <NATURAL_NUMBER>  [default: 1]
  -r, --regex <REGEX>                    
  -h, --help                             Print help
$ cargo run -q -- -n 100 -r "^(.*)$"
Args { natural_number: 100, regex: Some(Regex("^(.*)$")) }
$ cargo run -q -- -n 0
error: invalid value '0' for '--natural-number <NATURAL_NUMBER>': 0 is not in 1..18446744073709551615

For more information, try '--help'.
$ cargo run -q -- -r "^(.*$"
error: invalid value '^(.*$' for '--regex <REGEX>': regex parse error:
    ^(.*$
     ^
error: unclosed group

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

## おまけ 好きな惣菜発表ドラゴン

<script src="https://asciinema.org/a/pz8Ez8rAjbgAJc9JYpQW3aOqg.js" id="asciicast-pz8Ez8rAjbgAJc9JYpQW3aOqg" async="true"></script>

<details><summary>好きな総菜発表ドラゴン ソースコード全体</summary>

リポジトリ: https://github.com/anotherhollow1125/fav_say_dragon

お借りした素材

- [好きな惣菜発表ドラゴン](https://dic.pixiv.net/a/%E5%A5%BD%E3%81%8D%E3%81%AA%E6%83%A3%E8%8F%9C%E7%99%BA%E8%A1%A8%E3%83%89%E3%83%A9%E3%82%B4%E3%83%B3)のAA
    - https://twitter.com/mukiryokup/status/1772930362433253380
    - https://open.open2ch.net/test/read.cgi/kancolle/1449477234/386

```toml:Cargo.toml
[package]
name = "fav_say_dragon"
version = "0.1.0"
edition = "2021"
description = "好きな総菜発表ドラゴンsay"

[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
console = "0.15.8"
anyhow = "1.0.83"
toml = "0.8.12"
serde = { version = "1.0.200", features = ["derive"] }
```

```rust:main.rs
use anyhow::Result;
use clap::{Parser, Subcommand};
use console::{Alignment, Term};
use serde::Deserialize;
use std::path::{Path, PathBuf};
use std::thread::sleep;
use std::time::Duration;

#[derive(Parser)]
#[command(version, about, flatten_help = true)]
struct Args {
    #[command(subcommand)]
    sub: Command,
}

#[derive(Subcommand, Clone, Debug)]
enum Command {
    /// 一度に出力
    Say {
        /// おかず
        side_dish: String,
        /// キャプション
        caption: Option<String>,
    },
    /// アニメーション出力
    Anime {
        /// おかず
        side_dishes: Vec<String>,

        /// プレキャプション
        #[arg(short, long)]
        pre_captions: Vec<String>,

        /// アフターキャプション
        #[arg(short = 'A', long)]
        after_captions: Vec<String>,

        /// ファイルからスクリプトを読み込む
        #[arg(short = 'f', long,
            conflicts_with_all(["side_dishes", "pre_captions", "after_captions"]))
        ]
        script_file: Option<PathBuf>,

        /// インターバル (ms)
        #[arg(
            short, long,
            default_value_t = 1000,
            value_parser = clap::value_parser!(u64).range(10..))
        ]
        interval: u64,
    },
}

#[derive(Deserialize, Debug)]
struct Script {
    side_dishes: Vec<String>,
    pre_captions: Vec<String>,
    after_captions: Vec<String>,
}

impl Script {
    fn load(path: &Path) -> Result<Self> {
        let script = std::fs::read_to_string(path)?;
        Ok(toml::from_str(&script)?)
    }
}

fn main() -> Result<()> {
    match Args::parse().sub {
        Command::Say { side_dish, caption } => say(&side_dish, caption.as_deref())?,
        Command::Anime {
            side_dishes,
            pre_captions,
            after_captions,
            script_file,
            interval,
        } => anime(
            side_dishes,
            pre_captions,
            after_captions,
            script_file,
            interval,
        )?,
    }

    Ok(())
}

fn say(side_dish: &str, caption: Option<&str>) -> Result<()> {
    let term = Term::stdout();
    let terminal_width = term.size().1 as usize;

    let dragon = create_dragon(side_dish, terminal_width);
    for line in dragon {
        term.write_line(&line)?;
    }
    let caption = console::pad_str(caption.unwrap_or(""), 60, Alignment::Center, None);
    term.write_line(&caption)?;

    Ok(())
}

fn anime(
    side_dishes: Vec<String>,
    pre_captions: Vec<String>,
    after_captions: Vec<String>,
    script_file: Option<PathBuf>,
    interval: u64,
) -> Result<()> {
    let (side_dishes, pre_captions, after_captions) = match script_file {
        Some(path) => {
            let Script {
                side_dishes,
                pre_captions,
                after_captions,
            } = Script::load(&path)?;
            (side_dishes, pre_captions, after_captions)
        }
        None => (side_dishes, pre_captions, after_captions),
    };

    let term = Term::stdout();
    let terminal_width = term.size().1 as usize;
    term.clear_screen()?;
    let empty_dragon = create_dragon("", terminal_width);
    let mut printed_flag = false;

    let mut pre_captions_iter = pre_captions.into_iter().peekable();
    while let Some(pre_caption) = pre_captions_iter.next() {
        for line in empty_dragon.iter() {
            term.write_line(line)?;
        }
        let pre_caption = console::pad_str(&pre_caption, 60, Alignment::Center, None);
        term.write_line(&pre_caption)?;
        printed_flag = true;

        if pre_captions_iter.peek().is_some() {
            clear_dragon(interval, &term, &mut printed_flag)?;
        }
    }

    let mut side_dish_iter = side_dishes.iter().peekable();

    if printed_flag && side_dish_iter.peek().is_some() {
        clear_dragon(interval, &term, &mut printed_flag)?;
    }

    while let Some(side_dish) = side_dish_iter.next() {
        let dragon = create_dragon(side_dish, terminal_width);
        for line in dragon {
            term.write_line(&line)?;
        }
        let empty_line = console::pad_str("", 60, Alignment::Center, None);
        term.write_line(&empty_line)?;
        printed_flag = true;

        if side_dish_iter.peek().is_some() {
            clear_dragon(interval, &term, &mut printed_flag)?;
        }
    }

    let mut after_captions_iter = after_captions.into_iter().peekable();

    if printed_flag && after_captions_iter.peek().is_some() {
        clear_dragon(interval, &term, &mut printed_flag)?;
    }

    while let Some(after_caption) = after_captions_iter.next() {
        for line in empty_dragon.iter() {
            term.write_line(line)?;
        }
        let after_caption = console::pad_str(&after_caption, 60, Alignment::Center, None);
        term.write_line(&after_caption)?;

        if after_captions_iter.peek().is_some() {
            clear_dragon(interval, &term, &mut printed_flag)?;
        }
    }

    Ok(())
}

fn clear_dragon(interval: u64, term: &Term, printed_flag: &mut bool) -> Result<()> {
    sleep(Duration::from_millis(interval));
    term.clear_screen()?;
    *printed_flag = false;

    Ok(())
}

fn create_dragon(side_dish: &str, terminal_width: usize) -> Vec<String> {
    let lines: Vec<String> = match side_dish.lines().count() {
        0 => vec!["".to_string(), "".to_string()],
        1 => {
            let empty = Vec::new();
            let side_dish: Vec<char> = side_dish.chars().collect();
            match side_dish.len() {
                0..=16 => vec![side_dish.as_slice(), empty.as_slice()],
                17..=32 => vec![&side_dish[..16], &side_dish[16..]],
                _ => vec![&side_dish[..16], &side_dish[16..32]],
            }
            .into_iter()
            .map(|s| s.iter().collect::<String>())
            .collect()
        }
        _ => {
            let mut lines: Vec<String> = side_dish.lines().rev().map(|s| s.to_string()).collect();
            let line0 = lines.pop().unwrap_or("".to_string());
            let line1 = lines.pop().unwrap_or("".to_string());
            vec![line0, line1]
        }
    }
    .into_iter()
    .map(|s| console::pad_str(&s, 20, Alignment::Center, None).to_string())
    .collect();

    #[rustfmt::skip]
    let dragon = "                                          ,. ､
                                        く  r',ゝ
r'￣￣￣￣￣￣￣￣￣ヽ                   ,ゝｰ'､
|                    |          ､      ／      ヽ.
|                    |        く、｀ヽ/  ∩       |
|$line1$ ＞        ｀＞             |
|$line2$|         く´ , -'7         レ个ー─┐
|                    |          ｀´   //  /      ー个ー─'7
|                    |               //  /         |    (
ゝ＿＿＿＿＿＿＿＿__ノ              //  /'┤      |ヽv'⌒ヽ､ゝ
                                   くﾉ  lｰ┤       ヽ.
                                    ｀^^'ｰ┤          ▽_
                                    ((    )          ヽ乙_
                                    ((    )ヽ､          ヽレl
                                    ≧＿_ゝ    ｀ﾞー-=､.＿_,ゝ";

    dragon
        .replace("$line1$", &lines[0])
        .replace("$line2$", &lines[1])
        .lines()
        .map(|line| console::pad_str(line, terminal_width, Alignment::Left, None).to_string())
        .collect()
}
```

```toml:commands.toml
pre_captions = ["好きなコマンド発表ドラゴンが", "好きなコマンドを発表します"]

side_dishes = ["jq", "grep", "cat", "python3\n-m http.server"]

after_captions = ["なんの略称かわからないコマンドも", "好き　好き　大好き"]
```
</details>

以下、発表ドラゴンのコマンドライン引数部分を抜き出したものになります。

```rust:Rust
#[derive(Parser)]
#[command(version, about, flatten_help = true)]
struct Args {
    #[command(subcommand)]
    sub: Command,
}

#[derive(Subcommand, Clone, Debug)]
enum Command {
    /// 一度に出力
    Say {
        /// おかず
        side_dish: String,
        /// キャプション
        caption: Option<String>,
    },
    /// アニメーション出力
    Anime {
        /// おかず
        side_dishes: Vec<String>,

        /// プレキャプション
        #[arg(short, long)]
        pre_captions: Vec<String>,

        /// アフターキャプション
        #[arg(short = 'A', long)]
        after_captions: Vec<String>,

        /// ファイルからスクリプトを読み込む
        #[arg(short = 'f', long,
            conflicts_with_all(["side_dishes", "pre_captions", "after_captions"]))
        ]
        script_file: Option<PathBuf>,

        /// インターバル (ms)
        #[arg(
            short, long,
            default_value_t = 1000,
            value_parser = clap::value_parser!(u64).range(10..))
        ]
        interval: u64,
    },
}
```

`script_file`オプションがお気に入りです！`String`型としてではなくて受け取る構造体の時点で`PathBuf`型として記述することができており、`#[derive(Parser)]`を利用しているメリットを十分に感じられてスッキリします！


## clapまとめ

clapを使うことでそれっぽいCLIアプリもどきをたくさん量産できました :crab: :crab: :crab: 

clapにはサブコマンドや引数をまとめる機能など、本記事で紹介していない機能がたくさんあります！また、本記事で紹介したのは宣言的にコマンドライン引数の設定を記述できるderiveによる方法でしたが、もう少し柔軟に(例えば、ランタイム時などに)設定を行えるbuilderによる方法もあります。

かなり詳細なチュートリアルがあるので、気になった方は公式ドキュメントの方を参照してみてください。

https://docs.rs/clap/latest/clap/_derive/index.html

https://docs.rs/clap/latest/clap/_tutorial/chapter_0/index.html

本記事で紹介したかった内容の9割はclapでしたので以降は少し軽めになっていきますが、dialoguer, indicatif, consoleも見ていきます :eyes:

# インタラクティブ！ [dialoguer](https://docs.rs/dialoguer/latest/dialoguer/)

https://docs.rs/dialoguer/latest/dialoguer/

[dialoguer](https://docs.rs/dialoguer/latest/dialoguer/)は、対話的にユーザーからの入力を受け取る際に活躍するクレートです！

大体のCLIアプリはコマンドライン引数さえ受け取れれば後は出力のみという場合が多いかと思いますが、それで事足りずユーザーと対話する必要が出てきた際はこのdialoguerクレートの出番というわけです。

本記事では選択肢を提示してユーザーに選択してもらう[Select](https://docs.rs/dialoguer/latest/dialoguer/struct.Select.html)と、ユーザーからの標準入力を受け取る[Input](https://docs.rs/dialoguer/latest/dialoguer/struct.Input.html)を紹介します。

## [Select](https://docs.rs/dialoguer/latest/dialoguer/struct.Select.html)

https://docs.rs/dialoguer/latest/dialoguer/struct.Select.html

文字列スライスを `items` メソッドで渡したのち、その選択肢を `interact` メソッドにて表示します。ユーザーからの選択は文字列ではなく配列のインデックスとして受け取ります。

```rust:Rust
let choices = &["はい", "いいえ"];
let choice: usize = Select::new() // 選択したインデックスが返る
    .items(choices)
    .default(0)
    .interact()?;
```

<script src="https://asciinema.org/a/659000.js" id="asciicast-659000" async="true"></script>

<details><summary>ソースコード全体</summary>

```toml:Cargo.toml
[package]
name = "world_half"
version = "0.1.0"
edition = "2021"

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

[dependencies]
dialoguer = "0.11.0"
```

```rust:main.rs
use dialoguer::Select;

fn main() {
    println!(
        "もし、わしの味方になれば世界の半分を勇者にやろう。
どうじゃ？　わしの味方になるか？"
    );

    loop {
        let choices = &["はい", "いいえ"];
        let choice = Select::new().items(choices).default(0).interact().unwrap();

        match choice {
            0 => {
                println!(
                    "
では世界の半分、闇の世界を与えよう！
お前の旅は終わった。さぁ、ゆっくり休むがよい！
わあっはっはっはっ。
$ rm -rf /"
                );
                break;
            }
            _ => {
                println!("まぁまぁそう言わず、わしの味方になってくれまいか...？");
            }
        }
    }
}
```
</details>

列挙型等を渡せないのは残念ですが、シンプルに選択プロンプトを出せる良い機能です！

## [Input](https://docs.rs/dialoguer/latest/dialoguer/struct.Input.html)

https://docs.rs/dialoguer/latest/dialoguer/struct.Input.html

標準入力から文字列を受け取れるシンプルな機能になります！

```rust:Rust
let input: String = Input::new()
    .with_prompt("You")
    .interact_text()?;
```

<script src="https://asciinema.org/a/659182.js" id="asciicast-659182" async="true"></script>

<details><summary>ソースコード全体</summary>

リポジトリ: https://github.com/anotherhollow1125/chatgpt_demo

```toml:Cargo.toml
[package]
name = "chatgpt_demo"
version = "0.1.0"
edition = "2021"

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

[dependencies]
anyhow = "1.0.83"
dialoguer = "0.11.0"
dotenvy = "0.15.7"
reqwest = { version = "0.12.4", features = ["json"] }
serde = { version = "1.0.201", features = ["derive"] }
serde_json = "1.0.117"
tokio = { version = "1.37.0", features = ["full"] }
```

```rust:main.rs
use anyhow::Result;
use dialoguer::Input;
use reqwest::{Client, RequestBuilder};

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum Role {
    System,
    User,
    Assistant,
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
struct Message {
    role: Role,
    content: String,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct RequestBody {
    model: String,
    messages: Vec<Message>,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct Choice {
    index: u64,
    message: Message,
    finish_reason: String,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct Usage {
    prompt_tokens: u64,
    completion_tokens: u64,
    total_tokens: u64,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct ResponseBody {
    id: String,
    object: String,
    created: u64,
    choices: Vec<Choice>,
    usage: Usage,
}

fn common_header(api_key: &str) -> RequestBuilder {
    let api_key_field = format!("Bearer {}", api_key);

    Client::new()
        .post("https://api.openai.com/v1/chat/completions")
        .header("Content-Type", "application/json")
        .header("Authorization", api_key_field.as_str())
}

async fn query(api_key: &str, input_messages: &[Message]) -> Result<Message> {
    let mut response_body = common_header(api_key)
        .json(&RequestBody {
            model: "gpt-4-turbo".to_string(),
            messages: Vec::from(input_messages),
        })
        .send()
        .await?
        .json::<ResponseBody>()
        .await?;

    let res = response_body.choices.remove(0).message;
    Ok(res)
}

#[tokio::main]
async fn main() -> Result<()> {
    dotenvy::dotenv().ok();

    let api_key = std::env::var("CHATGPT_APIKEY")?;

    let mut messages = vec![Message {
        role: Role::System,
        content: "You are a helpful assistant.".to_string(),
    }];

    loop {
        let input = Input::new()
            .with_prompt("You")
            .interact_text()
            .unwrap_or_else(|_| "quit".to_string());

        if input == "quit" {
            break;
        }

        messages.push(Message {
            role: Role::User,
            content: input,
        });

        let response = query(&api_key, &messages).await?;

        println!("ChatGPT: {}", response.content);

        messages.push(response);
    }

    Ok(())
}
```

ChatGPT APIを叩くCLIアプリ例

:::note
記事とは関係ないですが、なんと記事を書いている途中で新しいモデルの `gpt-4o` が使えるようになりました...！同じコードでそのまま該当部分を差し替えるだけでおｋです！
:::

ほとんどクエリのための構造体の定義で行数を持っていかれていますが、入出力部分はシンプルな作りになっています。
</details>

ちなみにライブラリを使わずに「プロンプトを出して改行せずに標準入力を受け取る」のを実現するには、`print!`マクロと`std::io::Write::flush`を使う必要があり不格好です...

https://qiita.com/V_lasergun/items/bb2483f3ce6bd377c863

<details><summary> dialoguer::Input を使わない入力</summary>

```rust:main.rs
use std::io::{stdout, Write};

fn main() {
    let mut out = stdout();
    print!("You: ");
    out.flush().unwrap();

    let mut input = String::new();
    std::io::stdin().read_line(&mut input).unwrap();

    let input = input.trim();

    println!("Echo: {}", input);
}
```
</details>

Inputならユーザーによる標準入力処理をスッキリ書けるというわけです！

## その他

その他にも[Confirm](https://docs.rs/dialoguer/latest/dialoguer/struct.Confirm.html), [FuzzySelect](https://docs.rs/dialoguer/latest/dialoguer/struct.FuzzySelect.html), [MultiSelect](https://docs.rs/dialoguer/latest/dialoguer/struct.MultiSelect.html), [Sort](https://docs.rs/dialoguer/latest/dialoguer/struct.Sort.html), [Password](https://docs.rs/dialoguer/latest/dialoguer/struct.Password.html), Input with [History](https://docs.rs/dialoguer/latest/dialoguer/trait.History.html), [Editor](https://docs.rs/dialoguer/latest/dialoguer/struct.Editor.html)といった便利な構造体が用意されていますが、全部説明していくのは大変なため、一つのサンプルコードにまとめてみました！

<script src="https://asciinema.org/a/659062.js" id="asciicast-659062" async="true"></script>

<details><summary>ソースコード</summary>

リポジトリ: https://github.com/anotherhollow1125/diag_all_in_one

```toml:Cargo.toml
[package]
name = "diag_all_in_one"
version = "0.1.0"
edition = "2021"

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

[dependencies]
anyhow = "1.0.83"
console = "0.15.8"
dialoguer = { version = "0.11.0", features = ["history", "fuzzy-select"] }
```

```rust:main.rs
use anyhow::Result;
use console::Term;
use dialoguer::{BasicHistory, Confirm, Editor, FuzzySelect, Input, MultiSelect, Password, Sort};

fn confirm_demo() -> Result<()> {
    println!(
        "
【Confirm】

[y/n] 形式で質問する
"
    );

    let confirmation = Confirm::new()
        .with_prompt("縮小用廃棄物投入プロトコルを実施しますか?")
        .show_default(true)
        .wait_for_newline(true)
        .interact()?;

    if confirmation {
        println!("プロトコルを開始しました");
    } else {
        println!("中止しました");
    }

    Ok(())
}

fn fuzzyselect_demo() -> Result<()> {
    println!(
        "
【FuzzySelect】

入力による選択で、徐々に絞り込まれていく
"
    );

    let items = ["Python", "R", "Rust", "Ruby", "Ruby on Rails"];

    let selection = FuzzySelect::new()
        .with_prompt("What do you use?")
        .items(&items)
        .interact()?;

    println!("You chose: {}", items[selection]);

    Ok(())
}

fn multiselect_demo() -> Result<()> {
    println!(
        "
【MultiSelect】

チェックボックスによる複数選択ができる
"
    );

    let items = [
        "Basic",
        "Extension for VSCode",
        "Extension for Rust",
        "Utility for Git",
    ];

    let selections = MultiSelect::new()
        .with_prompt("インストールする項目を選んでください (スペースで選択/解除)")
        .item_checked(items[0], true)
        .items(&items[1..])
        .interact()?;

    println!("以下をインストールします:");
    for selection in selections {
        println!("{}", items[selection]);
    }

    Ok(())
}

fn sort_demo() -> Result<()> {
    println!(
        "
【Sort】

ユーザーによりソートされたインデックスを返す
"
    );

    let items = vec!["SSD-1", "HDD-1", "HDD-2", "USB", "DVD"];

    let order = Sort::new()
        .with_prompt("Boot Device Priority (Space key: select, Arrow keys: move)")
        .items(&items)
        .interact()?;

    let mut new_priority: Vec<_> = order
        .into_iter()
        .enumerate()
        .map(|(priority, i)| (priority, items[i]))
        .collect();

    new_priority.sort_by_key(|(priority, _)| *priority);

    let boot_devices = new_priority
        .iter()
        .map(|(_, item)| item)
        .collect::<Vec<_>>();

    println!("Sorted Vector: {:?}", boot_devices);

    Ok(())
}

fn password_demo() -> Result<()> {
    println!(
        "
【Password】

Inputと同様に入力を受け付けるが、入力内容を表示しない
"
    );

    let password = Password::new()
        .with_prompt("Enter your password")
        .interact()?;

    let password_confirm = Password::new()
        .with_prompt("Confirm your password")
        .interact()?;

    if password == password_confirm {
        println!("Password is confirmed");
    } else {
        println!("Password is not confirmed");
    }

    Ok(())
}

fn history_demo() -> Result<()> {
    println!(
        "
【History Trait (BasicHistory)】

過去の入力を履歴として保存する。
矢印キーにより過去の入力を呼び出せる
"
    );

    let mut history = BasicHistory::new().no_duplicates(true);

    loop {
        let input: String = Input::new()
            .with_prompt("Enter something")
            .history_with(&mut history)
            .interact_text()?;

        if input == "exit" {
            break;
        }
    }

    Ok(())
}

fn editor_demo() -> Result<()> {
    let content = Editor::new().edit(
        "
【Editor】

Vim, Emacsなどのデフォルトエディタを開き、入力を受け付ける
",
    )?;

    println!(
        "Edited content:
{}
",
        content.unwrap_or("None".to_string())
    );

    Ok(())
}

fn main() -> Result<()> {
    let term = Term::stdout();
    term.clear_screen()?;

    confirm_demo()?;
    let _ = term.read_key()?;
    term.clear_screen()?;

    fuzzyselect_demo()?;
    let _ = term.read_key()?;
    term.clear_screen()?;

    multiselect_demo()?;
    let _ = term.read_key()?;
    term.clear_screen()?;

    sort_demo()?;
    let _ = term.read_key()?;
    term.clear_screen()?;

    password_demo()?;
    let _ = term.read_key()?;
    term.clear_screen()?;

    history_demo()?;
    let _ = term.read_key()?;
    term.clear_screen()?;

    editor_demo()?;

    Ok(())
}
```
</details>

|構造体名|概要|
|:------|:---|
| [Confirm](https://docs.rs/dialoguer/latest/dialoguer/struct.Confirm.html) | [y/n] 形式で質問する |
| [FuzzySelect](https://docs.rs/dialoguer/latest/dialoguer/struct.FuzzySelect.html) | 入力による選択で、徐々に絞り込まれていく |
| [MultiSelect](https://docs.rs/dialoguer/latest/dialoguer/struct.MultiSelect.html) | チェックボックスによる複数選択ができる |
| [Sort](https://docs.rs/dialoguer/latest/dialoguer/struct.Sort.html) | ユーザーによりソートされたインデックスを返す |
| [Password](https://docs.rs/dialoguer/latest/dialoguer/struct.Password.html) | Inputと同様に入力を受け付けるが、入力内容を表示しない |
| [BaisicHistory](https://docs.rs/dialoguer/latest/dialoguer/struct.BasicHistory.html) | 過去の入力を履歴として保存する |
| [Editor](https://docs.rs/dialoguer/latest/dialoguer/struct.Editor.html) | Vim, Emacsなどのデフォルトエディタを開き、入力を受け付ける |

## dialoguer まとめ

dialoguerを使うことで、ライブラリなしだと多少煩雑なユーザーからの標準入力を扱えるのを示せました。

対話型CLIアプリもこれで安心して作れます！

# 進捗どうですか？ [indicatif](https://docs.rs/indicatif/latest/indicatif/)

https://docs.rs/indicatif/latest/indicatif/

重い処理や、複数のタスクからなる処理を実行する際、実行を開始した後になって「ちゃんとプログラムが動いていることを確かめる機構を付けておくべきだった...」と後悔した経験は誰しもあるのではないでしょうか？

タスクバーの表示というのはまさにそのためにあるものでしょう。というわけで、プログレスバーをお手軽に表示できる [indicatif](https://docs.rs/indicatif/latest/indicatif/) クレートの紹介です！

<script src="https://asciinema.org/a/659211.js" id="asciicast-659211" async="true"></script>

<details><summary>ソースコード</summary>

リポジトリ: https://github.com/anotherhollow1125/almost_done

```toml:Cargo.toml
[package]
name = "almost_done"
version = "0.1.0"
edition = "2021"

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

[dependencies]
indicatif = "0.17.8"
```

```rust:main.rs
use std::cmp::min;
use std::thread;
use std::time::Duration;

use indicatif::{MultiProgress, ProgressBar, ProgressStyle};

fn pure_pb(
    m: &mut MultiProgress,
    template: &str,
    msg: &'static str,
    total_size: u64,
    speed: u64,
    prog_char: Option<&str>,
) -> thread::JoinHandle<()> {
    let pb = m.add(ProgressBar::new(total_size));
    pb.set_style(
        ProgressStyle::with_template(template)
            .unwrap()
            .progress_chars(prog_char.unwrap_or("#>-")),
    );
    pb.set_message(msg);

    thread::spawn(move || {
        let mut downloaded = 0;

        while downloaded < total_size {
            let new = min(downloaded + speed, total_size);
            downloaded = new;
            pb.set_position(new);
            thread::sleep(Duration::from_millis(12));
        }

        pb.finish_with_message("downloaded");
    })
}

fn stop_pb(
    m: &mut MultiProgress,
    template: &str,
    msg: &'static str,
    total_size: u64,
    speed: u64,
    slow_speed: u64,
    prog_char: Option<&str>,
) -> thread::JoinHandle<()> {
    let pb = m.add(ProgressBar::new(total_size));
    pb.set_style(
        ProgressStyle::with_template(template)
            .unwrap()
            .progress_chars(prog_char.unwrap_or("#>-")),
    );
    pb.set_message(msg);

    thread::spawn(move || {
        let mut downloaded = 0;
        let mut s = speed;
        let mut wait;

        while downloaded < total_size {
            let new = min(downloaded + s, total_size);
            downloaded = new;
            pb.set_position(new);
            if total_size - downloaded >= speed * 3 {
                s = speed;
                wait = 12;
            } else {
                s = slow_speed;
                wait = 1200;
            };

            thread::sleep(Duration::from_millis(wait));
        }

        pb.finish_with_message("downloaded");
    })
}

fn main() {
    let mut m = MultiProgress::new();
    let template = "{spinner:.green} [{elapsed_precise}] {msg} {percent:>3}% [{bar:20.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})";
    let h1 = pure_pb(&mut m, template, "contents 1", 1000, 1, None);
    thread::sleep(Duration::from_millis(500));
    let h2 = pure_pb(&mut m, template, "contents 2", 1000, 10, Some("|:_"));
    thread::sleep(Duration::from_millis(500));
    let h3 = stop_pb(&mut m, template, "contents 3", 1000000, 5000, 1, None);

    h1.join().unwrap();
    h2.join().unwrap();
    h3.join().unwrap();
}
```
</details>

進捗は主に以下の方法で管理できます。

- `inc` メソッド等を呼び更新する方法
- イテレータをラップして要素処理ごとに更新される方法

なお、マルチスレッド処理が考慮されており内部的には [Arc](https://doc.rust-lang.org/std/sync/struct.Arc.html) が使われているようです。もとい上記例でもマルチスレッドであるように、プログレスバーを表示するようなアプリはどれもマルチスレッドにしがちでしょうから、当然の仕様なのかもしれません。

## `inc` メソッドによる方法

```rust:Rust
let bar = ProgressBar::new(1000);
for _ in 0..1000 {
    bar.inc(1);
    sleep(Duration::from_millis(1));
}
bar.finish_and_clear();
```

進捗のたびに `inc` メソッドを呼び出しインクリメントする方法です。

## イテレータによる方法

`indicatif::ProgressIterator`トレイトを読み込むことにより、イテレータに `progress` メソッドを生やす方法です。イテレートのたびにインクリメントされます。

for文で読み込むも良し、`map` や `for_each` を呼び出す前に挟んでも良しです！

```rust:Rust
use indicatif::ProgressIterator;

// iter progress bar 1
for _ in (0..1000).progress() {
    sleep(Duration::from_millis(1));
}

// iter progress bar 2
(0..1000).progress().for_each(|_| {
    sleep(Duration::from_millis(1));
});
```

上記で作られるプログレスバーは `inc` メソッドによるものと同じになります。

<details><summary>ソースコード全体</summary>

```toml:Cargo.toml
[package]
name = "indicatif_pg"
version = "0.1.0"
edition = "2021"

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

[dependencies]
indicatif = "0.17.8"
```

```rust:main.rs
use indicatif::ProgressBar;
use indicatif::ProgressIterator;
use std::thread::sleep;
use std::time::Duration;

fn main() {
    // normal progress bar
    let bar = ProgressBar::new(1000);
    for _ in 0..1000 {
        bar.inc(1);
        sleep(Duration::from_millis(1));
    }
    bar.finish_and_clear();

    // iter progress bar 1
    for _ in (0..1000).progress() {
        sleep(Duration::from_millis(1));
    }

    // iter progress bar 2
    (0..1000).progress().for_each(|_| {
        sleep(Duration::from_millis(1));
    });
}
```
</details>

## プログレスバーのフォーマット

プログレスバーとして表示する情報は細かく設定することが可能です。本記事では詳細には触れず軽く紹介します！

[indicatif::style::ProgressStyle::with_template](https://docs.rs/indicatif/latest/indicatif/style/struct.ProgressStyle.html#method.with_template) にて表示する情報のテンプレート指定、[progress_chars](https://docs.rs/indicatif/latest/indicatif/style/struct.ProgressStyle.html#method.progress_chars) にてプログレスバーの完了/途中/未着手部分の表示文字の変更ができます(下記例では `.progress_chars("#>-")` )。

テンプレートについては https://docs.rs/indicatif/latest/indicatif/#templates に詳細が書いています。本節最初に示した例のプログレスバーは次のようなフォーマットになっています。

```plaintext
{spinner:.green} [{elapsed_precise}] {msg} {percent:>3}% [{bar:20.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})
```

```plaintext:実際の表示
⠐ [00:00:05] contents 1  48% [#########>----------] 485 B/1000 B (82 B/s, 6s)
```

`{spinner:.green}` がわかりやすいですが、 `{項目:スタイル}` という形になっています。スタイルの方は桁数、アライメント(右揃え左揃え)やカラーを指定できます。項目については下記に示すようなものがあります(一部のみ示しています)。

| 項目 | 概要 |
|:-----|:----|
| spinner | 処理が止まらずに動いていることを示すためのぐるぐる |
| elapsed / elapsed_precise | 経過時間 |
| msg / wide_msg | メッセージ。wide_*の方は指定するとターミナル幅いっぱいに取る |
| percent / percent_precise | 達成割合 |
| bar / wide_bar | プログレスバー本体。msg同様wide_は幅いっぱいに取る |
| bytes | 経過バイト |
| bytes_per_sec | 現在の処理速度 |
| eta | 残り時間 |

## indicatif まとめ

ただプログレスバーを示してくれるだけではなく、経過時間や処理速度等までお手軽に示せるindicatifは、重たい処理を持つアプリの味方です。

途中の処理状態が見えるか見えないかはUXに大きく関わる部分ですから、使えそうなシーンがあれば積極的に使いたいクレートでした！

# ターミナル色付け職人 [console](https://docs.rs/console/latest/console/)

https://docs.rs/console/latest/console/

dialoguerも一部そうでしたが、特にindicatifが提供してくれるプログレスバーなどは、キャリッジリターンや[ANSIエスケープ](https://qiita.com/PruneMazui/items/8a023347772620025ad6)を用いて表示する必要があり、これを直接文字列に含めて操作を行うのは至難の業です。だからこそdialoguerやindicatifの存在がありがたいのでした。

この[console](https://docs.rs/console/latest/console/)は、そんなANSIエスケープをコードベースで行えるようにしてくれるクレートです！具体的には、ターミナル上のカーソル移動(ないし文字列削除・上書き)、色付き文字の出力ができます。それ以外にも、絵文字の安全なサポート、ユニコード文字列幅を考慮したパディング機能を備え、実装者のわがままを最後まで叶えてくれます！

```rust:Rust
let term = Term::stdout();

term.clear_screen()?;

let hands = [
    format!("{} {}", Emoji("🔥", "F"), style("fire").red()),
    format!("{} {}", Emoji("💧", "W"), style("water").cyan()),
    format!("{} {}", Emoji("🌿", "L"), style("leaf").green()),
];
```

<script src="https://asciinema.org/a/659207.js" id="asciicast-659207" async="true"></script>

<details><summary>ソースコード全体</summary>

```toml:Cargo.toml
[package]
name = "janken"
version = "0.1.0"
edition = "2021"

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

[dependencies]
console = "0.15.8"
dialoguer = "0.11.0"
rand = "0.8.5"
anyhow = "1.0.83"
```

```rust:main.rs
use anyhow::Result;
use console::{style, Emoji, Term};
use dialoguer::Select;
use rand::prelude::*;

fn main() -> Result<()> {
    let term = Term::stdout();

    term.clear_screen()?;

    let hands = [
        format!("{} {}", Emoji("🔥", "F"), style("fire").red()),
        format!("{} {}", Emoji("💧", "W"), style("water").cyan()),
        format!("{} {}", Emoji("🌿", "L"), style("leaf").green()),
    ];

    let mut rng = rand::thread_rng();
    let computer_choice = rng.gen_range(0..3);

    let selection = Select::new()
        .with_prompt("Choose your hand")
        .items(&hands)
        .default(0)
        .interact()?;

    println!("  Computer chose: {}", hands[computer_choice]);
    println!(
        "{}",
        match (selection + 3 - computer_choice) % 3 {
            0 => "It's a draw!",
            1 => "You win!",
            _ => "You lose...",
        }
    );

    Ok(())
}
```
</details>

ここからはconsoleクレートのおすすめ機能を紹介します。ソースコードは上記例と重複する部分が多いので省略します。適宜上記例を見ていただければと思います。

## カーソル操作・文字の削除

本記事で出してきた例においても何回か活用してきた機能です(language monster以外だと好きな惣菜発表ドラゴン等)。

[Term](https://docs.rs/console/latest/console/struct.Term.html)構造体はたくさんメソッドを持っていますが、特に下記に挙げたものは使いやすい印象でした。

| メソッド | 概要 |
|:--------|:-----|
| [clear_screen](https://docs.rs/console/latest/console/struct.Term.html#method.clear_screen) | 全画面クリア。なんだかんだこのメソッドを使う方法が一番悩まずに済みました |
| [hide_cursor](https://docs.rs/console/latest/console/struct.Term.html#method.hide_cursor) / [show_cursor](https://docs.rs/console/latest/console/struct.Term.html#method.show_cursor) | アニメーション中等はカーソルがちらつくので隠すといい感じです |
| [read_key](https://docs.rs/console/latest/console/struct.Term.html#method.read_key) | キー入力を受けたら先に進むアニメーションを作るのに役立ちました |

## 色付き文字

ターミナル生活に彩りを与えてくれる色付き文字出力機能は、[Style](https://docs.rs/console/latest/console/struct.Style.html)及び[StyledObject](https://docs.rs/console/latest/console/struct.StyledObject.html)として与えられています。[console::style](https://docs.rs/console/latest/console/fn.style.html)関数が文字列のStyledObjectを返してくれ、StyledObjectは色を付けるメソッドを呼び出せるので、例えば`style("赤文字").red()`のように書くことができます。

StyledObjectは `std::fmt::Display` を実装しているので、 `format!` マクロで文字列にしたり、`println!` マクロでそのまま表示してあげると色付き文字が出力されます。

## [console::pad_str](https://docs.rs/console/latest/console/fn.pad_str.html)

アライメントを考慮しつつ、文字列が所定の長さになるようにスペースでパディングしてくれるユーティリティです。

公式ドキュメントではトップページ以外で明記されていなかったのですが、内部では[unicode_width](https://docs.rs/unicode-width/latest/unicode_width/)が用いられており、**全角文字でも適切な長さにパディングしてくれます**！language monsterではこの関数をフル活用していました。

## console まとめ

ターミナル上で何でもありな出力を生み出してくれるconsoleの紹介でした！dialoguerやindicatifでも満足できない時に使ってみると良いかもしれません。

# その他便利なクレート

4選に入れるか悩んだクレート群です。CLIに関連した機能とは限らないけども、CLIアプリを作成する際に役立つものが多めです。一応触れておいたほうが良いかなぁという思いより本節を書いています。

軽く概要を紹介したいと思います！

## [serde](https://docs.rs/serde/latest/serde/)

https://docs.rs/serde/latest/serde/

clap以上に有名で説明不要かもしれないRustのシリアライザ/デシリアライザ (略してser/de)になります！`derive` featureを有効にして入れたのち、構造体に　`serde::Serialize`, `serde::Deserialize` をderiveする使い方が一般的です。

```rust:Rust
#[derive(serde::Serialize, serde::Deserialize)]
struct Article {
    title: String,
    body: String,
}
```

このように記述し、[serde_json](https://docs.rs/serde_json/latest/serde_json/) や [toml](https://docs.rs/toml/latest/toml/) クレートの力も借りると、下記のようなファイルの読み書きが可能になります。

```json:JSON
{
  "title":"タイトル",
  "body":"ボディ"
}
```

```toml:toml
title = "タイトル"
body = "ボディ"
```

<details><summary>活用例</summary>

```toml:Cargo.toml
[package]
name = "serde_pg"
version = "0.1.0"
edition = "2021"

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

[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
serde = { version = "1.0.201", features = ["derive"] }
serde_json = "1.0.117"
toml = "0.8.12"
```

```rust:main.rs
use clap::Parser;
use std::path::PathBuf;

#[derive(Parser)]
#[command(about, version)]
struct Args {
    save_path: PathBuf,

    #[command(flatten)]
    article: Article,
}

#[derive(serde::Serialize, serde::Deserialize, clap::Args)]
struct Article {
    title: String,
    body: String,
}

fn main() {
    let Args { save_path, article } = Args::parse();

    println!("Save path: {:?}", save_path);

    let content = match save_path.extension() {
        Some(ext) if ext == "json" => serde_json::to_string(&article).unwrap(),
        Some(ext) if ext == "toml" => toml::to_string(&article).unwrap(),
        _ => {
            eprintln!("Unsupported file extension");
            return;
        }
    };

    println!("Content: \n===\n{}\n===", content);

    std::fs::write(save_path, content).unwrap();
}
```
</details>

json, yaml, toml等、フォーマットが定められたファイルの読み書き時にはほぼデファクトで必須なクレートになっています！

## 環境変数関連

CLIアプリケーションでは環境変数による設定もつきものです。環境変数周りの便利クレートを紹介します。

<details><summary>本節のサンプルコード全体</summary>

```toml:Cargo.toml
[package]
name = "env_pg"
version = "0.1.0"
edition = "2021"

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

[dependencies]
dotenvy = "0.15.7"
envy = "0.4.2"
serde = { version = "1.0.201", features = ["derive"] }
```

```rust:main.rs
use dotenvy::dotenv;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
    ip: String,
    port: u16,
}

fn main() {
    dotenv().ok();

    let config = envy::prefixed("APP_").from_env::<Config>().unwrap();

    println!("IP: {}, Port: {}", config.ip, config.port);
}
```

```plaintext:.env
APP_IP="localhost"
APP_PORT=3000
```
</details>

### [envy](https://docs.rs/envy/latest/envy/)

clapの環境変数版とも言えるクレートで、環境変数を構造体にパースしてくれます！構造体には `serde::Deserialize` が実装されている必要があります。

```rust:Rust
#[derive(Deserialize, Debug)]
struct Config {
    ip: String,
    port: u16,
}

let config = envy::prefixed("APP_").from_env::<Config>().unwrap();
```

### [dotenvy](https://docs.rs/dotenvy/latest/dotenvy/) ([dotenv](https://docs.rs/dotenv/latest/dotenv/))

`.env` ファイルに書かれた設定を読み込み、環境変数として扱ってくれるクレートです。他言語でもよくあるやつ。

```plaintext:.env
APP_IP="localhost"
APP_PORT=3000
```

に対して、

```rust:Rust
dotenvy::dotenv().ok();
```

で`APP_IP`と`APP_PORT`が環境変数として扱えるようになります！

:::note warn
2024/05/14 現在

以前は[dotenv](https://docs.rs/dotenv/latest/dotenv/)が使われていたのですが、メンテナンスがされなくなった関係でフォークして開発が続けられているのが[dotenvy](https://docs.rs/dotenvy/latest/dotenvy/)になります。

本記事が古くなって状況が変わっている可能性があるので、一応両方のクレートについて調べてから使っていただきたいです。
:::

## [env_logger](https://docs.rs/env_logger/latest/env_logger/) / [log](https://docs.rs/log/latest/log/)

CLIアプリの丁寧な出力と言えばロギングです！見やすいフォーマットで標準エラー出力にログを吐いてくれる機能を簡単に紹介します！

<details><summary>Cargo.toml</summary>

```toml:Cargo.toml
[package]
name = "log_pg"
version = "0.1.0"
edition = "2021"

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

[dependencies]
dotenvy = "0.15.7"
env_logger = "0.11.3"
log = "0.4.21"
```
</details>

```plaintext:.env
RUST_LOG=INFO
```

```rust:main.rs
use log::{debug, error, info, trace, warn};

fn main() {
    dotenvy::dotenv().ok();

    env_logger::init();

    trace!("This is a trace message");
    debug!("This is a debug message");
    info!("This is an info message");
    warn!("This is a warning message");
    error!("This is an error message");
}
```

```plaintext:実行結果
[2024-05-13T23:42:29Z INFO  log_pg] This is an info message
[2024-05-13T23:42:29Z WARN  log_pg] This is a warning message
[2024-05-13T23:42:29Z ERROR log_pg] This is an error message
```

[log](https://docs.rs/log/latest/log/)クレートは抽象化されたログ出力機構で、ログを出力したい場所にてlogクレートのマクロを呼び出します。

[env_logger](https://docs.rs/env_logger/latest/env_logger/)クレートは実際にログを出力してくれる役割を担うクレートの一つです。環境変数 `RUST_LOG`にしたがって出力するログのレベルを決定します。上記コードでは.envにて設定したログレベル(INFO)以上のログを出力しています。

また、ログのフォーマットもenv_loggerから[カスタム可能](https://github.com/rust-cli/env_logger/blob/main/examples/custom_format.rs)です。

<!-- [tracing](https://docs.rs/tracing/latest/tracing/) については割愛 -->

---

以上、4選には選ばなかったもののCLI制作に役立つクレートたちでした！

この他、冒頭で紹介した[Command Line Applications in Rust](https://rust-cli.github.io/book/index.html)には本記事で紹介しなかった便利クレートが紹介されている他、コマンドの公開方法やドキュメントの生成方法などさらに役立つ情報が載っています、気になった方は読んでみてください！

https://rust-cli.github.io/book/index.html

# まとめ・所感

RustでのCLIアプリケーション制作のポテンシャルが伝わったでしょうか...？CLIアプリケーションさえ作れれば、[Tauriを用いることでGUI化](https://qiita.com/namn1125/items/36482917d9c7a8adb658)できますし、応用の幅は無限にあります！

clapをフル活用するまでは筆者もPython等別な言語でCLIアプリを作ることもあったのですが、clapを知ってからは便利すぎてプロトタイプ段階のアプリも全部Rustで書くようになりました。何より、clapを初め本記事で紹介したクレートによるCLIアプリ制作が楽しすぎて、本記事完成までめちゃくちゃ時間がかかってしまったほどです...！

どうかこのワクワクが伝わって、本記事が読者の皆様のCLIアプリ制作の一助になりますように✨

ここまで読んでいただき、誠にありがとうございました！