#shell #command #shell-environment #command-line #cmd

sheller

🐚 Sheller 是一个用 Rust 编写的壳命令构建器和标准命令扩展库

14 个版本 (5 个重大变更)

0.5.4 2024年2月12日
0.5.3 2024年2月12日
0.4.0 2024年2月9日
0.3.0 2024年2月9日
0.0.2 2024年2月6日

#7 in #cmd

Download history 331/week @ 2024-03-10 13/week @ 2024-03-17 7/week @ 2024-03-31 2/week @ 2024-04-07

每月下载量166

MIT 许可证

25KB
279

Sheller

🐚 Sheller 是一个用 Rust 编写的壳命令构建器和标准命令扩展库。

如果您能 star(⭐) 这个仓库,我将非常感激! 点击跳转到仓库。

Build Status Crates.io

为什么创建这个

TL;DR


我创建它是因为我想从 Rust 在多平台上调用 npm install
(npm 在 Windows 平台上以文件名 npm.cmd 安装,而 Rust 的 std::process::Command 不支持基于 PATHEXT 的可执行文件搜索,如 cmd.exepwsh.exego)


我使用 Rust 编写用于管理 Rust 库和应用程序项目的实用函数。
例如,调用 cargo clippy -- -D clippy::all -D clippy::pedantic
目前,此项目也是一个 Rust 项目。因此,我使用 Rust 编写实用函数。

让我们以 Git 预推送钩子为例,它在推送至 Github 之前进行验证。
此项目的 .cargo-husky/hooks/pre-push 文件在设置 Cargo 项目时被复制到 .git/hooks/pre-push,当调用 git push 时,此脚本将在实际推送之前被调用。
此脚本的实现,夸张地说,只有一行:cargo run --package tool-dev -- pre-push
脚本在一行中调用 Rust 命令行。实际的实现是用 Rust 代码编写的。
在代码中再次调用命令行,例如 cargo check ...cargo clippy ...cargo fmt ...,以及 cargo test ...

它被写成Rust的原因有三个。

首先,Unix Shell或Windows批处理脚本的语法非常困难。
写一两条命令时没有太大问题。
然而,管理需求逐渐增加。
条件语句、循环语句和函数语法按照我的标准来说也不直观。
由于我记不住语法,每次都得查阅Stack Overflow或询问Chat GPT。

第二,为了使用简单的JavaScript或Python,我需要在项目开发环境中添加依赖。
实际上,在我的开发环境中安装Node.js或Python运行时并不需要太多努力。
然而,Rust也有一个方便的工具,叫做 cargo run
根据项目的大小,构建可能需要相当长的时间。
然而,一旦构建了依赖库,即使多次调用也不会再次构建。
所以,在第一次构建之后,开发体验并不差。

第三,我是一个Rust新手。
撰写本文时,Rust项目始于2024年。
为了熟练掌握,每次有机会编程时,我都尽量用Rust来编写。

介绍有点长。
直截了说,是因为 npm install
为了解释因果关系,如下所述。
我开始了一个用Rust编写TypeScript编译器的项目。
我正在用Rust编写代码,在开发环境中安装TypeScript示例项目。
要在Rust中调用 npm install,我必须调用shell命令,并将 npm install 作为参数传递。
这是因为 npm 命令是一个脚本,而不是可执行文件。
由于它是一个脚本,必须作为参数传递给shell命令。
而且这个shell命令在Windows和Unix平台上很大程度上不同。
目标不是创建平台无关的脚本。
目标是能够在shell环境中调用命令行。

因此,我创建了Sheller库。
这是为了在使用的Rust项目中编写实用函数,或者在未来需要时在Rust应用程序中使用它们。

它是如何工作的

如果你想调用 echo hello,那么

Windows

target_familywindows 时。
COMSPEC 环境变量设置为 program,如果环境变量未设置,则使用 cmd.exe 作为后备程序。
还将 args 设置为 ["/D", "/S", "/C", "echo hello"].

Unix

target_familyunix 时。
将环境变量 SHELL 设置为程序,如果没有设置环境变量,则使用 /bin/sh 作为后备程序。
还将 args 设置为 ["-c", "echo hello"].

