34
18

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 に組み込まれたところがちょっと違うところです。面白そうな気がしたので、試してみました。

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

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!

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

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
---
[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/* してました。

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 しか認識してくれないようなので、今回の例の場合だと、

$ 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!

34
18
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
34
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?