Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update docs #7

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
198 changes: 183 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <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()
);
}
}
```

## 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::<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())
```

## 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 = <version.here>, 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<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:

```no_run
Expand Down Expand Up @@ -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.
97 changes: 2 additions & 95 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![doc = include_str!("../README.md")]

use command::output_and_write_streams;
use lazy_static::lazy_static;
use std::ffi::OsString;
Expand All @@ -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<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:
///
/// ```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:
///
Expand Down Expand Up @@ -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"));
}
Loading