安定化!cargo!安定化!
ステーブル!スクリプト!すてーぶる!!!!
こんにチュア!本記事は hooqアドベントカレンダー 6日目の記事です!
hooqというのは筆者が作成した ? 演算子と式の間にメソッドを挿入できる属性マクロです!
...え?「そんなマクロよりも、なんの舞を舞っているのか」...ですって?
それはもちろん「 一日一回cargo script安定化してくれの舞 」に決まっているじゃないですか!...ご存じない...?そうですか...
cargo scriptとは...?
RFC 3424 にある機能で、一言でいえばシバン (Shebang)を利用したRust式スクリプトファイルといったところです! cargo new でディレクトリを用意したりする必要なく、 ペラ1枚のスクリプトとして Rustのプログラムを管理できます。
次の記事様が詳しいです。
cargo scriptは、シバン・普段書くCargo.tomlの中身・ main 関数を含むRustプログラムで構成されます。
#!/usr/bin/env -S cargo +nightly -q -Zscript run --release --manifest-path
---
[dependencies]
hooq = "0.3.0"
---
#[hooq::hooq]
fn main() -> Result<(), &'static str> {
// https://x.com/namnium_01/status/1996659099891913211?s=20
let f = || Err("GOODBYE_WORLD!");
f()?;
Ok(())
}
シバン部分 #!/usr/bin/env -S cargo +nightly -q -Zscript run --release --manifest-path の意味はこんな感じです。
-
#!/usr/bin/env: env コマンドを呼び出し。cargoのパスを直接指定せずとも、環境変数PATHからcargoを取ってきてくれます -
-S: cargoに適切にオプションを渡すためのenvのオプションらしい -
cargo: ここ以降はcargoに渡すオプション -
+nightly: nightlyの機能を使用 -
-q: ビルド情報(Compiling project v0.1.0みたいなやつ)などの出力を抑制する -
-Zscript: cargo script機能を使う -
run --release:cargo run --releaseみたいな意味 -
--manifest-path: 本来であればCargo.tomlのパスを渡すのですが、とりあえずその後で---...---によってCargo.tomlの内容を渡す際は必要なようです
rustupでRustを導入済みであれば1、 chmod 755 ... でスクリプトに実行権限を付けることで、実行できます。
$ chmod 755 goodbye_world.rs
$ ./goodbye_world.rs
[goodbye_world.rs:10:16] "GOODBYE_WORLD!"
10> Err("GOOD..RLD!")
|
[goodbye_world.rs:12:8] "GOODBYE_WORLD!"
12> f()?
|
Error: "GOODBYE_WORLD!"
cargo scriptは便利すぎるのでhooqクレートでもたくさん使っています!
そしてすべてのスクリプトでhooq自体も使ってます。便利なので。
スクリプトにcargo scriptを使う甲斐あってか、hooqは結構色々なCIを走らせているのですがリポジトリは100%Rustです ![]()
cargo scriptは、nightly機能であり実はまだ まともに使えるとは言い難い機能 です。本記事ではそれでもcargo scriptを使いたいと筆者が思う理由を紹介します!
cargo scriptを使う理由3選
理由1: Rustで全部書ける・docs.rsを味方に付けられる
筆者にとってはこれがほぼすべてです。 Rustに慣れすぎてシェルスクリプトもPythonも書けなくなってしまいました ...なぜなら関数やコマンドの意味を調べるのに一苦労するからです。
Rustをスクリプトで使う最大のメリットは、 docs.rs というあまりにも便利なドキュメントサイトを利用できる ことにあります。
GoやTSなどモダンな言語は存じませんが、少なくともPythonやシェルスクリプトで登場するコマンドのそれと比べると、 Rustのドキュメントの読みやすさは段違いです !
なぜ読みやすいかというと、docs.rsでフォーマットが統一されているためというのが一つ。そして他言語にないRust言語の特徴である 可変参照 や 直和型のenum などもかなり読みやすさに寄与しています。 Rustの関数のシグネチャはそれ自体が使い方を説明している 場合が多く、どちらもシグネチャのわかりやすさにつながる文法です。
ドキュメントの読みやすさは筆者がRustを使い続けている理由1位に来るぐらいのものなので、他にそこそこのエコシステムがありドキュメントも読みやすい言語があったら是非教えてほしいです。
ドキュメントなんてなくてもAIに書かせればいいだろって...?あなたとは話し合いが必要そうです
理由2: 依存クレートのバージョンをしっかり固定できる
「uv登場前のPythonが嫌いでした6」と言えば伝わるのではないでしょうか?
この手のスクリプトというのは書き散らすもので、メンテナンスなんてしません。そのため長い時間が経ってグローバルに入れているライブラリのバージョンが変わればすぐ動かなくなります。
スクリプトだからこそ、長い年月が経過してもそのまま実行できてほしい です!そのためにスクリプトで利用している クレートバージョンはスクリプト内で明記・固定しておくべき でしょう。
cargo scriptはまさに理想形と言えます。
ちなみにライブラリのバージョン固定をスクリプトで行うというのは、uvもできるらしいです。
https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies
理由3: clap・handlebarsなどCLIツールを作るためのクレートが充実している
コマンドラインオプションを宣言的に記述できる clap クレートは特にcargo scriptを使いたい理由の大部分を占めます。
clapを使えば思考停止で --help が完成しますし、今回のテーマであるスクリプトという点でいくと、 コマンドライン引数の定義が宣言的である というのはかなりメンテのしやすさに寄与します!
clap周りは次の記事に以前まとめました!良かったら読んでみてください。
cargo scriptのデメリット2選
一方で前述の通り使いづらい部分もあり、cargo scriptは (まだ)普通の人にはオススメしません!
cargo scriptを利用する際にイマイチな理由を2つ話します。
デメリット1: 安定化しておらず、VS Codeも未対応
まだnightly機能で、 rust-analyzer・VS Codeがcargo scriptに対応していません 。
したがってVS Codeからのコーディング支援はないので、この支援に頼っている人にはcargo scriptは難易度が高いです。
冒頭で挙げた記事様 はVS Codeのrust-analyzerに認識させる方法を載せていますが、設定をいちいち書き換えるぐらいなら cargo new した方がよいとなってしまいますね... cargo build 時に出るエラーを元にプログラムを修正するのは筆者にとっては全然苦痛ではないので、筆者は普通に設定せずcargo scriptを書けてしまっています ![]()
対応状況は次で確認できます: Tracking Issue for cargo-script RFC 3424 #12207
改めて覗いてみると意外ともうすぐ安定化しそう...?stable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろstable入りしろ
デメリット2: 実行時エラーに(基本)トレースバックがない
これはRust言語自体の問題です。Rustで特に何も施さなかった場合 スクリプト言語よりもランタイムエラーの情報は乏しい という問題があります。
config.toml がないとエラーになるコードで解説します。
from pathlib import Path
def read_config() -> str:
return Path("config.toml").read_text(encoding="utf-8")
def main() -> None:
content = read_config()
print(f"config: {content}")
if __name__ == "__main__":
main()
この時Pythonなら トレースバックを得られます 。
$ python3 read_toml.py
Traceback (most recent call last):
File "/home/namn/workspace/qiita_adv_articles_2025/programs/read_toml.py", line 11, in <module>
main()
File "/home/namn/workspace/qiita_adv_articles_2025/programs/read_toml.py", line 7, in main
content = read_config()
File "/home/namn/workspace/qiita_adv_articles_2025/programs/read_toml.py", line 4, in read_config
return Path("config.toml").read_text(encoding="utf-8")
File "/usr/lib/python3.10/pathlib.py", line 1134, in read_text
with self.open(mode='r', encoding=encoding, errors=errors) as f:
File "/usr/lib/python3.10/pathlib.py", line 1119, in open
return self._accessor.open(self, mode, buffering, encoding, errors,
FileNotFoundError: [Errno 2] No such file or directory: 'config.toml'
一方Rustは、(デフォルトでは)トレースバック系のものを出力してくれません!
#!/usr/bin/env -S cargo +nightly -q -Zscript run --release --manifest-path
---
[dependencies]
anyhow = "1.0.100"
---
fn read_config() -> anyhow::Result<String> {
let content = std::fs::read_to_string("config.toml")?;
Ok(content)
}
fn main() -> anyhow::Result<()> {
let content = read_config()?;
println!("config: {content}");
Ok(())
}
$ ./read_toml.rs
Error: No such file or directory (os error 2)
Pythonの方がどこでエラーになったかがわかりやすいので、スクリプト側に問題があった際の修正が容易です!
Rustでもバックトレースは普通に得られるので問題なし(?)
でも実は RUST_BACKTRACE=1 を付けて実行すればバックトレースが得られます。
言われずとも知ってました...?(汗)
シバン部分に RUST_BACKTRACE=1 を仕込みます。
-#!/usr/bin/env -S cargo +nightly -q -Zscript run --release --manifest-path
+#!/usr/bin/env -S RUST_BACKTRACE=1 cargo +nightly -q -Zscript run --release --manifest-path
---
[dependencies]
anyhow = "1.0.100"
---
fn read_config() -> anyhow::Result<String> {
let content = std::fs::read_to_string("config.toml")?;
Ok(content)
}
fn main() -> anyhow::Result<()> {
let content = read_config()?;
println!("config: {content}");
Ok(())
}
次のようにバックトレースが追加されます!
$ ./read_toml.rs
Error: No such file or directory (os error 2)
Stack backtrace:
0: <anyhow::Error as core::convert::From<std::io::error::Error>>::from
1: std::sys::backtrace::__rust_begin_short_backtrace::<fn() -> core::result::Result<(), anyhow::Error>, core::result::Result<(), anyhow::Error>>
2: std::rt::lang_start::<core::result::Result<(), anyhow::Error>>::{closure#0}
3: std::rt::lang_start_internal
4: main
5: <unknown>
6: __libc_start_main
7: _start
先ほどよりは情報が得られていそうです。
hooqという選択肢を紹介したい
ただまだ問題点があって(執筆中に気づきました)、 cargo scriptにおいては anyhow(とcolor-eyre)のバックトレースでは ファイル名およびエラー発生行がわからない ようです!
そこでアドベントカレンダーの主役hooqを宣伝させてほしいです!hooqなら エラー発生箇所の情報を含めた バックトレースもどきが得られます!!!!
#!/usr/bin/env -S cargo +nightly -q -Zscript run --release --manifest-path
---
[dependencies]
anyhow = "1.0.100"
hooq = "0.3.0"
---
use hooq::hooq;
#[hooq(anyhow)]
fn read_config() -> anyhow::Result<String> {
let content = std::fs::read_to_string("config.toml")?;
Ok(content)
}
#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
let content = read_config()?;
println!("config: {content}");
Ok(())
}
$ ./read_toml_hooq.rs
Error: [read_toml_hooq.rs:18:32]
18> read_config()?
|
Caused by:
0: [read_toml_hooq.rs:12:57]
12> std::fs::read_to_string("conf..toml")?
|
1: No such file or directory (os error 2)
どこでどうエラーが発生し伝搬したかはっきりわかります!
気になった方はぜひドキュメントを読んでみてください ![]()
cargo scriptにおいては hooqの勝利 ...!!...hooqのためにもcargo script普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ!!!!!!!!!!!!!!!!!!!!!
まとめ
なんでhooqアドベントカレンダーでcargo scriptの話をしたかというと、hooqプロジェクトで利用しているからという理由が一つと、もう一つはcargo scriptでこそ以前は anyhow::Context::with_context を使いまくっていて、hooqでなんとかしたい領域だったからです!
まさかcargo scriptでまともなトレース(もどきですが...)を可読性を損なわずに得られる手段がhooqだけだと判明するとは思わず、棚からぼた餅です!
筆者のためにもcargo script安定化してくれ...というわけでこれからも舞います!

