108 个版本 (32 个稳定版)
1.9.4 | 2024年5月12日 |
---|---|
1.9.3 | 2023年11月29日 |
1.6.1 | 2023年10月29日 |
1.3.0 | 2021年10月16日 |
0.7.8 | 2019年10月2日 |
#80 在 Rust 模式
每月11,667 次下载
在 48 个crate(41 个直接) 中使用
78KB
1.5K SLoC
cmd_lib
Rust 命令行库
常见的 Rust 命令行宏和实用工具,便于在 Rust 编程语言中轻松编写类似 shell 脚本的任务。可在 crates.io 获取。
为什么你需要这个
如果你需要在 Rust 中运行一些外部命令,std::process::Command 是在操作系统系统调用之上的一个好抽象层。它提供了对如何生成新进程的细粒度控制,并允许你等待进程结束并检查退出状态或收集其所有输出。然而,当需要 重定向 或 管道 时,你需要手动设置父进程和子进程的 I/O 处理程序,就像在 Rust 烹饪书 中这样,这通常是繁琐且易出错的。
许多开发者只是选择使用 shell(sh、bash 等)脚本执行此类任务,通过使用 <
进行输入重定向,>
进行输出重定向,以及 |
进行管道。根据我的经验,这 是 shell 脚本中唯一好的部分。你可以找到各种陷阱和神秘的技巧来使 shell 脚本的其它部分工作。随着 shell 脚本的增长,它们最终会变得难以维护,没有人愿意再碰它们。
这个cmd_lib库试图提供重定向和管道功能,以及其他使编写类似shell脚本的任务变得容易的设施,而无需启动任何shell。对于rust菜谱示例,它们通常可以借助这个库实现为一行rust宏,如examples/rust_cookbook.rs所示。由于它们是rust代码,如果需要,您未来可以随时将其原生重写为rust,而无需启动外部命令。
这个库的样子
为了获得第一印象,这里是一个来自examples/dd_test.rs的示例
run_cmd! (
info "Dropping caches at first";
sudo bash -c "echo 3 > /proc/sys/vm/drop_caches";
info "Running with thread_num: $thread_num, block_size: $block_size";
)?;
let cnt = DATA_SIZE / thread_num / block_size;
let now = Instant::now();
(0..thread_num).into_par_iter().for_each(|i| {
let off = cnt * i;
let bandwidth = run_fun!(
sudo bash -c "dd if=$file of=/dev/null bs=$block_size skip=$off count=$cnt 2>&1"
| awk r#"/copied/{print $(NF-1) " " $NF}"#
)
.unwrap_or_else(|_| cmd_die!("thread $i failed"));
info!("thread {i} bandwidth: {bandwidth}");
});
let total_bandwidth = Byte::from_bytes((DATA_SIZE / now.elapsed().as_secs()) as u128).get_appropriate_unit(true);
info!("Total bandwidth: {total_bandwidth}/s");
输出将如下所示
➜ rust_cmd_lib git:(master) ✗ cargo run --example dd_test -- -b 4096 -f /dev/nvme0n1 -t 4
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/examples/dd_test -b 4096 -f /dev/nvme0n1 -t 4`
[INFO ] Dropping caches at first
[INFO ] Running with thread_num: 4, block_size: 4096
[INFO ] thread 3 bandwidth: 317 MB/s
[INFO ] thread 1 bandwidth: 289 MB/s
[INFO ] thread 0 bandwidth: 281 MB/s
[INFO ] thread 2 bandwidth: 279 MB/s
[INFO ] Total bandwidth: 1.11 GiB/s
这个库提供的内容
运行外部命令的宏
let msg = "I love rust";
run_cmd!(echo $msg)?;
run_cmd!(echo "This is the message: $msg")?;
// pipe commands are also supported
let dir = "/var/log";
run_cmd!(du -ah $dir | sort -hr | head -n 10)?;
// or a group of commands
// if any command fails, just return Err(...)
let file = "/tmp/f";
let keyword = "rust";
run_cmd! {
cat ${file} | grep ${keyword};
echo "bad cmd" >&2;
ignore ls /nofile;
date;
ls oops;
cat oops;
}?;
let version = run_fun!(rustc --version)?;
eprintln!("Your rust version is {}", version);
// with pipes
let n = run_fun!(echo "the quick brown fox jumped over the lazy dog" | wc -w)?;
eprintln!("There are {} words in above sentence", n);
无开销的抽象
由于所有宏的词法分析和语法分析都发生在编译时,它基本上可以生成与手动调用 std::process
API相同的代码。它还包括命令类型检查,因此大多数错误可以在编译时而不是在运行时发现。借助像 rust-analyzer
这样的工具,它可以为您提供对正在使用的损坏命令的实时反馈。
您可以使用 cargo expand
检查生成的代码。
直观的参数传递
在传递参数到 run_cmd!
和 run_fun!
宏时,如果它们不是rust 字符串字面量的一部分,它们将被转换为字符串作为原子组件,因此您不需要引用它们。参数将类似于 $a
或 ${a}
在 run_cmd!
或 run_fun!
宏中。
let dir = "my folder";
run_cmd!(echo "Creating $dir at /tmp")?;
run_cmd!(mkdir -p /tmp/$dir)?;
// or with group commands:
let dir = "my folder";
run_cmd!(echo "Creating $dir at /tmp"; mkdir -p /tmp/$dir)?;
您可以将 "" 视为粘合剂,因此引号内部的所有内容都将被视为单个原子组件。
如果它们是 原始字符串字面量的一部分,则不会进行字符串插值,这与惯用的rust相同。但是,您始终可以使用 format!
宏来形成新字符串。例如
// string interpolation
let key_word = "time";
let awk_opts = format!(r#"/{}/ {{print $(NF-3) " " $(NF-1) " " $NF}}"#, key_word);
run_cmd!(ping -c 10 www.google.com | awk $awk_opts)?;
注意这里 $awk_opts
将被视为传递给awk命令的单个选项。
如果您想使用动态参数,可以使用 $[]
访问向量变量
let gopts = vec![vec!["-l", "-a", "/"], vec!["-a", "/var"]];
for opts in gopts {
run_cmd!(ls $[opts])?;
}
重定向和管道
目前支持管道和stdin、stdout、stderr重定向。大部分内容与bash脚本中相同。
日志记录
这个库提供了方便的宏和内置命令来进行日志记录。所有打印到stderr的消息都将被记录。它还会将完整的运行命令包含在错误结果中。
let dir: &str = "folder with spaces";
run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir)?;
run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir; rmdir /tmp/$dir)?;
// output:
// [INFO ] mkdir: cannot create directory ‘/tmp/folder with spaces’: File exists
// Error: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1
它使用rust log crate,您可以使用您最喜欢的日志记录实现。注意,如果您没有提供任何日志记录器,它将使用env_logger从进程的stderr打印消息。
您还可以使用main()
函数标记#[cmd_lib::main]
,这将默认记录main()中的错误。例如
[ERROR] FATAL: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1
内置命令
cd
cd:设置进程当前目录。
run_cmd! (
cd /tmp;
ls | wc -l;
)?;
注意,内置的cd
只会改变当前作用域,并在退出作用域时恢复先前的当前目录。
如果您想更改整个程序的工作目录,请使用std::env::set_current_dir
。
ignore
忽略命令执行中的错误。
echo
将消息打印到stdout。
-n do not output the trailing newline
error, warn, info, debug, trace
以不同的级别将消息打印到日志。如果不需要在命令组内进行日志记录,您也可以使用常规的日志宏。
run_cmd!(error "This is an error message")?;
run_cmd!(warn "This is a warning message")?;
run_cmd!(info "This is an information message")?;
// output:
// [ERROR] This is an error message
// [WARN ] This is a warning message
// [INFO ] This is an information message
低级进程创建宏
spawn!
宏将整个命令作为子进程执行,并返回对其的句柄。默认情况下,stdin、stdout和stderr将从父进程继承。进程将在后台运行,因此您可以同时运行其他东西。您可以调用wait()
来等待进程完成。
使用spawn_with_output!
,您可以通过调用wait_with_output()
、wait_with_all()
甚至使用wait_with_pipe()
进行流处理来获取输出。
还有其他有用的API,您可以查看文档以获取更多详细信息。
let mut proc = spawn!(ping -c 10 192.168.0.1)?;
// do other stuff
// ...
proc.wait()?;
let mut proc = spawn_with_output!(/bin/cat file.txt | sed s/a/b/)?;
// do other stuff
// ...
let output = proc.wait_with_output()?;
spawn_with_output!(journalctl)?.wait_with_pipe(&mut |pipe| {
BufReader::new(pipe)
.lines()
.filter_map(|line| line.ok())
.filter(|line| line.find("usb").is_some())
.take(10)
.for_each(|line| println!("{}", line));
})?;
注册自定义命令的宏
使用正确的签名声明您的函数,并使用use_custom_cmd!
宏进行注册
fn my_cmd(env: &mut CmdEnv) -> CmdResult {
let args = env.get_args();
let (res, stdout, stderr) = spawn_with_output! {
orig_cmd $[args]
--long-option xxx
--another-option yyy
}?
.wait_with_all();
writeln!(env.stdout(), "{}", stdout)?;
writeln!(env.stderr(), "{}", stderr)?;
res
}
use_custom_cmd!(my_cmd);
定义、获取和设置线程局部全局变量的宏
tls_init!(DELAY, f64, 1.0);
const DELAY_FACTOR: f64 = 0.8;
tls_set!(DELAY, |d| *d *= DELAY_FACTOR);
let d = tls_get!(DELAY);
// check more examples in examples/tetris.rs
其他注意事项
环境变量
您可以使用std::env::var从当前进程获取环境变量键。如果环境变量不存在,它将报告错误,并且它还包括其他检查以避免静默失败。
要设置环境变量,请使用std::env::set_var。在std::env模块中还有其他相关的API。
仅为此命令设置环境变量,可以将赋值放在命令之前。例如:
run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?;
安全注意事项
使用宏实际上可以避免命令注入,因为我们会在变量替换之前进行解析。例如,下面的代码即使在没有任何引号的情况下也是可以的
fn cleanup_uploaded_file(file: &Path) -> CmdResult {
run_cmd!(/bin/rm -f /var/upload/$file)
}
在bash中并非如此,它会始终首先进行变量替换。
通配符/通配符
这个库不提供通配符函数,以避免沉默错误和其他惊喜。您可以使用glob包代替。
线程安全
这个库非常努力地不设置全局状态,因此可以很好地执行并行的cargo test
。在多线程环境中不支持的已知API是tls_init!
/tls_get!
/tls_set!
宏,并且您应该只为线程局部变量使用它们。
许可证:MIT OR Apache-2.0
依赖项
~4–13MB
~146K SLoC