本記事では、RustでCLIアプリケーションを作成する際に便利なクレートを4つ紹介いたします!
筆者は大のRust好きですが、それを抜きにしても CLIアプリ制作はRustが一番楽 と言い切ってしまえるぐらいのポテンシャルがこのクレート達(特にclap)にはあります!筆者にとって使用頻度が高い順に彼らを紹介していきたいと思います。
クレート名 | 概要 | 項目へ |
---|---|---|
clap | コマンドライン引数パーサー | |
dialoguer | ユーザー入力ダイアログ | |
indicatif | プログレスバー表示 | |
console | 装飾・ANSIエスケープ | |
その他 | serde, log, etc... |
とにかく clapクレートが便利 です!
clapを使うだけで一瞬でCLIアプリケーションの体裁が整います
他3つは全てconsole-rsが提供するユーティリティクレートで、かゆいところに手が届くイメージです
4つのクレートを全て活用すると次のデモのようなCLIアプリケーションも簡単(※当社比)に作れます!
asciinema
リポジトリ: https://github.com/anotherhollow1125/lang_monster
Gif中の次のシーンにて本記事で紹介するクレートが使われています。
-
./lang_monster --help
- clapクレートによりヘルプオプションが追加されています
- アセットダウンロードプログレスバー
- indicatifクレートを使って表示しています
- 左上始点でセリフ表示・色付き文字
- consoleクレートにてANSIエスケープを出力することで実現しています
- 主人公名ライバル名の選択・入力
- dialoguerクレートを使って実現しています
- オプション入力 (
./lang_monster -t auto -s fast -u Rust -r Go
)- clapクレートでパースしています
RustでCLIアプリケーションを作成するためのチュートリアルは色々あります。特にRust CLI WGがまとめているCommand Line Applications in Rustは各種トピックがバランスよく散りばめられている感じで良かったりするので、今さら普通にまとめてもn番煎じかもしれません。
そこで本記事のオリジナリティとして、CLI "らしさ" を備えたクレートをおすすめ順に、豊富なサンプルコードとともに紹介していければなと思います!
環境・前提
Rustの環境構築に関しては省略させていただきます。
ちなみにインストールはほぼ一瞬です!: はじめに - Rustプログラミング言語
$ cargo --version
cargo 1.77.2 (e52e36006 2024-03-26)
$ cargo new project
では、
ゆめと ぼうけんと!
Rust CLIアプリ制作 のせかいへ!
レッツゴー!
お手軽パーサー clap
clapはCLIアプリの要であろう、コマンドライン引数をパースするためのライブラリです!Command Line Argument Parser略してCLAP (多分)
cargo add clap --features derive
とコマンドを打ってクレートを追加し、構造体に少しだけ細工をするだけで一気にCLIアプリケーションもどきが作れる、それがclapです! お手軽すぎる
cargo add
での追加とCargo.toml
に直接追記する追加どちらでも変わらないため、次節以降はCargo.toml
ファイルを提示するに留めます
--help
オプションと --version
オプション
例えば、clap::Parser
を構造体にderiveしてparse
メソッドを呼び出すだけでヘルプオプションが生えます。
use clap::Parser;
#[derive(Parser)]
struct Args;
fn main() {
let _ = Args::parse();
}
実行・出力例
$ cargo run -q -- --help
Usage: project
Options:
-h, --help Print help
cargo run
による実行時、cargo
ではなくプログラムへ引数を渡すには、間に--
が必要です。なお、cargo
コマンドに渡している-q
はcargo
コマンド自体の出力(コンパイル情報等)を抑制するものです。
「一瞬でヘルプオプションが生えただと...?貴様何をした...?!」
「何って...トレイトをderiveしただけですが...また俺何かやっちゃいましたか...? 」
Cargo.toml
に書いた description
をヘルプコマンドで表示することも可能です!ついでに --version
オプションも生やしてみます。
[package]
name = "project"
+ version = "3.3.4"
edition = "2021"
+ description = "clapクレートデモ用プロジェクト"
[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
use clap::Parser;
#[derive(Parser)]
+ #[command(version, about)]
struct Args;
fn main() {
let _ = Args::parse();
}
実行・出力例
$ cargo run -q -- --help
clapクレートデモ用プロジェクト
Usage: project
Options:
-h, --help Print help
-V, --version Print version
$ cargo run -q -- --version
project 3.3.4
何の機能もないアプリなのにこれだけでそれっぽいです!
ポジショナル引数
コマンドライン引数を追加していき、さらにそれっぽくしてみます。
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
/// ポジショナル引数
quote: String,
}
fn main() {
let args = Args::parse();
println!("{:?}", args);
}
実行・出力例
$ cargo run -q -- おひたし
Args { quote: "おひたし" }
下記どちらかの方法で省略可能にもできます。
-
#[arg(default_value = "~")]
や#[arg(default_value_t = ~)]
によってデフォルト値を付与 -
Option
型にする
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
/// ポジショナル引数
quote: String,
/// 省略可能ポジショナル引数
/// デフォルト値付き
#[arg(default_value_t = 1)]
repeat_num: usize,
/// 省略可能ポジショナル引数
/// Option型
lines_num: Option<usize>,
}
実装コード全体例
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));
}
}
///
で始まるドキュメンテーションコメントをフィールドにつけると、--help
でコメントの内容を表示してくれます!
実行・出力例
$ 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
型を使用することで複数の値を受け取れるようにできます!
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
quotes: Vec<String>,
}
実装コード全体例
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
quotes: Vec<String>,
}
fn main() {
let args = Args::parse();
println!("{:?}", args);
}
実行・出力例
$ 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 = "〇〇"
とすれば変数名とは異なる引数名を設定することも可能です。
#[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>,
}
実装コード全体例
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!();
}
}
実行・出力例
$ 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
を実装することで、バリアントがそのまま候補値になります。また、実質的に文字列引数のバリデーションがかかることになります。
#[derive(ValueEnum, Debug, Clone)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
}
#[derive(Parser, Debug)]
struct Args {
#[arg(short = 'L', long, default_value = "error")]
log_level: LogLevel,
}
実装コード全体例
use clap::{Parser, ValueEnum};
#[derive(ValueEnum, Debug, Clone)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
}
#[derive(Parser, Debug)]
struct Args {
#[arg(short = 'L', long, default_value = "error")]
log_level: LogLevel,
}
fn main() {
let args = Args::parse();
println!("{:?}", args);
}
実行・出力例
$ 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'.
カスタムバリデーション
列挙型にできないような値のバリデーションも属性風マクロ value_parser
を介して指定することが可能です!clap自体が用意しているパーサーの他、トレイトFn(&str) -> Result<T, E>
が実装されている関数やクロージャを指定することができます。以下の例ではRegex::new
がちょうど適していたためそのまま渡しています。
#[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>,
}
実装コード全体例
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);
}
実行・出力例
$ 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'.
おまけ 好きな惣菜発表ドラゴン
好きな総菜発表ドラゴン ソースコード全体
リポジトリ: https://github.com/anotherhollow1125/fav_say_dragon
お借りした素材
- 好きな惣菜発表ドラゴンのAA
[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"] }
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()
}
pre_captions = ["好きなコマンド発表ドラゴンが", "好きなコマンドを発表します"]
side_dishes = ["jq", "grep", "cat", "python3\n-m http.server"]
after_captions = ["なんの略称かわからないコマンドも", "好き 好き 大好き"]
以下、発表ドラゴンのコマンドライン引数部分を抜き出したものになります。
#[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アプリもどきをたくさん量産できました
clapにはサブコマンドや引数をまとめる機能など、本記事で紹介していない機能がたくさんあります!また、本記事で紹介したのは宣言的にコマンドライン引数の設定を記述できるderiveによる方法でしたが、もう少し柔軟に(例えば、ランタイム時などに)設定を行えるbuilderによる方法もあります。
かなり詳細なチュートリアルがあるので、気になった方は公式ドキュメントの方を参照してみてください。
本記事で紹介したかった内容の9割はclapでしたので以降は少し軽めになっていきますが、dialoguer, indicatif, consoleも見ていきます
インタラクティブ! dialoguer
dialoguerは、対話的にユーザーからの入力を受け取る際に活躍するクレートです!
大体のCLIアプリはコマンドライン引数さえ受け取れれば後は出力のみという場合が多いかと思いますが、それで事足りずユーザーと対話する必要が出てきた際はこのdialoguerクレートの出番というわけです。
本記事では選択肢を提示してユーザーに選択してもらうSelectと、ユーザーからの標準入力を受け取るInputを紹介します。
Select
文字列スライスを items
メソッドで渡したのち、その選択肢を interact
メソッドにて表示します。ユーザーからの選択は文字列ではなく配列のインデックスとして受け取ります。
let choices = &["はい", "いいえ"];
let choice: usize = Select::new() // 選択したインデックスが返る
.items(choices)
.default(0)
.interact()?;
ソースコード全体
[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"
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!("まぁまぁそう言わず、わしの味方になってくれまいか...?");
}
}
}
}
列挙型等を渡せないのは残念ですが、シンプルに選択プロンプトを出せる良い機能です!
Input
標準入力から文字列を受け取れるシンプルな機能になります!
let input: String = Input::new()
.with_prompt("You")
.interact_text()?;
ソースコード全体
リポジトリ: https://github.com/anotherhollow1125/chatgpt_demo
[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"] }
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アプリ例
記事とは関係ないですが、なんと記事を書いている途中で新しいモデルの gpt-4o
が使えるようになりました...!同じコードでそのまま該当部分を差し替えるだけでおkです!
ほとんどクエリのための構造体の定義で行数を持っていかれていますが、入出力部分はシンプルな作りになっています。
ちなみにライブラリを使わずに「プロンプトを出して改行せずに標準入力を受け取る」のを実現するには、print!
マクロとstd::io::Write::flush
を使う必要があり不格好です...
dialoguer::Input を使わない入力
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);
}
Inputならユーザーによる標準入力処理をスッキリ書けるというわけです!
その他
その他にもConfirm, FuzzySelect, MultiSelect, Sort, Password, Input with History, Editorといった便利な構造体が用意されていますが、全部説明していくのは大変なため、一つのサンプルコードにまとめてみました!
ソースコード
リポジトリ: https://github.com/anotherhollow1125/diag_all_in_one
[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"] }
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(())
}
構造体名 | 概要 |
---|---|
Confirm | [y/n] 形式で質問する |
FuzzySelect | 入力による選択で、徐々に絞り込まれていく |
MultiSelect | チェックボックスによる複数選択ができる |
Sort | ユーザーによりソートされたインデックスを返す |
Password | Inputと同様に入力を受け付けるが、入力内容を表示しない |
BaisicHistory | 過去の入力を履歴として保存する |
Editor | Vim, Emacsなどのデフォルトエディタを開き、入力を受け付ける |
dialoguer まとめ
dialoguerを使うことで、ライブラリなしだと多少煩雑なユーザーからの標準入力を扱えるのを示せました。
対話型CLIアプリもこれで安心して作れます!
進捗どうですか? indicatif
重い処理や、複数のタスクからなる処理を実行する際、実行を開始した後になって「ちゃんとプログラムが動いていることを確かめる機構を付けておくべきだった...」と後悔した経験は誰しもあるのではないでしょうか?
タスクバーの表示というのはまさにそのためにあるものでしょう。というわけで、プログレスバーをお手軽に表示できる indicatif クレートの紹介です!
ソースコード
リポジトリ: https://github.com/anotherhollow1125/almost_done
[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"
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();
}
進捗は主に以下の方法で管理できます。
-
inc
メソッド等を呼び更新する方法 - イテレータをラップして要素処理ごとに更新される方法
なお、マルチスレッド処理が考慮されており内部的には Arc が使われているようです。もとい上記例でもマルチスレッドであるように、プログレスバーを表示するようなアプリはどれもマルチスレッドにしがちでしょうから、当然の仕様なのかもしれません。
inc
メソッドによる方法
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
を呼び出す前に挟んでも良しです!
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
メソッドによるものと同じになります。
ソースコード全体
[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"
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));
});
}
プログレスバーのフォーマット
プログレスバーとして表示する情報は細かく設定することが可能です。本記事では詳細には触れず軽く紹介します!
indicatif::style::ProgressStyle::with_template にて表示する情報のテンプレート指定、progress_chars にてプログレスバーの完了/途中/未着手部分の表示文字の変更ができます(下記例では .progress_chars("#>-")
)。
テンプレートについては https://docs.rs/indicatif/latest/indicatif/#templates に詳細が書いています。本節最初に示した例のプログレスバーは次のようなフォーマットになっています。
{spinner:.green} [{elapsed_precise}] {msg} {percent:>3}% [{bar:20.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})
⠐ [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
dialoguerも一部そうでしたが、特にindicatifが提供してくれるプログレスバーなどは、キャリッジリターンやANSIエスケープを用いて表示する必要があり、これを直接文字列に含めて操作を行うのは至難の業です。だからこそdialoguerやindicatifの存在がありがたいのでした。
このconsoleは、そんなANSIエスケープをコードベースで行えるようにしてくれるクレートです!具体的には、ターミナル上のカーソル移動(ないし文字列削除・上書き)、色付き文字の出力ができます。それ以外にも、絵文字の安全なサポート、ユニコード文字列幅を考慮したパディング機能を備え、実装者のわがままを最後まで叶えてくれます!
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()),
];
ソースコード全体
[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"
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(())
}
ここからはconsoleクレートのおすすめ機能を紹介します。ソースコードは上記例と重複する部分が多いので省略します。適宜上記例を見ていただければと思います。
カーソル操作・文字の削除
本記事で出してきた例においても何回か活用してきた機能です(language monster以外だと好きな惣菜発表ドラゴン等)。
Term構造体はたくさんメソッドを持っていますが、特に下記に挙げたものは使いやすい印象でした。
メソッド | 概要 |
---|---|
clear_screen | 全画面クリア。なんだかんだこのメソッドを使う方法が一番悩まずに済みました |
hide_cursor / show_cursor | アニメーション中等はカーソルがちらつくので隠すといい感じです |
read_key | キー入力を受けたら先に進むアニメーションを作るのに役立ちました |
色付き文字
ターミナル生活に彩りを与えてくれる色付き文字出力機能は、Style及びStyledObjectとして与えられています。console::style関数が文字列のStyledObjectを返してくれ、StyledObjectは色を付けるメソッドを呼び出せるので、例えばstyle("赤文字").red()
のように書くことができます。
StyledObjectは std::fmt::Display
を実装しているので、 format!
マクロで文字列にしたり、println!
マクロでそのまま表示してあげると色付き文字が出力されます。
console::pad_str
アライメントを考慮しつつ、文字列が所定の長さになるようにスペースでパディングしてくれるユーティリティです。
公式ドキュメントではトップページ以外で明記されていなかったのですが、内部ではunicode_widthが用いられており、全角文字でも適切な長さにパディングしてくれます!language monsterではこの関数をフル活用していました。
console まとめ
ターミナル上で何でもありな出力を生み出してくれるconsoleの紹介でした!dialoguerやindicatifでも満足できない時に使ってみると良いかもしれません。
その他便利なクレート
4選に入れるか悩んだクレート群です。CLIに関連した機能とは限らないけども、CLIアプリを作成する際に役立つものが多めです。一応触れておいたほうが良いかなぁという思いより本節を書いています。
軽く概要を紹介したいと思います!
serde
clap以上に有名で説明不要かもしれないRustのシリアライザ/デシリアライザ (略してser/de)になります!derive
featureを有効にして入れたのち、構造体に serde::Serialize
, serde::Deserialize
をderiveする使い方が一般的です。
#[derive(serde::Serialize, serde::Deserialize)]
struct Article {
title: String,
body: String,
}
このように記述し、serde_json や toml クレートの力も借りると、下記のようなファイルの読み書きが可能になります。
{
"title":"タイトル",
"body":"ボディ"
}
title = "タイトル"
body = "ボディ"
活用例
[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"
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();
}
json, yaml, toml等、フォーマットが定められたファイルの読み書き時にはほぼデファクトで必須なクレートになっています!
環境変数関連
CLIアプリケーションでは環境変数による設定もつきものです。環境変数周りの便利クレートを紹介します。
本節のサンプルコード全体
[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"] }
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);
}
APP_IP="localhost"
APP_PORT=3000
envy
clapの環境変数版とも言えるクレートで、環境変数を構造体にパースしてくれます!構造体には serde::Deserialize
が実装されている必要があります。
#[derive(Deserialize, Debug)]
struct Config {
ip: String,
port: u16,
}
let config = envy::prefixed("APP_").from_env::<Config>().unwrap();
dotenvy (dotenv)
.env
ファイルに書かれた設定を読み込み、環境変数として扱ってくれるクレートです。他言語でもよくあるやつ。
APP_IP="localhost"
APP_PORT=3000
に対して、
dotenvy::dotenv().ok();
でAPP_IP
とAPP_PORT
が環境変数として扱えるようになります!
env_logger / log
CLIアプリの丁寧な出力と言えばロギングです!見やすいフォーマットで標準エラー出力にログを吐いてくれる機能を簡単に紹介します!
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"
RUST_LOG=INFO
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");
}
[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クレートは抽象化されたログ出力機構で、ログを出力したい場所にてlogクレートのマクロを呼び出します。
env_loggerクレートは実際にログを出力してくれる役割を担うクレートの一つです。環境変数 RUST_LOG
にしたがって出力するログのレベルを決定します。上記コードでは.envにて設定したログレベル(INFO)以上のログを出力しています。
また、ログのフォーマットもenv_loggerからカスタム可能です。
以上、4選には選ばなかったもののCLI制作に役立つクレートたちでした!
この他、冒頭で紹介したCommand Line Applications in Rustには本記事で紹介しなかった便利クレートが紹介されている他、コマンドの公開方法やドキュメントの生成方法などさらに役立つ情報が載っています、気になった方は読んでみてください!
まとめ・所感
RustでのCLIアプリケーション制作のポテンシャルが伝わったでしょうか...?CLIアプリケーションさえ作れれば、Tauriを用いることでGUI化できますし、応用の幅は無限にあります!
clapをフル活用するまでは筆者もPython等別な言語でCLIアプリを作ることもあったのですが、clapを知ってからは便利すぎてプロトタイプ段階のアプリも全部Rustで書くようになりました。何より、clapを初め本記事で紹介したクレートによるCLIアプリ制作が楽しすぎて、本記事完成までめちゃくちゃ時間がかかってしまったほどです...!
どうかこのワクワクが伝わって、本記事が読者の皆様のCLIアプリ制作の一助になりますように✨
ここまで読んでいただき、誠にありがとうございました!