-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 8f49903
Showing
8 changed files
with
965 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
name: CI | ||
|
||
on: | ||
push: | ||
# Avoid duplicate builds on PRs. | ||
branches: | ||
- main | ||
pull_request: | ||
|
||
jobs: | ||
lint: | ||
runs-on: ubuntu-22.04 | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Update Rust toolchain | ||
# Most of the time this will be a no-op, since GitHub releases new images every week | ||
# which include the latest stable release of Rust, Rustup, Clippy and rustfmt. | ||
run: rustup update | ||
- name: Rust Cache | ||
uses: Swatinem/[email protected] | ||
- name: Clippy | ||
# Using --all-targets so tests are checked and --deny to fail on warnings. | ||
# Not using --locked here and below since Cargo.lock is in .gitignore. | ||
run: cargo clippy --all-targets --all-features -- --deny warnings | ||
- name: rustfmt | ||
run: cargo fmt -- --check | ||
- name: Check docs | ||
# Using RUSTDOCFLAGS until `cargo doc --check` is stabilised: | ||
# https://github.com/rust-lang/cargo/issues/10025 | ||
run: RUSTDOCFLAGS="-D warnings" cargo doc --all-features --document-private-items --no-deps | ||
|
||
unit-test: | ||
runs-on: ubuntu-22.04 | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Update Rust toolchain | ||
run: rustup update | ||
- name: Rust Cache | ||
uses: Swatinem/[email protected] | ||
- name: Run unit tests | ||
run: cargo test --all-features |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/target | ||
/Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
## Unreleased | ||
|
||
## 0.1.0 | ||
|
||
- First release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
[package] | ||
name = "fun_run" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
lazy_static = "1" | ||
which_problem = { version = "0.1", optional = true } | ||
regex = "1" | ||
|
||
[features] | ||
which_problem = ["dep:which_problem"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
The MIT License (MIT) | ||
Copyright © 2023 Richard Schneeman | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# 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! That's exactly what `fun_run` does. It makes running your Rust `Command`s more fun, by naming them. | ||
|
||
## What is Fun Run? | ||
|
||
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](https://github.com/heroku/buildpacks-ruby/tree/4f514f6046568ada523eefd41b3024f86f1c67ce). | ||
|
||
Here's some things you can do with fun_run: | ||
|
||
- Advertise the command being run before execution | ||
- Customize how commands are displayed | ||
- Return error messages with the command name. | ||
- Turn non-zero status results into an error | ||
- Embed stdout and stderr into errors (when not streamed) | ||
- Store stdout and stderr for debug and diagnosis without displaying them (when streamed) | ||
|
||
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. | ||
|
||
## Ready to Roll | ||
|
||
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()) | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
use std::io::Write; | ||
use std::process::Command; | ||
use std::{io, process, thread}; | ||
use std::{mem, panic}; | ||
|
||
pub(crate) fn output_and_write_streams<OW: Write + Send, EW: Write + Send>( | ||
command: &mut Command, | ||
stdout_write: OW, | ||
stderr_write: EW, | ||
) -> io::Result<process::Output> { | ||
let mut stdout_buffer = Vec::new(); | ||
let mut stderr_buffer = Vec::new(); | ||
|
||
let mut stdout = tee(&mut stdout_buffer, stdout_write); | ||
let mut stderr = tee(&mut stderr_buffer, stderr_write); | ||
|
||
let mut child = command | ||
.stdout(process::Stdio::piped()) | ||
.stderr(process::Stdio::piped()) | ||
.spawn()?; | ||
|
||
thread::scope(|scope| { | ||
let stdout_thread = mem::take(&mut child.stdout).map(|mut child_stdout| { | ||
scope.spawn(move || std::io::copy(&mut child_stdout, &mut stdout)) | ||
}); | ||
let stderr_thread = mem::take(&mut child.stdout).map(|mut child_stderr| { | ||
scope.spawn(move || std::io::copy(&mut child_stderr, &mut stderr)) | ||
}); | ||
|
||
stdout_thread | ||
.map_or_else( | ||
|| Ok(0), | ||
|handle| match handle.join() { | ||
Ok(value) => value, | ||
Err(err) => panic::resume_unwind(err), | ||
}, | ||
) | ||
.and({ | ||
stderr_thread.map_or_else( | ||
|| Ok(0), | ||
|handle| match handle.join() { | ||
Ok(value) => value, | ||
Err(err) => panic::resume_unwind(err), | ||
}, | ||
) | ||
}) | ||
.and_then(|_| child.wait()) | ||
}) | ||
.map(|status| process::Output { | ||
status, | ||
stdout: stdout_buffer, | ||
stderr: stderr_buffer, | ||
}) | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
use std::process::Command; | ||
|
||
#[test] | ||
#[cfg(unix)] | ||
fn test_output_and_write_streams() { | ||
let mut stdout_buf = Vec::new(); | ||
let mut stderr_buf = Vec::new(); | ||
|
||
let mut cmd = Command::new("echo"); | ||
cmd.args(["-n", "Hello World!"]); | ||
|
||
let output = output_and_write_streams(&mut cmd, &mut stdout_buf, &mut stderr_buf).unwrap(); | ||
|
||
assert_eq!(stdout_buf, "Hello World!".as_bytes()); | ||
assert_eq!(stderr_buf, Vec::<u8>::new()); | ||
|
||
assert_eq!(output.status.code(), Some(0)); | ||
assert_eq!(output.stdout, "Hello World!".as_bytes()); | ||
assert_eq!(output.stderr, Vec::<u8>::new()); | ||
} | ||
} | ||
|
||
/// Constructs a writer that writes to two other writers. Similar to the UNIX `tee` command. | ||
pub(crate) fn tee<A: io::Write, B: io::Write>(a: A, b: B) -> TeeWrite<A, B> { | ||
TeeWrite { | ||
inner_a: a, | ||
inner_b: b, | ||
} | ||
} | ||
|
||
/// A tee writer that was created with the [`tee`] function. | ||
#[derive(Debug, Clone)] | ||
pub(crate) struct TeeWrite<A: io::Write, B: io::Write> { | ||
inner_a: A, | ||
inner_b: B, | ||
} | ||
|
||
impl<A: io::Write, B: io::Write> io::Write for TeeWrite<A, B> { | ||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { | ||
self.inner_a.write_all(buf)?; | ||
self.inner_b.write_all(buf)?; | ||
Ok(buf.len()) | ||
} | ||
|
||
fn flush(&mut self) -> io::Result<()> { | ||
self.inner_a.flush()?; | ||
self.inner_b.flush() | ||
} | ||
} |
Oops, something went wrong.