#shell #process #script #pipe #env-var #cli #cli-command

cmd_lib_cf

内置 CREATE_NO_WINDOW 的 cmd_lib 的修改版本

5 个稳定版本

1.3.4 2021 年 12 月 5 日

#547 in 命令行界面

MIT/Apache

68KB
1K SLoC

cmd_lib_cf

Rust 命令行库

常见的 Rust 命令行宏和实用工具,便于在 Rust 编程语言中轻松编写类似 shell 脚本的任务。可在 crates.io 上找到。

Build status Crates.io

你需要它的原因

如果你需要在 Rust 中运行一些外部命令,std::process::Command 是在操作系统系统调用之上提供良好抽象层的良好抽象层。它提供了对如何创建新进程的细粒度控制,并允许你等待进程结束并检查退出状态或收集所有输出。然而,当需要 重定向管道 时,你需要手动设置父进程和子进程的 IO 处理器,就像在 rust cookbook 中这样,这通常是一项繁琐的工作。

许多开发者只是选择使用 shell (sh, bash, ...) 脚本来执行此类任务,通过使用 < 来重定向输入,> 来重定向输出和 '|' 来管道输出。在我的经验中,这是 shell 脚本中 唯一好的部分。你可以找到各种陷阱和神秘的技巧来使 shell 脚本的其它部分工作。随着 shell 脚本的增长,它们最终将变得难以维护,没有人愿意再碰它们。

这个 cmd_lib_cf 库试图提供重定向和管道功能,以及其它功能,使得无需启动任何 shell 就可以轻松编写类似 shell 脚本的任务。对于 rust cookbook 示例,它们通常可以用这个库的帮助实现为一行 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"));
    cmd_info!("thread $i bandwidth: $bandwidth");
});
let total_bandwidth = Byte::from_bytes((DATA_SIZE / now.elapsed().as_secs()) as u128)
    .get_appropriate_unit(true)
    .to_string();
cmd_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 1.56s
     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 1 bandwidth: 286 MB/s
INFO - thread 3 bandwidth: 269 MB/s
INFO - thread 2 bandwidth: 267 MB/s
INFO - thread 0 bandwidth: 265 MB/s
INFO - Total bandwidth: 1.01 GiB/s

此库提供的内容

运行外部命令的宏

  • run_cmd! --> CmdResult
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";
if run_cmd! {
    cat ${file} | grep ${keyword};
    echo "bad cmd" >&2;
    ignore ls /nofile;
    date;
    ls oops;
    cat oops;
}.is_err() {
    // your error handling code
}
  • run_fun! --> FunResult
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 的消息都将被记录。由于它是返回结果类型,因此也可以记录命令执行失败时的错误。

// this code snppit is using a builtin simple logger, you can replace it with a real logger
init_builtin_logger();
let dir: &str = "folder with spaces";
assert!(run_cmd!(mkdir /tmp/$dir; ls /tmp/$dir).is_ok());
assert!(run_cmd!(mkdir /tmp/"$dir"; ls /tmp/"$dir"; rmdir /tmp/"$dir").is_err());
// output:
// INFO - mkdir: cannot create directory ‘/tmp/folder with spaces’: File exists

它使用 rust log crate,您可以使用您喜欢的任何日志实现。请注意,如果您不提供任何日志记录器,stderr 输出将被丢弃。

内置命令

cd

cd: 设置进程当前目录,可以使用而不需要导入。

run_cmd! (
    cd /tmp;
    ls | wc -l;
)?;

请注意,内置的 cd 只会更改当前作用域,并且在退出作用域时会恢复先前的当前目录。

如果您想更改整个程序的工作目录,请使用 std::env::set_current_dir

ignore

忽略命令执行错误,可以使用而不需要导入。

echo

将消息打印到 stdout,需要使用 use_builtin_cmd! 宏来导入。

use_builtin_cmd!(echo, warn); // find more builtin commands in src/builtins.rs
run_cmd!(echo "This is from builtin command!")?;
run_cmd!(warn "This is from builtin command!")?;

注册您自己的命令的宏

使用 #[export_cmd(..)] 属性来声明您的函数,并使用 use_custom_cmd! 宏来导入它

#[export_cmd(my_cmd)]
fn foo(env: &mut CmdEnv) -> CmdResult {
    let msg = format!("msg from foo(), args: {:?}", env.args());
    writeln!(env.stderr(), "{}", msg)?;
    writeln!(env.stdout(), "bar")
}

use_custom_cmd!(my_cmd);
run_cmd!(my_cmd)?;
println!("get result: {}", run_fun!(my_cmd)?);

低级进程创建宏

spawn! 宏将整个命令作为子进程执行,并返回其句柄。默认情况下,stdin、stdout 和 stderr 从父进程继承。进程将在后台运行,因此您可以同时运行其他操作。您可以通过调用 wait() 来等待进程完成。

使用 spawn_with_output!,您可以通过调用 wait_with_output() 来获取输出,或者甚至使用 wait_with_pipe() 来进行流处理。

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));
})?;

用于定义、获取和设置线程局部全局变量的宏

  • tls_init! 用于定义线程局部全局变量
  • tls_get! 用于获取值
  • tls_set! 用于设置值
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

依赖关系

~1.4–2MB
~43K SLoC