Giter Club home page Giter Club logo

rust_cmd_lib's Introduction

cmd_lib

Rust command-line library

Common rust command-line macros and utilities, to write shell-script like tasks easily in rust programming language. Available at crates.io.

Build status Crates.io

Why you need this

If you need to run some external commands in rust, the std::process::Command is a good abstraction layer on top of different OS syscalls. It provides fine-grained control over how a new process should be spawned, and it allows you to wait for process to finish and check the exit status or collect all of its output. However, when Redirection or Piping is needed, you need to set up the parent and child IO handles manually, like this in the rust cookbook, which is often tedious and error prone.

A lot of developers just choose shell(sh, bash, ...) scripts for such tasks, by using < to redirect input, > to redirect output and | to pipe outputs. In my experience, this is the only good parts of shell script. You can find all kinds of pitfalls and mysterious tricks to make other parts of shell script work. As the shell scripts grow, they will ultimately be unmaintainable and no one wants to touch them any more.

This cmd_lib library is trying to provide the redirection and piping capabilities, and other facilities to make writing shell-script like tasks easily without launching any shell. For the rust cookbook examples, they can usually be implemented as one line of rust macro with the help of this library, as in the examples/rust_cookbook.rs. Since they are rust code, you can always rewrite them in rust natively in the future, if necessary without spawning external commands.

What this library looks like

To get a first impression, here is an example from 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");

Output will be like this:

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

What this library provides

Macros to run external commands

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

Abstraction without overhead

Since all the macros' lexical analysis and syntactic analysis happen at compile time, it can basically generate code the same as calling std::process APIs manually. It also includes command type checking, so most of the errors can be found at compile time instead of at runtime. With tools like rust-analyzer, it can give you real-time feedback for broken commands being used.

You can use cargo expand to check the generated code.

Intuitive parameters passing

When passing parameters to run_cmd! and run_fun! macros, if they are not part to rust String literals, they will be converted to string as an atomic component, so you don't need to quote them. The parameters will be like $a or ${a} in run_cmd! or run_fun! macros.

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

You can consider "" as glue, so everything inside the quotes will be treated as a single atomic component.

If they are part of Raw string literals, there will be no string interpolation, the same as in idiomatic rust. However, you can always use format! macro to form the new string. For example:

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

Notice here $awk_opts will be treated as single option passing to awk command.

If you want to use dynamic parameters, you can use $[] to access vector variable:

let gopts = vec![vec!["-l", "-a", "/"], vec!["-a", "/var"]];
for opts in gopts {
    run_cmd!(ls $[opts])?;
}

Redirection and Piping

Right now piping and stdin, stdout, stderr redirection are supported. Most parts are the same as in bash scripts.

Logging

This library provides convenient macros and builtin commands for logging. All messages which are printed to stderr will be logged. It will also include the full running commands in the error result.

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

It is using rust log crate, and you can use your actual favorite logger implementation. Notice that if you don't provide any logger, it will use env_logger to print messages from process's stderr.

You can also mark your main() function with #[cmd_lib::main], which will log error from main() by default. Like this:

[ERROR] FATAL: Running ["mkdir" "/tmp/folder with spaces"] exited with error; status code: 1

Builtin commands

cd

cd: set process current directory.

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

Notice that builtin cd will only change with current scope and it will restore the previous current directory when it exits the scope.

Use std::env::set_current_dir if you want to change the current working directory for the whole program.

ignore

Ignore errors for command execution.

echo

Print messages to stdout.

-n     do not output the trailing newline
error, warn, info, debug, trace

Print messages to logging with different levels. You can also use the normal logging macros, if you don't need to do logging inside the command group.

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

Low-level process spawning macros

spawn! macro executes the whole command as a child process, returning a handle to it. By default, stdin, stdout and stderr are inherited from the parent. The process will run in the background, so you can run other stuff concurrently. You can call wait() to wait for the process to finish.

With spawn_with_output! you can get output by calling wait_with_output(), wait_with_all() or even do stream processing with wait_with_pipe().

There are also other useful APIs, and you can check the docs for more details.

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

Macro to register your own commands