使用方法

sheller 添加到您的依赖项中。

# Cargo.toml
[dependencies]
sheller = "0.5"

以下是一些使用 sheller 的示例。

如果您只想运行一个 shell 脚本,请按以下方式使用。

// crates/examples/readme/src/run.rs
use sheller::run;

fn main() {
    run!("echo hello");
    // It will be printed as below, or panicked.
    // hello
}

如果您不想出现 panic,可以使用 try_run 方法接收和处理 sheller::Result<()>.

// crates/examples/readme/src/try_run.rs
use sheller::try_run;

fn main() -> sheller::Result<()> {
    try_run!("echo hello")
}

📢 如果您想看到执行的命令行输出,请将 tracing 添加到您的依赖项中。
Sheller 内部使用流行的集中式结构化日志系统 tracing

以下是使用 tracing 的示例。

# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
// crates/examples/readme/src/run_with_log.rs
use sheller::run;

fn main() {
    init_log();

    run!("echo hello");
    // 2024-02-09T19:11:29.897389Z  INFO sheller: Running command. command="/bin/bash" "-c" "echo hello"
    // hello
    // 2024-02-09T19:11:29.898254Z  INFO sheller: Succeeded to run command with zero exit code. command="/bin/bash" "-c" "echo hello"
}

fn init_log() {
    tracing::subscriber::set_global_default(
        tracing_subscriber::FmtSubscriber::builder()
            .with_max_level(tracing::Level::TRACE)
            .finish(),
    )
    .expect("setting default subscriber failed");
}

👀 关于如何使用跟踪的更多信息,请查看 tracing 文档

Sheller 使用 std::process::Command
如果您想更改当前工作路径、stdout/stderr 或环境变量,请使用 Sheller::build 方法。
此方法返回 std::process::Command

以下是更改当前工作路径的示例。

⚠️ 如果您看不到 run 方法,请检查 use sheller::CommandExt

// crates/examples/readme/src/builder.rs
use sheller::{new, CommandExt};

fn main() {
    let mut command = new!("echo hello").build();
    command.current_dir("/my/dir").run();
}

同样,可以同时使用 runtry_run

如果您想将 stdout 管道化,请参阅下面的示例。

// crates/examples/readme/src/pipe.rs
use sheller::new;

static EOL: &str = if cfg!(windows) { "\r\n" } else { "\n" };

fn main() {
    let output = new!("echo hello")
        .build()
        .stdout(std::process::Stdio::piped())
        .output()
        .unwrap();
    assert_eq!(output.stdout, format!("hello{EOL}").as_bytes());
}

除了上述方法外,您当然也可以使用 Rust 官方的 std::process::Command 方法。
有关 std::process::Command 的更多信息,请查看 Rust 官方页面

runtry_run 方法作为 CommandExt 实现。
这些方法的目的在于实用。
因此,您不必一定使用 Sheller

以下是仅使用 CommandExt 而不使用 Sheller 的示例。

// crates/examples/readme/src/command_ext.rs
use sheller::CommandExt;

fn main() {
    let mut command = std::process::Command::new("echo");
    command.arg("hello").run();
}

信息
如果你的 shell 进程没有结束,请使用 kill_tree
例如,当发生 ctrl + c(或 command + c)事件时,将 kill_tree::Config.include_target 设置为 false 可以为除了当前进程外的所有子进程结束。

use kill_tree::{blocking::kill_tree_with_config, Config};
use std::sync::mpsc::channel;

fn cleanup_children() {
    let current_process_id = std::process::id();
    let config = Config {
        include_target: false,
        ..Default::default()
    };
    let result = kill_tree_with_config(current_process_id, &config);
    println!("kill_tree_with_config: {result:?}");
}

fn main() {
    let (tx, rx) = channel();

    ctrlc::set_handler(move || {
        cleanup_children();
        tx.send(()).expect("Could not send signal on channel.");
    })
    .expect("Error setting handler.");

    println!("Current process id: {}", std::process::id());
    println!("Waiting for signal...");
    rx.recv().expect("Could not receive from channel.");
    println!("Got it! Exiting...");
}

依赖项

~420KB