この記事は Rust Advent Calendar 2023 シリーズ 2 の 16日目の記事です。
はじめに
Nightly 限定ですが、Rust がスクリプト言語っぽく使えるようになっています。Unix系OSやLinuxなどには、text-file
の先頭行に #!program
と書いておくと、program text-file
が起動される shebang と呼ばれる機能がありますが、Rust(というか、cargo)がそれに対応しましたよ、ということです。これまでも cargo-script や rust-script など、同じような取り組みがありましたが、cargo に組み込まれたところがちょっと違うところです。面白そうな気がしたので、試してみました。今回の環境は以下の通りです。
- Macbook Pro 13-inch, M1, 2020
- macOS 14.2.1
- rustc 1.76.0-nightly (de686cbc6 2023-12-14)
- cargo 1.76.0-nightly (1aa9df1a5 2023-12-12)
Hello, World!
さっそく、やってみましょう!
#!/usr/bin/env -S cargo +nightly -q -Zscript
fn main() {
println!("Hello, World!");
}
ファイルを hello
という名前で保存して、chmod +x hello
しておきます。
$ ./hello
Hello, World!
期待通りですね。1回目の実行時間は 0.50秒程度、2回目以降は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 が提供するクレートを使うサンプルを参考に書いてみます。
#!/usr/bin/env -S cargo +nightly -q -Zscript run --release --manifest-path
```cargo
[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)のファイルからファイルにはない文字列をサーチしてみましょう。
1回目は、
$ time ./search xxxxxxxxxxx ~/tmp/ls-l.txt
real 0m10.372s
user 0m0.125s
sys 0m0.182s
ですが、2回目以降は、
$ time ./search xxxxxxxxxxx /tmp/ls-l.txt
real 0m0.194s
user 0m0.086s
sys 0m0.096s
となりました。比較のために、ripgrep、grep, GNU awk でも実測してみました。いずれも、5回の測定での最速値になります。
実行時間(秒) | |
---|---|
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/*
してました。
おわりに
Rust のクレートもだいぶ充実しており、Rust で数十行の自分だけのツールを作ることも時々あるので、この機能は意外と使えるんじゃないかなと思っているところです。
ということで、今回はちょっとした小ネタでした。では、Have a merry Christmas!
Visual Studio Code で rust-analyzer を使う場合、1つのディレクトリに1つの Rust ソースファイルがある場合、概ね正しく動作するようです。```cargo
と```
の間が、Syntax Error になってしまったり proc macro がうまくハンドリングできなかったりなどの問題が残っているようです。 (2024.5.6 追記)