Declare your function with the right signature, and register it with use_custom_cmd! macro:

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

Macros to define, get and set thread-local global variables

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

Other Notes

Environment Variables

You can use std::env::var to fetch the environment variable key from the current process. It will report error if the environment variable is not present, and it also includes other checks to avoid silent failures.

To set environment variables, you can use std::env::set_var. There are also other related APIs in the std::env module.

To set environment variables for the command only, you can put the assignments before the command. Like this:

run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?;

Security Notes

Using macros can actually avoid command injection, since we do parsing before variable substitution. For example, below code is fine even without any quotes:

fn cleanup_uploaded_file(file: &Path) -> CmdResult {
    run_cmd!(/bin/rm -f /var/upload/$file)
}

It is not the case in bash, which will always do variable substitution at first.

Glob/Wildcard

This library does not provide glob functions, to avoid silent errors and other surprises. You can use the glob package instead.

Thread Safety

This library tries very hard to not set global states, so parallel cargo test can be executed just fine. The only known APIs not supported in multi-thread environment are the tls_init!/tls_get!/tls_set! macros, and you should only use them for thread local variables.

License: MIT OR Apache-2.0

rust_cmd_lib's People

Contributors

crzysdrs avatar disco0 avatar gdetal avatar rust-shell-script avatar tao-guo avatar yxdunc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rust_cmd_lib's Issues

Would the performance be the same with your library vs "std::process::Command"

Command::new("sh")
    .arg("-c")
    .arg("echo \"%{l}Hello%{c}yellow%{r}Bar\" | lemonbar -p -F#ffffff -B#232323")
    .spawn()
    .expect("Error: connect run \"lemonbar\" Please ensure it is installed!");

If I were to run this exact same command into your library, would the performance be the same or is it literally using bash?

How to catch all output and return code?

Assume I run a find command and its output shows found items and also some errors like this

find testdir/
testdir/
testdir/testfile
testdir/fstab
testdir/not_readable
find: ‘testdir/not_readable’: Permission denied

Here I like to catch

  • the found items
  • the errors
  • the return code

How can I do this? Thanks.

Command substitution workaround

Hi, i couldn't use <(cmd) is there any other workaround without doing something like:

cmd > tempfile
cmd2 tempfile
rm tempfile

nested `cd` does not work

This is the minimal example to reproduce the problmen

Cargo.toml

[dependencies]
cmd_lib = "1.3"

main.rs

use cmd_lib::run_cmd;
fn main() {
    run_cmd! {
        mkdir -p build;
        cd build;
        mkdir -p nested;
        cd nested;
    }.unwrap();
}

Run the above code gives the followin error

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Custom { kind: Other, error: "Running [\"cd\", \"nested\"] failed: cd nested: No such file or directory" }', src/main.rs:8:7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

How can one spawn a process without opening up a window?

I'm compiling my exe with #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
an issue I've noticed that whenever I trigger a new command it opens up a new window.

I've used Process before where I could've just pass a flag to creation_flags which indicates to not spawn a new window.

is there a solution for cmd_lib?

builtin/custom command may become blocking when pipe is full

Right now for builtin/custom commands, we just call the registered functions to get the results instead of spawning separate processes. However, when they are used in pipes, it could become blocking when the pipe is full:

code:

rust_cmd_lib git:(master) ✗ cat examples/one.rs 
use cmd_lib::*;

fn main() -> CmdResult {
    init_builtin_logger();
    use_builtin_cmd!(cat);    // remove this line, there will be no blocking
    let big_file_content = "a".repeat(64 * 4096 * 2);
    std::fs::write("/tmp/big_file", big_file_content)?;
    run_cmd!(cat /tmp/big_file | wc)?;
    Ok(())
}

output:

