从零开始的 Rust 学习笔记(18) —— Rust Script Runner

Rust 并不能像 Python 那样有全局的 Package(当然,现在就算是写 Python,也很少有谁一上来就往全局环境里安装 Package 了),于是 Rust 要想单独运行一个引用了第三方库的 Rust script 时,就必须用 Cargo 创建一个 project。

绝大多数时候这个倒也是能接受啦,但是有时真的只是想在一边测试一个小的 function 或者验证一下自己的想法。如果直接在 working-in-progress 的 project 里写的话,就可能

  1. 不得不配合已有的部分做一些 error handling
  2. 或者手工测试到该条代码路径上
  3. 又或者写上相应的 unit test

显然只是想快速验证一下的话,上面三种方式都有不便之处。如果单独再用 cargo new 一个 project 的话,也不是不行,但是懒(

在用 Code Runner(对你来说也许是 VSCode 之类的)的时候,直接新建一个 Rust file 开始写会相对方便。假如我们的 Rust script 叫 example.rs,那么要引入第三方 crate 的话,比如引用 regex,我们可以用这样的语法,

// cargo-deps: regex="1"

如果要控制 crate 的 feature 之类的,则可以写

// cargo-deps: opencv = {version = "0.28", default-features = false, features = ["opencv-41", "contrib"]}

虽然并不能实现 Python 那样的全局 package,但是我们可以用代码扫描 exmaple.rs 里面所有的 // cargo-deps: {:dependency},然后自动生成一个 example 目录和相应的 Cargo.toml 文件,接着将 example.rs 文件复制到 example/src/main.rs, 最后自动调用 cargo run

例如对于如下的 example.rs

// cargo-deps: regex="1"
extern crate regex;
use std::env;
use regex::Regex;

fn main() {
    let args = env::args().collect::<Vec<String>>();
    if args.len() != 2 {
        panic!("Please input a string for testing");
    }
    
    let p: Regex = Regex::new(r"\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+_gitlab_backup\.tar").unwrap();
    if let Some(file) = p.captures(&args[1]) {
        println!("[Backup]: {:#?}", file.get(0).unwrap().as_str());
    }
}

我们将生成的目录放在 ${HOME}/.rust-script/example 下,这个目录其实就跟正常 cargo new application 的 layout 相同。自动生成的 Cargo.toml 里, name 会使用 Rust script 的除了扩展名以外的部分,[dependencies] section 下面会原封不动的依次输出 // cargo-deps: {:dependency}{:dependency} 的部分~

[package]
name = "example"
version = "0.1.0"
authors = ["Rust Script"]
edition = "2018"

[dependencies]
regex="1"

那么这里正则表达式如下,我们允许在 cargo-deps: 前后都可以用任意数量的空白字符

lazy_static! {
    static ref CARGO_DEP: Regex = Regex::new(r"^//(?:\s+)cargo-deps:(?:\s+)(.*)").unwrap();
}

接下来就是读文件,遍历每一行,看看是否能跟这个正则表达式匹配。如果匹配的话,就将捕获到的放进 deps 数组里

let mut deps: Vec<String> = Vec::new();
let script = String::from(&*args[1]);
// Open the file in read-only mode (ignoring errors).
let file = File::open(script.clone()).expect(&format!("no such file found: {}", &*args[1]));
let reader = BufReader::new(file);

// Read the file line by line using the lines() iterator from std::io::BufRead.
for (_, line) in reader.lines().enumerate() {
    let line = line.unwrap(); // Ignore errors.
    if let Some(dep) = CARGO_DEP.captures(&line) {
        // push into deps
        deps.push(dep.get(1).unwrap().as_str().into())
    }
}

接下来就是创建 ${HOME}/.rust-script/example 目录和写入Cargo.toml 文件,最后把 example.rs 复制到 ${HOME}/.rust-script/example/src/main.rs

创建目录和写入文件的代码这里就省去了~最后则是使用 std::process::Command切换工作目录,随后设置好参数调用 cargo,当然,我们需要把自己收到的多的 args 也传过去~

同时,我们还需要把 stdin, stdoutstderr 继承给它,这样才能在 example.rs 里正常的输入输出~

let mut cargo_args = vec!["-c"];
let args = format!("cargo run {}", args[2..].join(" "));
cargo_args.push(&args);
let mut run_script = Command::new("bash");
run_script
    .current_dir(project_root)
    .args(&cargo_args)
    .stdin(Stdio::inherit())
    .stderr(Stdio::inherit())
    .stdout(Stdio::inherit())
    .output()
    .expect("failed to execute process");

这样一来,在使用的时候就是

rust-script example.rs arg1 arg2

对于 rust-script 来说,它收到的是 4 个 arg,然后从下标 2 开始 append 到了 cargo run 后面,也就是在新的目录下执行

cargo run arg1 arg2

因此对于 example.rs 来说就是

example arg1 arg2

Leave a Reply

Your email address will not be published. Required fields are marked *

13 − 5 =