#script #exec #sh-exec

build sh-exec

Set of functions and macros to write more concise Rust scripts

5 releases

Uses new Rust 2024

new 0.1.6 Apr 18, 2025
0.1.5 Apr 18, 2025
0.1.4 Mar 16, 2025
0.1.3 Mar 15, 2025
0.1.2 Mar 15, 2025

#212 in Build Utils

Download history 323/week @ 2025-03-15 26/week @ 2025-03-22

349 downloads per month

Unlicense

22KB
258 lines

shell-exec

This Rust crate simplifies the execution of CLI programs by a Rust programs. It exports two macros: s! and exec!. In general, you want to use macro s! to execute shell commands. We export exec! for backward compatibility with some existing code.

Macro exec!

Macro exec! takes three agruments: exec!(error_id, verbose, cmd). Argument error_id is just a unique string literal that is printed in case of an error. I typically generate this string literal as follows:

echo "\"$RANDOM-$RANDOM-$RANDOM\", "

and paste the output as the first argument of macro exec! (or, even better the first argument of s!). This simplifies finding the code that issued a certain error message.

Argument verbose must be of type bool. If it is true, the command that will be executed is first printed to stderr.

On success, exec! returns the stdout of the executed command. If the execution fails, the macro returns an Err of type ShellError. One can handle the errors using the question mark operator:

    exec!("10874-26631-30577", false, "ls")?`

The cmd argument is a format sting, i.e., one can use positional and named arguments as well as variable names:

    let path="/tmp";
    exec!("17068-22053-696", "ls {path}")`

As for macro format!, macro exec! supports positional arguments:

    // example: with position argument "/"
    println!("ls of {path} is {}", exec!("15911-12192-19189", false, "ls {}", "/")?);

exec! also supports named arguments:

    // example: with named argument p="/tmp"
    println!("ls of {path} is {}", exec!("15911-12192-19189", false, "ls {p}", p="/tmp")?);

Macro s!

Macro s! is similar to macro exec!: s! uses crate log to issue log output instead of eprintln!. Hence, it does not have a flag verbose. Moreover, it

  • logs the executed command with all arguments at info level,
  • logs the output, i.e., the stdout of the command, at debug level, and
  • logs errors, i.e., the stderr of the command, at error level.

On success of the executed command, sh! returns the stdout of the executed command wrapped in Ok(stdout).

Example

You can use macro s! as follows:

        // s! the command is executed and the output is returned
        // s! uses the logger to print the command if the log level is set to info
        // s! uses the logger to print the output of the command if the log level is set to debug
        s!("14526-30026-17058", "echo Hello World")?;

On error, s!, logs an error that includes:

  • the command line that failed,
  • the error ID,
  • the stdout,
  • the stderr,

You can combine macro s! with crate anyhow to add more context to error messages.


use sh_exec::*;
// show how to use together with anyhow
use anyhow::*;

fn main() -> Result<()> {
        env_logger::init();

        // example: ls of /tmp
        let path="/etc";
        println!("- ls of {path} is {}", exec!("17068-22053-696", true, "ls {path}").with_context(|| format!("Very unexpected - ls failed on {path}"))?);

        // example: with position argument "/"
        println!("ls of {path} is {}", s!("15911-12192-19189",  "ls {}", "/").with_context(|| "Very unexpected - ls failed on /".to_string())?);

        // Explicit error handling
        match s!("28328-2323-3278", "nonexistent_command").with_context(|| "Failed to execute command 'nonexistent_command'".to_string()) {
            std::result::Result::Ok(output) => println!("Unexpected success: {}", output),
            Err(e) => println!("Expected error: {}", e),
        }

        Ok(())
    }

Macro a!

Macro a! is similar to macro s! but it has a timeout argument. If the command does not finish in time, a timeout is returned.

Example:

        // macro a! provides timeouts and it will return with a Timeout error 
        // if the command does not finish in time
        let ten_secs = time::Duration::from_secs(3);

        println!("sleep = {:?}", a!("14526-30888026-777", ten_secs, "sleep 2; echo Hello World"));

rust-script Example

Here is a simple program that uses this crate. Note that you need to define dependency sh-exec to import this crate and additionally dependencies colored, and log in your Cargo.toml:

[dependencies]
sh-exec = "*"
colored = "*"
log =  "*"

and in your Rust program, you import the macros as follows:

use sh_exec::*;

You can use this crate also from within Rust scripts:

#!/usr/bin/env rust-script
//! ```cargo
//! [package]
//! name = "example"
//! edition = "2024"
//!
//! [dependencies]
//! sh-exec = "*"
//! colored = "*"
//! log = "*"
//! ```

use sh_exec::*;

fn main() {
    trap_panics_and_errors!("18428-30925-25863", || {

        // example: ls of /tmp
        let path="/etc";
        exec!("17068-22053-696", true, "ls -d {path}")?;

        // example: with position argument "/"
        println!("ls -d of / is {}", exec!("15911-12192-19189", false,  "ls -d {}", "/")?);

        // example: with named argument p="/tmp"
        println!("ls of /etc/hosts is {}", s!("15911-12192-19189", "ls {p}", p="/etc/hosts")?);

        // Test successful command
        let output = exec!("28328-2323-44343", true, "bash -c 'echo Hello World'")?;
        println!("Output: {}", output);

        // Test failing command
        match exec!("28328-2323-3278", true, "nonexistent_command") {
            Ok(output) => println!("Unexpected success: {}", output),
            Err(e) => println!("Expected error: {}", e),
        }
        // expecting to fail:
        exec!( "28328-2323-333", true,  "nonexistent_command arg1 arg2")?;

        // We need to help Rust regarding the error type
        Ok::<(), Box<dyn Error>>(())
    });
}

Executing the code as a rust-script (see file example.rs), we get the following output:

$ ./example.rs
exec!(17068-22053-696,ls -d /etc)
ls -d of / is /

ls of /etc/hosts is /etc/hosts

exec!(28328-2323-44343,bash -c 'echo Hello World')
Output: Hello World

exec!(28328-2323-3278,nonexistent_command)
Expected error: Command failed: 'nonexistent_command'
sh_exec Exit code: 127
sh_exec Error ID:  28328-2323-3278
Standard error:
sh: 1: nonexistent_command: not found


exec!(28328-2323-333,nonexistent_command arg1 arg2)

Dependencies

~5–14MB
~193K SLoC