From 2ca562befc3a0ad6ad6c68c4037601404dcf6655 Mon Sep 17 00:00:00 2001 From: Schneems Date: Wed, 19 Jun 2024 12:00:54 -0500 Subject: [PATCH] Update docs Added more examples to the README and re-using the readme in the crates module docs. --- CHANGELOG.md | 4 ++ Cargo.toml | 2 +- README.md | 198 +++++++++++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 97 +------------------------ 4 files changed, 190 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 785925f..e5eb0c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +## 0.1.3 + +- Update docs on crates.io (https://github.com/schneems/fun_run/pull/7) + ## 0.1.2 - Add metadata for crates.io (https://github.com/schneems/fun_run/pull/5) diff --git a/Cargo.toml b/Cargo.toml index 5c7bca6..4204fed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fun_run" -version = "0.1.2" +version = "0.1.3" edition = "2021" license = "MIT" description = "The fun way to run your Rust Comand" diff --git a/README.md b/README.md index ca16489..a34d9d3 100644 --- a/README.md +++ b/README.md @@ -47,33 +47,177 @@ match result { } ``` -Or capture output without streaming: +## Pretty (good) errors -```no_run +Fun run comes with nice errors by default: + +``` use fun_run::CommandWithName; use std::process::Command; -let mut cmd = Command::new("bundle"); -cmd.args(["install"]); +let mut cmd = Command::new("becho"); +cmd.args(["hello", "world"]); -// Advertise the command being run before execution -println!("Quietly Running `{name}`", name = cmd.name()); +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") + } +} +``` -// Don't stream -// Turn non-zero status results into an error -let result = cmd.named_output(); +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: -// Command name is persisted on success or failure -match result { - Ok(output) => { - assert_eq!("bundle install", &output.name()) - }, +``` +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) => { - assert_eq!("bundle install", &cmd_error.name()) + let expected = r#" +Command failed `bash -c "echo -n 'hello world' && exit 1"` +exit status: 1 +stdout: hello world +stderr: + "#; + + 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: +stderr: + "#; + 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() + ); + } +} +``` + +## Renaming + +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::::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()) +``` + +## 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](https://github.com/schneems/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: + +```shell +$ 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`: + +```toml +# Cargo.toml +fun_run = { version = , features = ["which_problem"] } +``` + +And annotate errors: + +```no_run +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. + +## 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 `Result` (Which doesn't contain an `Output`). If you want to run for fun in the background, spawn a thread and join it manually: ```no_run @@ -102,3 +246,27 @@ match result { } } ``` + +## FUN(ctional) + +If you don't want to use the trait, you can still use `fun_run` by functionally mapping the features you want: + +```no_run +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 user +- [`nonzero_captured`] - Like `nonzero_streamed` but for when the user hasn't already seen the output +- [`display`] - Converts an `&mut Command` into a human readable string +- [`display_with_env_keys`] - Like `display` but selectively shows environment variables. diff --git a/src/lib.rs b/src/lib.rs index 8a3f04d..abb6eaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![doc = include_str!("../README.md")] + use command::output_and_write_streams; use lazy_static::lazy_static; use std::ffi::OsString; @@ -12,89 +14,6 @@ use which_problem::Which; mod command; -/// For a quick and easy fun run you can use the `fun_run::CommandWithName` trait extension to stream output: -/// -/// ```no_run -/// 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()) -/// } -/// } -/// ``` -/// -/// Or capture output without streaming: -/// -/// ```no_run -/// 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!("Quietly Running `{name}`", name = cmd.name()); -/// -/// // Don't stream -/// // Turn non-zero status results into an error -/// let result = cmd.named_output(); -/// -/// // 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()) -/// } -/// } -/// ``` -/// -/// 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` (Which doesn't contain an `Output`). If you want to run for fun in the background, spawn a thread and join it manually: -/// -/// ```no_run -/// 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()) -/// } -/// } -/// ``` /// /// Rename your commands: /// @@ -668,15 +587,3 @@ impl std::error::Error for IoErrorAnnotation { Some(&self.source) } } - -#[cfg(doctest)] -mod test_readme { - macro_rules! external_doc_test { - ($x:expr) => { - #[doc = $x] - extern "C" {} - }; - } - - external_doc_test!(include_str!("../README.md")); -}