Rust 并不能像 Python 那样有全局的 Package(当然,现在就算是写 Python,也很少有谁一上来就往全局环境里安装 Package 了),于是 Rust 要想单独运行一个引用了第三方库的 Rust script 时,就必须用 Cargo 创建一个 project。
绝大多数时候这个倒也是能接受啦,但是有时真的只是想在一边测试一个小的 function 或者验证一下自己的想法。如果直接在 working-in-progress 的 project 里写的话,就可能
- 不得不配合已有的部分做一些 error handling
- 或者手工测试到该条代码路径上
- 又或者写上相应的 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
, stdout
和 stderr
继承给它,这样才能在 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