Skip to content

Commit

Permalink
First
Browse files Browse the repository at this point in the history
  • Loading branch information
schneems committed Nov 1, 2023
0 parents commit 8f49903
Show file tree
Hide file tree
Showing 8 changed files with 965 additions and 0 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
/Cargo.lock
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Unreleased

## 0.1.0

- First release
14 changes: 14 additions & 0 deletions Cargo.toml
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"]
8 changes: 8 additions & 0 deletions LICENSE
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.
104 changes: 104 additions & 0 deletions README.md
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())
}
}
```
107 changes: 107 additions & 0 deletions src/command.rs
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()
}
}
Loading

0 comments on commit 8f49903

Please sign in to comment.