Rustをスクリプト言語として使ってみよう

この記事は [Rust Advent Calendar 2023](https://qiita.com/advent-calendar/2023/rust) シリーズ 2 の 16日目の記事です。

## はじめに

Nightly 限定ですが、Rust がスクリプト言語っぽく使えるようになっています。Unix系OSやLinuxなどには、`text-file` の先頭行に `#!program` と書いておくと、`program text-file` が起動される shebang と呼ばれる機能がありますが、Rust（というか、cargo）がそれに対応しましたよ、ということです。これまでも cargo-script や rust-script など、同じような取り組みがありましたが、cargo に組み込まれたところがちょっと違うところです。面白そうな気がしたので、試してみました。

:::note
manifest の記述を挟むための ` ``` ` が `---` に変更となりましたので修正しました。（2024.11.2）
:::
:::note
Visual Studio Code で rust-analyzer を使う方法について追記しました。（2025.1.5）
:::

ちなみに、今回の環境は以下の通りです。

+ Macbook Pro 13-inch, M1, 2020
+ macOS ~~14.2.1~~ 14.7.2
+ rustc ~~1.76.0-nightly (de686cbc6 2023-12-14)~~ rustc 1.85.0-nightly (dd84b7d5e 2024-12-27)
+ cargo ~~1.76.0-nightly (1aa9df1a5 2023-12-12)~~ cargo 1.85.0-nightly (c86f4b3a1 2024-12-24)

## Hello, World!

さっそく、やってみましょう！

```rust:hello
#!/usr/bin/env -S cargo +nightly -q -Zscript

fn main() {
    println!("Hello, World!");
}
```

ファイルを `hello` という名前で保存して、`chmod +x hello` しておきます。

```
$ ./hello
Hello, World!
```

期待通りですね。1回目の実行時間は 0.50秒程度、２回目以降は0.02秒〜0.04秒程度でした。`/bin/echo` ですと 0.01秒未満なので、簡単なスクリプトなら、Rust を使う必要はなさそうです。

ちなみに、macOS の場合は shebang 行中の `-S` がなくても結果は一緒ですが、Linux の場合には、

```
$ ./hello
/usr/bin/env: ‘cargo +nightly -q -Zscript’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines
```

と怒られてしまいます。

## 大きなファイルのサーチ

大きなファイルをサーチするスクリプトを書いてみます。ここでは、[ripgrep が提供するクレートを使うサンプル](https://github.com/BurntSushi/ripgrep/blob/master/crates/searcher/examples/search-stdin.rs)を参考に書いてみます。

````rust:search
#!/usr/bin/env -S cargo +nightly -q -Zscript run --release --manifest-path
---
[package]
version = "0.0.1"
edition = "2021"
[dependencies]
grep-searcher = "0.1"
grep-regex = "0.1"
---

use std::error::Error;
use std::fs::File;
use std::io::BufReader;

fn main() -> Result<(), Box<dyn Error>> {
    let pattern = match std::env::args().nth(1) {
        Some(pattern) => pattern,
        None => { return Err(From::from(format!("usage: search pattern [file]"))) }
    };
    let matcher = grep_regex::RegexMatcher::new(&pattern)?;
    let filename = std::env::args().nth(2).unwrap_or("/dev/stdin".to_string());

    let f = File::open(filename)?;
    let file = BufReader::new(f);

    grep_searcher::Searcher::new().search_reader(
        &matcher,
        file,
        grep_searcher::sinks::UTF8(|lnum, line| {
            print!("{}: {}", lnum, line);
            Ok(true)
        }),
    )?;
    Ok(())
}
````

リリースプロファイルで（つまり最適化をして）ビルドするために、一行目を `cargo +nightly -q -Zscript run --release --manifest-path` としています。残念ながら、`cargo +nightly -q -Zscript --release` では怒られてしまいます。`cargo -Zscript` は `cargo -Zscript run` の省略形みたいですが、今のところ `--release` オプションには対応していないようです。

それから、` ---cargo `と` --- `の間に本来 `Cargo.toml` へ書くべき項目を記載することができます。この例で最低限必要なのは `[dependencies]` の部分になります。

では、`find / -xdev -ls > /tmp/ls-l.txt` して得られた 5.2百万行（1.1GB）のファイルからファイルにはない文字列をサーチしてみましょう。

１回目は、

```
$ time ./search xxxxxxxxxxx ~/tmp/ls-l.txt 

real    0m10.372s
user    0m0.125s
sys     0m0.182s
```

ですが、２回目以降は、

```
$ time ./search xxxxxxxxxxx /tmp/ls-l.txt 

real    0m0.194s
user    0m0.086s
sys     0m0.096s
```

となりました。比較のために、[ripgrep](https://github.com/BurntSushi/ripgrep.git)、grep, GNU awk でも実測してみました。いずれも、５回の測定での最速値になります。

|  | 実行時間（秒）|
|:--|---:|
| search script | 0.194 |
| ripgrep       | 0.311 |
| grep          | 3.357 |
| GNU awk       | 1.605 |

ripgrep も速いですが、不要な処理を全部省いた分、爆速となりました。Rust ですから、ビルドはそれなりに時間がかかるものの、良いクレートを上手く使うと、なかなか素敵な速度で動く感じです。

## cargo のサブコマンドを動かしてみる

`--manifest-path` のフラグを使うと、`cargo` の `run` 以外のサブコマンドも使うことができます。例えば、`build`, `test`, `tree`, `clean` などが良く使うところでしょう。

```
$ cargo +nightly -Zscript tree --manifest-path ./search
search v0.0.1 (/Users/xxxx/xxxx/rust-scripts)
├── grep-regex v0.1.12
│   ├── bstr v1.8.0
│   │   ├── memchr v2.6.4
│   │   └── regex-automata v0.4.3
│   │       ├── aho-corasick v1.1.2
│   │       │   └── memchr v2.6.4
│   │       ├── memchr v2.6.4
│   │       └── regex-syntax v0.8.2
│   ├── grep-matcher v0.1.7
│   │   └── memchr v2.6.4
│   ├── log v0.4.20
│   ├── regex-automata v0.4.3 (*)
│   └── regex-syntax v0.8.2
└── grep-searcher v0.1.13
    ├── bstr v1.8.0 (*)
    ├── encoding_rs v0.8.33
    │   └── cfg-if v1.0.0
    ├── encoding_rs_io v0.1.7
    │   └── encoding_rs v0.8.33 (*)
    ├── grep-matcher v0.1.7 (*)
    ├── log v0.4.20
    ├── memchr v2.6.4
    └── memmap2 v0.9.3
        └── libc v0.2.151
```

`clean` も普通に使えます。

```
$ cargo +nightly -Zscript clean --manifest-path ./search
     Removed 490 files, 170.2MiB total
```

この使い方を発見するまでは、`rm -fr ~/.cargo/target/*` してました。

## Visual Studio Code で rust-analyzer を使う

（2025.1.5 追記）

特に設定しないままだと rust-analyzer から「Cargo.toml や rust-project.json などの manifest file が見つからない」と怒られてしまいます。rust-analyzer は cargo -Zscripts には対応しているので、Rust script コードを manifest file として rust-analyzer に伝えてあげれば、rust-analyzer を Vitual Studio Code で使うことができるようになります。

ただし、ファイル名の拡張子が .rs しか認識してくれないようなので、今回の例の場合だと、

```shell
$ mv hello hello.rs; ln -s hello.rs hello
$ mv search search.rs; ln -s search.rs search
```

などとしてから、


```.vscode/settings.json
{
    "rust-analyzer.linkedProjects": [
        "hello.rs",
        "search.rs"
    ]
}
```

とすると良いです。

## おわりに

Rust のクレートもだいぶ充実しており、Rust で数十行の自分だけのツールを作ることも時々あるので、この機能は意外と使えるんじゃないかなと思っているところです。

ということで、今回はちょっとした小ネタでした。では、Have a merry Christmas!