Skip to main content

Crate fun_run

Crate fun_run 

Source
Expand description

§Fun Run

What does the “Zombie Zoom 5K”, the “Wibbly wobbly log jog”, and the “Turkey Trot” have in common? They’re runs with a fun name! The fun_run library adds display and safety features to make running a Rust Command better for you and your users.

Stream the command and raise on non-zero exit:

use fun_run::CommandWithName;
use std::process::Command;

let mut cmd = Command::new("bash");
cmd.args(["-c", "echo -n oops all berries; exit 1"]);

// Advertise the command being run before execution
println!("Running `{name}`", name = cmd.name());

// Stream output to the end user
// Turn non-zero status results into an error
let error = cmd
    .stream_output(std::io::stdout(), std::io::stderr())
    .unwrap_err();

assert_eq!(
    indoc::indoc!{r#"
        Command failed `bash -c "echo -n oops all berries; exit 1"`
        exit status: 1
        stdout: <see above>
        stderr: <see above>
    "#}.trim().to_string(),
    error.to_string()
);

Run the command quietly, capture stdout/stderr and raise on non-zero exit:

let error = cmd.named_output().unwrap_err();
assert_eq!(
    indoc::indoc!{r#"
        Command failed `bash -c "echo -n oops all berries; exit 1"`
        exit status: 1
        stdout: oops all berries
        stderr: <empty>
    "#}.trim().to_string(),
    error.to_string()
);

Output of the command is preserved in success and error cases:

// Both Ok and Err from result store the output for inspection
assert!(
    error.output().unwrap().stdout_lossy()
    .contains("oops all berries")
);

§Install

$ cargo add fun_run

§Renaming a command

If you need to provide an alternate display for your command you can rename it, this is useful for omitting implementation details.

use fun_run::CommandWithName;
use std::process::Command;

let mut cmd = Command::new("bash");
cmd
    .args(["-eo", "pipefail", "-c"])
    .arg("echo -n 'hello world' && exit 1");

let mut renamed_cmd = cmd.named("echo 'hello world'");

assert_eq!("echo 'hello world'", &renamed_cmd.name());

This is also useful for adding additional information, such as environment variables:

use fun_run::CommandWithName;
use std::process::Command;

let mut cmd = Command::new("bundle");
cmd.arg("install");

let env_vars = std::env::vars();

let mut renamed_cmd = cmd.named_fn(|cmd| fun_run::display_with_env_keys(
    cmd,
    env_vars,
    ["RAILS_ENV"]
));

assert_eq!(r#"RAILS_ENV="production" bundle install"#, renamed_cmd.name())

§What won’t it do?

The fun_run library doesn’t support executing a Command in ways that do not produce an Output, for example calling Command::spawn returns a std::process::Child (Which doesn’t contain an Output). If you want to run-for-fun in the background, spawn a thread and join it manually:

use fun_run::CommandWithName;
use std::process::Command;
use std::thread;

let mut cmd = Command::new("bundle");
cmd.args(["install"]);

// Advertise the command being run before execution
println!("Quietly Running `{name}` in the background", name = cmd.name());

let result = thread::spawn(move || {
    cmd.named_output()
}).join().unwrap();

// Command name is persisted on success or failure
match result {
    Ok(output) => {
        assert_eq!("bundle install", &output.name())
    },
    Err(cmd_error) => {
        assert_eq!("bundle install", &cmd_error.name())
    }
}

§Async

This library uses synchronous command execution. If you’re using this library in an async context, you’ll want to use an async wrapper like tokio::task::spawn_blocking.

§Clippy

To ensure all commands have their exit status checked you can add this to your clippy.toml to prevent accidentally spawning an un-checked plain Command:

[[disallowed-methods]]
path = "std::process::Command::output"
reason = "Use fun_run::CommandWithName::named_output"

[[disallowed-methods]]
path = "std::process::Command::status"
reason = "Use fun_run::CommandWithName::named_output and read the status from the result"

[[disallowed-methods]]
path = "std::process::Command::spawn"
reason = "Use fun_run::CommandWithName::stream_output(std::io::stdout(), std::io::stderr())"

§Debugging system failures with which_problem

When a command execution returns an Err due to a system error (and not because the program it executed launched but returned non-zero status), it’s usually because the executable couldn’t be found, or if it was found, it couldn’t be launched, for example due to a permissions error. The which_problem crate is designed to add debugging errors to help you identify why the command couldn’t be launched.

The crate which_problem works like which but helps you identify common mistakes such as typos:

$ cargo whichp zuby
Program "zuby" not found

Info: No other executables with the same name are found on the PATH

Info: These executables have the closest spelling to "zuby" but did not match:
      "hub", "ruby", "subl"

Fun run supports which_problem integration through the which_problem feature. In your Cargo.toml:

# Cargo.toml
fun_run = { version = <version.here>, features = ["which_problem"] }

And annotate errors:

#[cfg(not(feature = "which_problem"))] { return; }
use fun_run::CommandWithName;
use std::process::Command;

let mut cmd = Command::new("becho");
cmd.args(["hello", "world"]);

#[cfg(feature = "which_problem")]
cmd.stream_output(std::io::stdout(), std::io::stderr())
    .map_err(|error| fun_run::map_which_problem(error, cmd.mut_cmd(), std::env::var_os("PATH"))).unwrap();

Now if the system cannot find a becho program on your system the output will give you all the info you need to diagnose the underlying issue.

Note that which_problem integration is not enabled by default because it outputs information about the contents of your disk such as layout and file permissions.

§Nightly-only items

A few items (display_env_vars and CommandWithName::named_env_vars) require a nightly toolchain. They depend on the unstable command_resolved_envs feature, auto-detected at build time, and are absent on stable. Because https://docs.rs builds on nightly, these appear in the published docs even though stable users cannot use them.

Structs§

NamedCommand
It’s a command, with a name
NamedOutput
Holds an Output of a command’s execution along with its “name”

Enums§

CmdError
Who says (Command) errors can’t be fun?

Traits§

CommandWithName
CommandWithName trait
ExitStatusFromCode
Extension trait for ExitStatus to build an instance based on exit code
OutputWithName
Extension trait for Output to generate NamedOutput

Functions§

display
Converts a command and its arguments into a user readable string
display_env_vars
Converts a command, and specified environment variables to user readable string
display_with_env_keys
Converts a command, arguments, and specified environment variables to user readable string
nonzero_captured
Converts an Output into an error when status is non-zero
nonzero_streamed
Converts an Output into an error when status is non-zero
on_system_error
Converts a std::io::Error into a CmdError which includes the formatted command name