32
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RustAdvent Calendar 2023

Day 16

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

Last updated at Posted at 2023-12-24

この記事は 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 14.7.1
  • rustc 1.76.0-nightly (de686cbc6 2023-12-14) 1.84.0-nightly (705cfe0e9 2024-11-01)
  • cargo 1.76.0-nightly (1aa9df1a5 2023-12-12) 1.84.0-nightly (e75214ea4 2024-10-25)

Hello, World!

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

hello
#!/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 が提供するクレートを使うサンプルを参考に書いてみます。

search
#!/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 -Zscriptcargo -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 のフラグを使うと、cargorun 以外のサブコマンドも使うことができます。例えば、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 追記、2024.11.2 修正)

manifest の記述を挟むための ```--- に変更となりましたので修正しました。 (2024.11.2 追記)

32
17
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
32
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?