rust_cmd_lib git:(master) ✗ CMD_LIB_DEBUG=1 cargo run --example one
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/examples/one`
DEBUG - Running ["cat", "/tmp/big_file"] | ["wc"] ...
---> blocking here

We can fix the above bug by launching threads for each builtin/custom commands, however, it seems overkill for other builtin commands like echo/true/info/error/debug/..., and it would also complicate the current code. Need to figure out a way to solve this blocking issue more gracefully.

how to use vector of arguments in run_cmd?

this is more a question than an issue, but I'm trying to do something like:

run_cmd!(docker save $all_images -o images.tar)

where all_images is a Vec<String>.

I tried converting the vector to a single String, let all_images = all_images.join(" "); but the command is ran with docker save "image1 image2 image3" -o images.tar which makes docker think I'm saving only one image when in fact is multiple.
Using the Vector directly does not compile due to some trait bounds not being satisfied which I think I kind of understand but don't see how to overcome.

Any suggestions?

How is escaping handled?

Hi,

I was trying to figure out how escaping / splitting arguments is handled. Obviously in bash writing things like:

var="My Documents"; cd $var doesn't work, as var is split. How is this type of thing handled? I would discuss it somewhere (briefly) in the readme, as it's one of the things which annoys me most about bash

Build fails on arm-unknown-linux-gnueabihf (raspberry pi).

The dependency faccess fails to install due to an unmerged pull request that fixes a small issue.

Here's the error:

 --> /home/pi/.cargo/registry/src/github.com-1ecc6299db9ec823/faccess-0.2.3/src/lib.rs:95:36
   |
95 |             if faccessat(AT_FDCWD, path.as_ptr() as *const i8, mode, AT_EACCESS) == 0 {
   |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `u8`, found `i8`
   |
   = note: expected raw pointer `*const u8`
              found raw pointer `*const i8`

Would it be possible to change the dependency to:

faccess = { version="0.2.4", git="https://github.com/MightyPork/faccess" }

?

Build fails in Cargo workspace

Hi,

when using cmd_lib in a crate that is part of a cargo workspace it get's confused because of a path relative CARGO_MANIFEST_DIR in source_text.rs.

 let __st_file: Vec<char> = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", file!()))

I check version 0.8.3 and latest HEAD.

For reproducing create a cargo workspace with a containing crate that uses cmd lib (I used one if the examples.

error: couldn't read /Users/felix/tmp/ws/cmd_lib_workspace/cmd_lib_workspace/src/main.rs: No such file or directory (os error 2)
  --> cmd_lib_workspace/src/main.rs:8:5
   |
8  | /     run_cmd!{
9  | |         cd $dir;
10 | |         pwd;
11 | |         sleep $gap;
12 | |         cd $f;
13 | |     }
   | |_____^
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: couldn't read /Users/felix/tmp/ws/cmd_lib_workspace/cmd_lib_workspace/src/main.rs: No such file or directory (os error 2)
  --> cmd_lib_workspace/src/main.rs:17:5
   |
17 |     run_fun!(date +%Y)
   |     ^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: couldn't read /Users/felix/tmp/ws/cmd_lib_workspace/cmd_lib_workspace/src/main.rs: No such file or directory (os error 2)
  --> cmd_lib_workspace/src/main.rs:22:5
   |
22 |     run_cmd!(ls /tmp/nofile || true; echo "continue")?;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: couldn't read /Users/felix/tmp/ws/cmd_lib_workspace/cmd_lib_workspace/src/main.rs: No such file or directory (os error 2)
  --> cmd_lib_workspace/src/main.rs:23:5
   |
23 |     run_cmd!(cd /tmp; ls | wc -l;)?;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: couldn't read /Users/felix/tmp/ws/cmd_lib_workspace/cmd_lib_workspace/src/main.rs: No such file or directory (os error 2)
  --> cmd_lib_workspace/src/main.rs:24:5
   |
24 |     run_cmd!(pwd)?;
   |     ^^^^^^^^^^^^^
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: couldn't read /Users/felix/tmp/ws/cmd_lib_workspace/cmd_lib_workspace/src/main.rs: No such file or directory (os error 2)
  --> cmd_lib_workspace/src/main.rs:27:5
   |
27 |     run_cmd!(echo $name)?;
   |     ^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: couldn't read /Users/felix/tmp/ws/cmd_lib_workspace/cmd_lib_workspace/src/main.rs: No such file or directory (os error 2)
  --> cmd_lib_workspace/src/main.rs:28:5
   |
28 |     run_cmd!(|name| echo "hello, $name")?;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: couldn't read /Users/felix/tmp/ws/cmd_lib_workspace/cmd_lib_workspace/src/main.rs: No such file or directory (os error 2)
  --> cmd_lib_workspace/src/main.rs:29:5
   |
29 |     run_cmd!(du -ah . | sort -hr | head -n 5 | wc -w)?;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: couldn't read /Users/felix/tmp/ws/cmd_lib_workspace/cmd_lib_workspace/src/main.rs: No such file or directory (os error 2)
  --> cmd_lib_workspace/src/main.rs:31:18
   |
31 |     let result = run_fun!(du -ah . | sort -hr | head -n 5)?;
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0433]: failed to resolve: use of undeclared type or module `cmd_lib_core`
  --> cmd_lib_workspace/src/main.rs:8:5
   |
8  | /     run_cmd!{
9  | |         cd $dir;
10 | |         pwd;
11 | |         sleep $gap;
12 | |         cd $f;
13 | |     }
   | |_____^ use of undeclared type or module `cmd_lib_core`
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0433]: failed to resolve: use of undeclared type or module `cmd_lib_core`
  --> cmd_lib_workspace/src/main.rs:17:5
   |
17 |     run_fun!(date +%Y)
   |     ^^^^^^^^^^^^^^^^^^ use of undeclared type or module `cmd_lib_core`
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0433]: failed to resolve: use of undeclared type or module `cmd_lib_core`
  --> cmd_lib_workspace/src/main.rs:22:5
   |
22 |     run_cmd!(ls /tmp/nofile || true; echo "continue")?;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of undeclared type or module `cmd_lib_core`
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0433]: failed to resolve: use of undeclared type or module `cmd_lib_core`
  --> cmd_lib_workspace/src/main.rs:23:5
   |
23 |     run_cmd!(cd /tmp; ls | wc -l;)?;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of undeclared type or module `cmd_lib_core`
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0433]: failed to resolve: use of undeclared type or module `cmd_lib_core`
  --> cmd_lib_workspace/src/main.rs:24:5
   |
24 |     run_cmd!(pwd)?;
   |     ^^^^^^^^^^^^^ use of undeclared type or module `cmd_lib_core`
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0433]: failed to resolve: use of undeclared type or module `cmd_lib_core`
  --> cmd_lib_workspace/src/main.rs:27:5
   |
27 |     run_cmd!(echo $name)?;
   |     ^^^^^^^^^^^^^^^^^^^^ use of undeclared type or module `cmd_lib_core`
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0433]: failed to resolve: use of undeclared type or module `cmd_lib_core`
  --> cmd_lib_workspace/src/main.rs:28:5
   |
28 |     run_cmd!(|name| echo "hello, $name")?;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of undeclared type or module `cmd_lib_core`
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0433]: failed to resolve: use of undeclared type or module `cmd_lib_core`
  --> cmd_lib_workspace/src/main.rs:29:5
   |
29 |     run_cmd!(du -ah . | sort -hr | head -n 5 | wc -w)?;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of undeclared type or module `cmd_lib_core`
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0433]: failed to resolve: use of undeclared type or module `cmd_lib_core`
  --> cmd_lib_workspace/src/main.rs:31:18
   |
31 |     let result = run_fun!(du -ah . | sort -hr | head -n 5)?;
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of undeclared type or module `cmd_lib_core`
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 18 previous errors

For more information about this error, try `rustc --explain E0433`.
error: could not compile `cmd_lib_workspace`.

To learn more, run the command again with --verbose.

Unnecessary character escape occurs on dynamic input

OS: Windows (don't know if it's an issue on Linux)

Let's say my application accepts arbitrary commands dynamically via user input
I have the next:

        #[cfg(target_os = "windows")]
        let output = run_fun! (
            cmd /c $user_input
        );

The issue lies whenever that user input includes any thing escapable such as double quotes.
Let's say a user tries to run the next:

echo "Hello World"

After looking through proc explorer and exploring the source I've noticed that what's being passed to the command line looks as follow:

echo \"Hello World\"

As you can see there is the unnecessary escape of the double quotes, what's more interesting that through debugging the library's code I've found out that it escapes more than that, through this exact process:

  • echo "Hello World"
  • echo \"Hello World\"
  • echo \\\"Hello World\\\"

Now ... for this specific command it ain't that bad of an issue but where it comes to more sophisticated commands such as:

SCHTASKS /Create /TN "New Task" /SC HOURLY /TR xxx.exe /RL xxx

the issue arises in the parameter of the /TN flag, trying to run this specific command through:

        let input = r#"SCHTASKS /Create /TN "New Task" /SC HOURLY /TR blah.exe /RL HIGHEST"#

        #[cfg(target_os = "windows")]
        let output = run_fun! (
            cmd /c $input
        );

We get an error back because the quotes were escaped, instead of successfully creating the task/running the command at all, what we're getting back is an error:

ERROR: Invalid argument/option - 'Task"'.
Type "SCHTASKS /CREATE /?" for usage.

this happens because the command cmd_lib actually runs looks like:
SCHTASKS /Create /TN \"New Task\" /SC HOURLY /TR blah.exe /RL HIGHEST <- Notice the escaped double quotes

Is there anything we can do about it? I've tried to hack into the internals of the library with little to no success in that matter.
any help will be appreciated.

EDIT:
https://internals.rust-lang.org/t/std-process-on-windows-is-escaping-raw-literals-which-causes-problems-with-chaining-commands/8163/16?u=xpyder

https://users.rust-lang.org/t/std-process-is-escaping-a-raw-string-literal-when-i-dont-want-it-to/19441

Found the above threads, actually an issue which goes back to MS-DOS on windows, nothing you can really do about it :(

Idiomatic way to include optional arguments in command

I'd like to run a command / function where what is run is composed of some dynamic options passed in by the user. So I have a command and depending on whether or not the user passes some argument to my rust program, I'd like to run (via run_fun!) another external program either including or not this option. There are several such options, so enumerating all possibilities grows combinatorially.

I tried building a single command string, but realized the run_fun! macro won't tokenize that on spaces the way the subprocess needs. What is the recommended way to achieve this functionality in cmd_lib?

Thanks!
Rob

Stderr is unconditionally redirected

Description

Contents of stderr will always be sliently redirected and consumed.

Reproduction

cargo build outputs are printed to stderr, so we can use it as a demo:

run_cmd!(cargo build).unwrap();

Run it and nothing happens.

The Cause

In bd7837f, an attempt to fix logging for stderr:

rust_cmd_lib/src/process.rs

Lines 464 to 467 in f125db8

// set up error pipe
let (pipe_reader, pipe_writer) = os_pipe::pipe()?;
self.stderr_redirect = Some(CmdOut::Pipe(pipe_writer));
self.stderr_logging = Some(pipe_reader);

spawn with output

This is a very handy tool, finally I don't need to write bash.

It would be nice to have spawn pipe out stdout and stderr, at least provide a default option, just log it with a prefix.

ability to kill (long-running) pipeline

My use-case is that I have a long-running pipeline that runs on web request. If the connection is dropped before the processing is finished, the pipeline still continues to run until completion. If the handle returned from spawn!() had a kill() method on it, I could call it in the appropriate place for the web framework to stop the pipeline when the connection was dropped. I'm thinking something akin to std::process::Child::kill(), maybe on the CmdChildren struct.

Or is there maybe an alternative way I can achieve the same without having to implement new functionality?

Clash with shell environment variable syntax

Great project, looks very ergonomic to use!

Would you consider designating interpolation variables differently? $var_name already has a meaning and is very commonly used in shell scripts, and there may be times where the user wants to refer to external environment variables. There aren't many characters which don't already have a meaning, but even something like $$ would drastically reduce the number of potential clashes - it's used to get the PID but that's far less common than environment variable use.

run_cmd!{} alone with curly braces doesn't compile

This doesn't compile

fn main() -> std::io::Result<()> {
    run_cmd!{ls}?;
    Ok(())
}

compiles says

error: expected expression, found `?`
 --> src/main.rs:4:17
  |
4 |     run_cmd!{ls}?;
  |                 ^ expected expression

error[E0308]: mismatched types
 --> src/main.rs:4:5
  |
4 |     run_cmd!{ls}?;
  |     ^^^^^^^^^^^^ expected `()`, found enum `Result`
  |
  = note: expected unit type `()`
                  found enum `Result<(), std::io::Error>`

while this compiles fine

fn main() -> std::io::Result<()> {
    run_cmd!(ls)?;
    Ok(())
}

This with curly braces compiles too

fn main() -> std::io::Result<()> {
    let _ = run_cmd!{ls}?;
    Ok(())
}

is there any way to access Cmd or arguments from the result?

specially when commands fail, it would be good if it was possible to access the Cmd or the arguments from the error result.

For example, when running something arg1 arg2 fails, I see the following in the logs:

Running ["something", "arg1", "arg2"] exited with error; status code: 255

this is ok, but it would be great if we could customize this output or even access the commands and the arguments.
For example:

Running "something arg1 arg2" exited with error; status code: 255

so at least users can copy-paste the same command on the shell without having to delete all the extra quotes and array syntax.
Same thing applies to Ok results; perhaps I want to show something like Running "something arg1 arg2" succeeded in debug mode for example.

Problem with pipefail

I have this minimal example

fn main() -> CmdResult {
    cmd_lib::set_pipefail(true);
    println!("{}", run_fun!(du -ah . | sort -hr | head -n 10)?);
    Ok(())
}

and get

Error: Custom { kind: Other, error: "Running [\"sort\", \"-hr\"] exited with error; terminated by signal: 13" }

Running du -ah . | sort -hr | head -n 10 in the shell no matter if bash/zsh or fish, works (no suprise) fine.

Command never ends when being executed in cmd_lib::spawn_with_output

I have a ffmpeg command to process a video file.

ffmpeg -i 4.mp4 -i a2.png -ss 00:00:00 -to 00:01:00 -y -filter_complex "overlay=x=263:y=752" -c:a copy -max_muxing_queue_size 9999 -nostdin testtttt.mp4

It runs very well when being executed in a terminal. But when I run it with cmd_lib::spawn_with_output, it never ends and the program just stuck there. This is my program.

fn main() -> eyre::Result<()>{
    std::env::set_current_dir(r##"f:\temp"##);

    println!("{:?}", "Starting......");
    let output = cmd_lib::spawn_with_output!(
ffmpeg -i 4.mp4 -i a2.png -ss 00:00:00 -to 00:01:00 -y -filter_complex "overlay=x=263:y=752"  -c:a copy -max_muxing_queue_size 9999 -nostdin testtttt.mp4
    )?.wait_fun_result()?;
    println!("{:?}", "Done!");
    println!("{:?}", output);
    println!("{:?}", output.trim());
    Ok(())
}

Is there something I'm doing wrong? Thanks.

Support expressions in string template parameters

I've tried ${variable_name} inside run_cmd!, that works fine.
Unfortunately ${foo.bar()} produces an error.
It's easy to work around this by setting a variable to the expression and using the variable as a template parameter.
However, this reduces the effectiveness of the macro in making the code neater.

So, be great if this could be added. Thanks for the library.

Iterate over options

Let's suppose we have a command named x and think about this code.

let options = vec!["-a -b 1", "-c 2"];
for option in options {
  run_cmd!(x $option).unwrap();
}

doesn't recognize each option correctly but handle as a atomic variable.

How can I do this with this library?

Strange behavior with current_dir().

I'm trying to extract some payload from a macOS package in a build.rs.
This is located in ./target/3Delight/3delight.pkg.
So if I do this in my shell:

cd target/3Delight/3delight.pkg; cat Payload | gunzip -dc | cpio -i

I end up with the Payload unpacked in ./target/3Delight/3delight.pkg.

But if I run below Rust code, my Payload is unpacked in ., not in ./target/3Delight/3delight.pkg.

I do not understand this. cat Payload will only work from ./target/3Delight/3delight.pkg. How is it possible that the output ends up three levels above?

Process::new("cat Payload")
    .pipe("gunzip -dc")
    .pipe("cpio -i")
    .current_dir(extract_path.to_str().unwrap())
    .wait::<CmdResult>().expect("Could no extract payload from package.");

Output:

INFO: Running "cat Payload | gunzip -dc | cpio -i (cd: /Users/moritz/code/bindit/target/3Delight/3delight.pkg)"

How would I pipe commands with no hardcoding of command strings?

Hi, I would like to know how you can combine commands without hardcoding the actual cmd in the macro?

Not sure how escaping works in these cases.

Rough Example:

        let post_cmd  = "grep 'blah' | another_cmd"
        let output_raw = self.someVarEval();

        let data = run_fun!(echo "$output_raw" | $post_cmd );

When I run the above in full code. I get the following output error:

No such file or directory (os error 2)

spawn: doesn't appear as though file descriptors are inherited

I tried a long running command, e.g:

spawn!(docker build -f ${tmp_docker_file} -t ${tag} ${tmp_path_sub})?.wait_cmd_result()?;

But output blocked until the command finished execution successfully.
Parent command executed in a terminal.

Consistently, I don't see Stdio::inherit() being used the code of rust_cmd_lib.

The alternative to use the standard API worked for me:

use std::process::{Stdio, Command};   
let mut cmd = Command::new("docker"); 
cmd.stdout(Stdio::inherit());         
cmd.stderr(Stdio::inherit());         
cmd.stdin(Stdio::inherit());          
cmd.arg("build");                     
cmd.arg("-f");                        
cmd.arg(tmp_docker_file);             
cmd.arg("-t");                        
cmd.arg(tag);                         
cmd.arg(tmp_path_sub);                
let mut child = cmd.spawn()?;         
child.wait()?;

output stderr only to logs, not to stderr of process

Hi, first off this is a great library that's helping me a lot.

I would ideally like a way to "capture" stderr such that it is logged but not sent to the stderr of the process running the rust code. Is this possible? I'd also like to do the same for stdout.

同步执行命令?

使用run_cmd进入指定工作目录下执行gradle命令,期待.gradlew命令执行结束后下述方法才返回.但我发现首先输出build failed错误日志,然后才./gradlew :assembleDebug才开始执行.有什么方式能同步执行?
example:
if run_cmd! {
cd /Users/person/app;
./gradlew :assembleDebug
}
.is_err()
{
eprintln!("build failed");
}

Does not compile

error[E0554]: `#![feature]` may not be used on the stable release channel
 --> /home/matteo/.cargo/registry/src/github.com-1ecc6299db9ec823/cmd_lib_macros-0.1.0/src/lib.rs:1:1
  |
