こんにチュア!本記事は hooqアドベントカレンダー 7日目の記事です!
hooqというのは筆者が作成した ? 演算子と式の間にメソッドを挿入できる属性マクロです!
本記事では 昨日の記事 の続きということで、良い機会なので筆者がcargo scriptで良く使うクレート群を紹介します。
cargo scriptでよく使うクレート紹介
clapに関しては昨日の記事に以前書いた記事へのリンクを貼りました(なんかややこしいw)が、clapやdialoguer・indicatif以外にも便利なクレートは色々あります!
紹介するためのサンプルスクリプトを作成しました。OpenAI APIを叩いてChatGPTと対話するプログラムです!実行すると冒頭掲載のGIF画像のように対話できます1。
今回のクレート紹介はこのサンプルを実例として実際にどういう風に使っているかを紹介します。
#!/usr/bin/env -S cargo +nightly -q -Zscript run --release --manifest-path
---
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
anyhow = "1.0.100"
hooq = "0.3.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
toml = "0.9.8"
tokio = { version = "1.48.0", features = ["full"] }
reqwest = { version = "0.12.24", features = ["json"] }
dialoguer = "0.12.0"
indicatif = "0.18.3"
---
use anyhow::Result;
use clap::Parser;
use hooq::hooq;
use serde_json::json;
use std::path::PathBuf;
#[derive(Debug, Parser)]
struct Args {
#[arg(short, long, default_value = "gpt-4.1")]
model: String,
#[arg(short, long)]
api_key: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
}
#[derive(serde::Serialize, Clone)]
struct Message {
role: String,
content: String,
}
#[hooq(anyhow)]
async fn query(api_key: &str, model: &str, conversation: &mut Vec<Message>) -> Result<Message> {
let res = reqwest::Client::new()
.post("https://api.openai.com/v1/responses")
.bearer_auth(api_key)
.json(&json!({
"model": model,
"input": conversation,
}))
.send()
.await?;
let body: serde_json::Value = res.json().await?;
#[hooq::method(.$so_far.with_context(|| format!("API response: {:#?}", body)))]
let reply_content = body["output"][0]["content"][0]["text"]
.as_str()?
.to_string();
let reply_message = Message {
role: "assistant".to_string(),
content: reply_content,
};
conversation.push(reply_message.clone());
Ok(reply_message)
}
#[hooq(anyhow)]
#[tokio::main]
async fn main() -> Result<()> {
let Args { model, api_key, output } = Args::parse();
#[hooq::method(.$so_far.context("API key not provided"))]
let api_key = api_key
.or_else(|| std::env::var("OPENAI_API_KEY").ok())?;
let mut conversation: Vec<Message> = Vec::new();
loop {
let user_message = tokio::task::spawn_blocking(|| {
dialoguer::Input::<String>::new()
.with_prompt("You")
.interact_text()
}).await??;
if user_message == "/exit" {
break;
}
conversation.push(Message {
role: "user".to_string(),
content: user_message,
});
let spinner = indicatif::ProgressBar::new_spinner();
spinner.set_message("Waiting for response...");
spinner.enable_steady_tick(tokio::time::Duration::from_millis(100));
let reply = query(&api_key, &model, &mut conversation).await?;
spinner.finish_and_clear();
println!("Assistant: {}", reply.content);
}
if let Some(output_path) = output {
#[derive(serde::Serialize)]
struct Conversations {
messages: Vec<Message>,
}
let serialized = toml::to_string(&Conversations { messages: conversation })?;
tokio::fs::write(output_path, serialized).await?;
}
Ok(())
}
Gist: https://gist.github.com/anotherhollow1125/dda312002226c3405a28c1f8f1c490e8
ChatGPTをCLIから呼んでみるプログラム自体は今までも 何度か組んだことがあった のですが、まとめなおして作成した今回のプログラムはなかなか読みやすそうな自信があります!
紹介するクレートの早見表です。
| クレート名 | グループ | 役割 |
|---|---|---|
| clap | CLI周り | 必須級コマンドライン引数パーサー |
| dialoguer | CLI周り | プロンプトを出してユーザーからの入力を受ける |
| indicatif | CLI周り | 処理進捗/処理中の表示 |
| anyhow | エラー周り | とにかくエラーをいい感じに扱うためのクレート |
| hooq | エラー周り | cargo scriptでエラートレースを表示するためのクレート |
| serde | ファイル読み書き | Rustの構造体をjson・tomlなど別構造に変換するためのユーティリティ |
| serde_json | ファイル読み書き | json読み書きユーティリティ |
| toml | ファイル読み書き | toml読み書きユーティリティ |
| tokio | 非同期周り | Rust非同期ランタイムのデファクトスタンダード |
| reqwest | API叩き周り | お手軽にWebエンドポイントにリクエストを送るクレート |
では順に紹介します!
CLI周りをきれいに記述するクレート
Rustでcargo script・CLIアプリをきれいに書くために使われるクレートです!
可読性マシマシなCLIアプリができます。
clap
言わずもがなRust CLIアプリ界のエースで4番!clapです。
何が最高かというと derive featureを有効にしている場合はコマンドライン引数やオプションを構造体という形で 宣言的に 書けることです。
#[derive(Debug, Parser)]
struct Args {
#[arg(short, long, default_value = "gpt-4.1")]
model: String,
#[arg(short, long)]
api_key: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
}
これでヘルプコマンドまで生えます!オプションの指定方法も「 Option 型だからない時もある」とわかるようになっていたりと、とにかくあらゆる言語の中で一番わかりやすいオプション宣言だと思っています!
言語問わずclapより優れてると言えるコマンドライン引数記述があるならぜひ教えてほしいレベルですね。ないと思います。
dialoguer
CLIアプリにおいてユーザー入力を取り扱う受付係です!dialoguerを使えばとてもシンプルにプロンプトを出せます。
let user_message = tokio::task::spawn_blocking(|| {
dialoguer::Input::<String>::new()
.with_prompt("You")
.interact_text()
}).await??;
今回、全体が非同期アプリなのでお行儀良いコーディングのために一応 tokio::task::spawn_blocking で包むようにしています。(今回の場合だと特に意味はないかも...)
ユーザーの入力がそのまま返り値になるので、Pythonの input のような書き心地を提供してくれています!
indicatif
dialoguerが受付係でその入力を受けて裏方で処理が走るのだとすると、さしずめ受付と裏方の連絡係がindicatifです!
let spinner = indicatif::ProgressBar::new_spinner();
spinner.set_message("Waiting for response...");
spinner.enable_steady_tick(tokio::time::Duration::from_millis(100));
let reply = query(&api_key, &model, &mut conversation).await?;
spinner.finish_and_clear();
GIF画像で Waiting for response... という文言とスピナーが出ていると思います。時間がかかる処理を行う時、ユーザーにプログラムがフリーズしていないことを伝えるのはUI/UXを考えると重要です。
indicatifでお手軽にUXを爆上げできるというわけです。お気に入りのクレートです!
お手軽エラーハンドリングのためのクレート
cargo scriptで書くような規模のプログラムでも、 Result 型で関数の失敗可能性を表すことと、 ? 演算子でその位置を示すというのは、.unwrap() で書くよりもプログラムの見通しを良くしてくれます!
ハンドリングをサボりつつ Result 型・ ? 演算子による可読性向上の恩恵を受けるためのクレートを2つ紹介します!
use anyhow::Result;
use hooq::hooq;
// ...
#[hooq(anyhow)]
async fn query(api_key: &str, model: &str, conversation: &mut Vec<Message>) -> Result<Message> {
// ...
let body: serde_json::Value = res.json().await?;
#[hooq::method(.$so_far.with_context(|| format!("API response: {:#?}", body)))]
let reply_content = body["output"][0]["content"][0]["text"]
.as_str()?
.to_string();
// ...
Ok(reply_message)
}
anyhow
Rustで雑エラーハンドリングを行うための最も有名なクレートですね。 anyhow::Error は内部的には Box<dyn std::error::Error> 、すなわちトレイトオブジェクトが使われています。match式での細かいハンドリングをせずmain関数の最上位まで持っていく使い方をすると楽です。
特にバックトレースにより丁寧な表示を望む場合、color-eyreクレートの利用をお勧めします。(しかし!特にcargo scriptにおいてはバックトレースだとエラー発生行が取得できずあまり意味がない点にお気をつけください。)
hooq
筆者が作成したクレートです!
cargo scriptにてエラートレースを得たい場合、バックトレースはまともな情報を表示してくれないので、筆者が確認している限りでは「anyhow::Context::with_contextをすべての ? 演算子に挟んで line!() マクロで行数をスタックさせておく方法」ぐらいしかありません。
それを代わりにやってくれるのがhooqマクロです!
詳しくは2日目の記事を読んでいただけたら幸いです ![]()
ファイル入出力のためのクレート
ここからはcargo scriptで良く利用するというだけで、Rustを使っていれば皆知っているものばかりなクレートの紹介になります!しかしその常識を記事にしていくことが大切だと考え、ここに記すことにしました。
serde
「Rustの構造体・列挙体 ↔ jsonやtomlなどのフォーマットファイル」
この相互変換を行うためのものとしてRustのクレートの中でも最も有名な部類でしょう。
#[derive(serde::Serialize, Clone)]
struct Message {
role: String,
content: String,
}
#[derive(serde::Serialize)]
struct Conversations {
messages: Vec<Message>,
}
今回は「jsonへのシリアライズ」「tomlへのシリアライズ」が必要なため、各構造体について serde::Serialize を付与しています。
serde_json
jsonとの相互変換でお世話になるクレートです。
let res = reqwest::Client::new()
.post("https://api.openai.com/v1/responses")
.bearer_auth(api_key)
.json(&json!({
"model": model,
"input": conversation,
}))
.send()
.await?;
let body: serde_json::Value = res.json().await?;
#[hooq::method(.$so_far.with_context(|| format!("API response: {:#?}", body)))]
let reply_content = body["output"][0]["content"][0]["text"]
.as_str()?
.to_string();
json! マクロや添え字アクセスがとても便利です。
toml
tomlとの相互変換でお世話になるクレートです。
if let Some(output_path) = output {
#[derive(serde::Serialize)]
struct Conversations {
messages: Vec<Message>,
}
let serialized = toml::to_string(&Conversations { messages: conversation })?;
tokio::fs::write(output_path, serialized).await?;
}
tomlはRust文化圏だとjsonよりも良く見るフォーマットかもしれません。
慣れればjsonより読みやすく、人間が書き換えたり確認したりするファイルの形式に向いています。
Webアクセス周り
今回のスクリプトで利用していたので一応の紹介になります!ちょっとしたCLIツールでWeb APIを叩くこと結構ありますよね...?
tokio
#[hooq(anyhow)]
#[tokio::main]
async fn main() -> Result<()> {
// ...
loop {
let user_message = tokio::task::spawn_blocking(|| {
dialoguer::Input::<String>::new()
.with_prompt("You")
.interact_text()
}).await??;
// ...
}
// ...
if let Some(output_path) = output {
// ...
tokio::fs::write(output_path, serialized).await?;
}
Ok(())
}
とても有名なRustの非同期ランタイムクレートです!
tokio::fs::write が良い例ですが、同期文脈で使うブロッキングIO周りのクレートや関数が、tokioクレート経由で提供されていたりということは結構多く、実際非同期ではそちらを使った方が良いです。他の人が書いたtokio利用コードや、クレート探索は結構収穫があるでしょう。
reqwest
RustでWeb APIを叩く際に用いられるクレートです!hyperをラップしているらしい...?(要出典)
let res = reqwest::Client::new()
.post("https://api.openai.com/v1/responses")
.bearer_auth(api_key)
.json(&json!({
"model": model,
"input": conversation,
}))
.send()
.await?;
blocking featureを有効にするとtokio抜きでも利用できるようになります。
ただ今回みたいにレスポンスが返ってくるまでの間スピナーを回したいなどの事情があるならば、デフォルトである非同期の方を利用しておくのが安牌でしょう。
Rustは非同期の 基礎 についての学習コストは他言語よりも低いため、「処理中にスピナーを回す」みたいなのが特に苦労せず書ける感じがするのもcargo scriptの魅力だと感じます。
まとめ
他にもregex2やhandlebarsなどまれによく利用するクレートはいくつかありますが、本当によく利用するものはこのページで揃えられたのではないかと思います!
VS Codeがcargo scriptを本格サポートしCargo.tomlパート管理も行ってくれるようになれば、お手軽にスクリプトを書ける言語としてRustはかなりよい候補になるはずです。
とても強い信念があるので今日も舞います!ここまで読んでいただきありがとうございました ![]()
