1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

一日一回cargo script安定化してくれの舞① hooq勝利編

Last updated at Posted at 2025-12-05

安定化!cargo!安定化!
ステーブル!スクリプト!すてーぶる!!!!

cargo_script_eye_catch_mini.png

こんにチュア!本記事は 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を導入済みであれば1chmod 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です :thumbsup:

rust_100p.png

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を書けてしまっています :sweat_smile:

対応状況は次で確認できます: 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 がないとエラーになるコードで解説します。

Python
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は、(デフォルトでは)トレースバック系のものを出力してくれません!

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 を仕込みます。

Rust
-#!/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(())
}

次のようにバックトレースが追加されます!

Rust
$ ./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なら エラー発生箇所の情報を含めた バックトレースもどきが得られます!!!!

Rust
#!/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)

どこでどうエラーが発生し伝搬したかはっきりわかります!

気になった方はぜひドキュメントを読んでみてください :bow:

cargo scriptにおいては hooqの勝利 ...!!...hooqのためにもcargo script普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ普及しろ!!!!!!!!!!!!!!!!!!!!!

まとめ

なんでhooqアドベントカレンダーでcargo scriptの話をしたかというと、hooqプロジェクトで利用しているからという理由が一つと、もう一つはcargo scriptでこそ以前は anyhow::Context::with_context を使いまくっていて、hooqでなんとかしたい領域だったからです!

まさかcargo scriptでまともなトレース(もどきですが...)を可読性を損なわずに得られる手段がhooqだけだと判明するとは思わず、棚からぼた餅です!

筆者のためにもcargo script安定化してくれ...というわけでこれからも舞います!

  1. 環境によっては追加の環境構築が必要な場合があります。nightly機能ではあるのですが、もしnightlyを入れていない場合でも実行時に勝手にnightly Rustがインストールされます。

  2. ドキュメント更新用スクリプト

  3. Cargo.toml を持たない main.rs 単体のビルドについてテストするためのスクリプト

  4. mdBookをビルドするためのスクリプト

  5. mdBook向けサンプルコードで、スナップショット付きのプロジェクトを作成するためのスクリプト

  6. 本当に嫌いだったわけではないですよ?スクリプトとしては使わなくなっていった、の意味です

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?