1 | #![feature(proc_macro_span)]
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^


Inconsistent behavior

I'm using version 1.2.2 and I have the following program.

fn main() -> eyre::Result<()> {
    std::env::set_current_dir(r##"f:\temp"##);
    let output = cmd_lib::spawn_with_output!(ls -la)?.wait_fun_result()?;

    println!("{:?}", output);
    Ok(())
}

When I run it, I can get the expected result like:

"total 5996106\ndrwxr-xr-x 1 admin None         0 Sep 25 16:26 .\n..............................."

When I change the command ls -la to more complex one (which probably takes several seconds to complete), I get empty output.

fn main() -> eyre::Result<()> {
    std::env::set_current_dir(r##"f:\temp"##);
    let output = cmd_lib::spawn_with_output!(
        ffmpeg -i 1.mp4 -i a2.png -y -filter_complex "overlay=x=263:y=752"  -c:a copy -max_muxing_queue_size 99999 -y test.mp4
    )?.wait_fun_result()?;

    println!("{:?}", output);
    Ok(())
}

Output is:

""

But can I find the produced output file of the ffmpeg command, which indicates the command did run. If I enabled the logger like this:

use cmd_lib::init_builtin_logger;
fn main() -> eyre::Result<()> {
    init_builtin_logger();
    std::env::set_current_dir(r##"f:\temp"##);
    let output = cmd_lib::spawn_with_output!(
        ffmpeg -i 1.mp4 -i a2.png -ss 00:00:00 -to 00:01:00 -y -filter_complex "overlay=x=263:y=752"  -c:a copy -max_muxing_queue_size 99999 -y testtttt.mp4
    )?.wait_fun_result()?;
    println!("{:?}", output);
    Ok(())
}

I can get the ffmpeg output in the console:

INFO - ffmpeg version N-103679-g7bbad32d5a-20210918 Copyright (c) 2000-2021 the FFmpeg developers
INFO -   built with gcc 10-win32 (GCC) 20210408
INFO -   configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-
...
INFO - [libx264 @ 0000010cf15da6c0] ref B L1: 97.3%  2.7%
INFO - [libx264 @ 0000010cf15da6c0] kb/s:134.12
""

But note the "" at the end of the output, I still can't get the output in the variable output and it still contains an empty string.

Why for simple command, the output is captured in a variable. But for complex command, it doesn't do this. Is this inconsistence a bug? How can I store the command output in a variable when the command is kinda complex? Thank.

Possible examples of testing / mocking of outputs

Hello, love the library.

I am looking to see if there are examples available for testing / mocking the outputs. I have tried a number of methodologies to mock the output. I hope that I am able to test a number of scenarios.

Support unix exec for replacing current process

It would be handy to be able to use Command::exec() with a convenient macro. I commonly use this when I need to transform the current process entirely into another process without creating a new subprocess. This can be particularly useful for creating wrappers around other programs or for use in certain types of daemon processes.

For example:

use std::os::unix::process::CommandExt;
use std::process::Command;

fn main() {
    let error = Command::new("ls")
        .arg("-l")
        .arg("/home")
        .exec();  // Replaces the current process with `ls`

    eprintln!("Failed to execute ls: {}", error);
}

Does not work on stable anymore?

error[E0554]: `#![feature]` may not be used on the stable release channel
 --> /home/jokler/.local/share/cargo/registry/src/github.com-1ecc6299db9ec823/cmd_lib_macros-0.1.0/src/lib.rs:1:1
  |
1 | #![feature(proc_macro_span)]
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0554`.
error: could not compile `cmd_lib_macros`.

Feature request: Allow cd-ing to a path

It would be convenient if this was valid

let dir = Path::from(...);
run_cmd! {
    cd $dir;
    rm foo;
}

the workaround is fine, but mildly less convenient

let dir = Path::from(...);
let dir_s = dir.to_string();
run_cmd! {
    cd $dir_s;
    rm foo;
}

How do I get the return code of the command?

I tried this but it did not work:

    if run_fun!(rg --quiet -F "LayoutTU.S." $HOME/Library/Preferences/com.apple.HIToolbox.plist).is_err() {
        println!("🇮🇷" )
    } else {
        println!("🇺🇸" )
    }

How to support the glob pattern?

pub fn build() {
  println!("[PACK] running...");
  run_cmd! (
    rm -rf "dist";
    mkdir -p "dist/wasm";
    cp "target/project/*.wasm" "dist/wasm"; // glob: *.js
  )
  .unwrap();
}

this code doesn't work...

Behavior of semi-colon in macros is confusing

In a shell script, a semi-colon indicates that the next command should run regardless of if the preceding command succeeds. However, in run_cmd!, a semi-colon behaves like && instead. Ideally, both && and ; would be supported and behave like they do in a shell command, or if that's not possible, the README would make it extra clear how it differs.

dynamic redirection and piping

i have a string literal which contains a pipe (|) and/or and angle bracket (e.g. >). i'd like this to work just like in bash. is this possible somehow? i've read the section on using vectors for dynamic parameters. see discussion on discourse. something like this:

    let args = shell_words::split("echo hi > echo.out").unwrap();
    let child = spawn!($[args]).unwrap();

Prevent stripping quotes?

Is there a way to keep quotes intact when passing a series of commands to
run_cmd!?

I'm trying to do the following:

let msg: &str = "update the repository";
let _ = run_cmd!("git commit -am \"{}\"; git push origin master", msg);

Here I'd like to keep the msg within quotes when passed to the git commit
command, but run_cmd strips the quotes. Any guidance on this would be appreciated.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.