Crates.io | fun_run |
lib.rs | fun_run |
version | 0.2.0 |
source | src |
created_at | 2023-11-01 16:20:57.25613 |
updated_at | 2024-06-25 22:58:49.934473 |
description | The fun way to run your Rust Comand |
homepage | |
repository | https://github.com/schneems/fun_run |
max_upload_size | |
id | 1021277 |
size | 33,885 |
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! That's exactly what fun_run
does. It makes running your Rust Command
s more fun, by naming them.
Fun run is designed for the use case where not only do you want to run a Command
you want to output what you're running and what happened. Building a CLI tool is a great use case. Another is creating a buildpack.
Here's some things you can do with fun_run:
Just like you don't need to dress up in a giant turkey costume to run a 5K you also don't need fun_run
to do these things. Though, unlike the turkey costume, using fun_run
will also make the experience easier.
For a quick and easy fun run you can use the fun_run::CommandWithName
trait extension to stream output:
use fun_run::CommandWithName;
use std::process::Command;
let mut cmd = Command::new("bundle");
cmd.args(["install"]);
// 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 result = cmd
.stream_output(std::io::stdout(), std::io::stderr());
// 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())
}
}
Fun run comes with nice errors by default:
use fun_run::CommandWithName;
use std::process::Command;
let mut cmd = Command::new("becho");
cmd.args(["hello", "world"]);
match cmd.stream_output(std::io::stdout(), std::io::stderr()) {
Ok(_) => todo!(),
Err(cmd_error) => {
let expected = r#"Could not run command `becho hello world`. No such file or directory"#;
let actual = cmd_error.to_string();
assert!(actual.contains(expected), "Expected {actual:?} to contain {expected:?}, but it did not")
}
}
And commands that don't return an exit code 0 return an Err so you don't accidentally ignore a failure, and the output of the command is captured:
use fun_run::CommandWithName;
use std::process::Command;
let mut cmd = Command::new("bash");
cmd.arg("-c");
cmd.arg("echo -n 'hello world' && exit 1");
// Quietly gets output
match cmd.named_output() {
Ok(_) => todo!(),
Err(cmd_error) => {
let expected = r#"
Command failed `bash -c "echo -n 'hello world' && exit 1"`
exit status: 1
stdout: hello world
stderr: <empty>
"#;
let actual = cmd_error.to_string();
assert!(
actual.trim().contains(expected.trim()),
"Expected {:?} to contain {:?}, but it did not", actual.trim(), expected.trim()
)
}
}
By default, streamed output won't duplicated in error messages (but is still there if you want to inspect it in your program):
use fun_run::CommandWithName;
use std::process::Command;
let mut cmd = Command::new("bash");
cmd.arg("-c");
cmd.arg("echo -n 'hello world' && exit 1");
// Quietly gets output
match cmd.stream_output(std::io::stdout(), std::io::stderr()) {
Ok(_) => todo!(),
Err(cmd_error) => {
let expected = r#"
Command failed `bash -c "echo -n 'hello world' && exit 1"`
exit status: 1
stdout: <see above>
stderr: <see above>
"#;
let actual = cmd_error.to_string();
assert!(
actual.trim().contains(expected.trim()),
"Expected {:?} to contain {:?}, but it did not", actual.trim(), expected.trim()
);
let named_output: fun_run::NamedOutput = cmd_error.into();
assert_eq!(
"hello world",
named_output.stdout_lossy().trim()
);
assert_eq!(
"bash -c \"echo -n 'hello world' && exit 1\"",
named_output.name()
);
}
}
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.arg("-c");
cmd.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 env_vars = std::collections::HashMap::<String, String>::new();
# env_vars.insert("RAILS_ENV".to_string(), "production".to_string());
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())
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 debuggin errors to help you identify why the command couldn't be launched.
The name which_problem
works like which
to 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:
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.
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 Result<std::process::Child, std::io::Error>
(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())
}
}
If you don't want to use the trait, you can still use fun_run
by functionally mapping the features you want:
let mut cmd = std::process::Command::new("bundle");
cmd.args(["install"]);
let name = fun_run::display(&mut cmd);
cmd.output()
.map_err(|error| fun_run::on_system_error(name.clone(), error))
.and_then(|output| fun_run::nonzero_captured(name.clone(), output))
.unwrap();
Here's some fun functions you can use to help you run:
on_system_error
] - Convert std::io::Error
into CmdError
nonzero_streamed
] - Produces a NamedOutput
from Output
that has already been streamd to the usernonzero_captured
] - Like nonzero_streamed
but for when the user hasn't already seen the outputdisplay
] - Converts an &mut Command
into a human readable stringdisplay_with_env_keys
] - Like display
but selectively shows environment variables.