#shell #process #script #pipe #cli

cmd_lib_macros

常见的Rust命令行宏和工具,便于轻松编写类似shell脚本的任务

63个版本 (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.1.0 2020年9月7日

#1104过程宏

Download history 2091/week @ 2024-04-23 2049/week @ 2024-04-30 3168/week @ 2024-05-07 2678/week @ 2024-05-14 3217/week @ 2024-05-21 2864/week @ 2024-05-28 2850/week @ 2024-06-04 3270/week @ 2024-06-11 3537/week @ 2024-06-18 2861/week @ 2024-06-25 2719/week @ 2024-07-02 3393/week @ 2024-07-09 3299/week @ 2024-07-16 3294/week @ 2024-07-23 2919/week @ 2024-07-30 3081/week @ 2024-08-06

每月13,244次下载
39 个crate中(通过 cmd_lib)使用

MIT/Apache

29KB
633

cmd_lib

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库旨在提供重定向和管道功能,以及其他使编写类似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 中的消息。

您还可以使用 #[cmd_lib::main] 标记您的 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中并非如此,bash总是会首先进行变量替换。

全局/通配符

这个库不提供全局函数,以避免静默错误和其他惊喜。您可以使用 glob 包代替。

线程安全

这个库非常努力地不设置全局状态,因此可以并行执行 cargo test。在多线程环境中不支持的所有已知API是 tls_init!/tls_get!/tls_set! 宏,并且您应该只为 线程局部 变量使用它们。

许可证:MIT OR Apache-2.0

依赖关系

~1.5MB
~36K SLoC