rusty-pee 0.2.0

Fan stdin out to N concurrent shell-spawned children — a Rust port of moreutils `pee` with strict-compat mode, exit-code aggregation (Default max / Strict bitwise OR), backpressure-paced byte-perfect delivery, and a typed library API.
Documentation

rusty-pee

Fan stdin out to N concurrent shell-spawned children. Rust port of moreutils pee(1).

crates.io docs.rs CI MSRV license: MIT OR Apache-2.0

Run one stream through several commands at once: journalctl | rusty-pee 'grep selinux > a.log' 'wc -l > b.txt' feeds every byte to each child. It aggregates their exit codes, drops BrokenPipe sinks cleanly, & holds memory at O(BUFSIZ × N) instead of buffering the whole stream. Default mode adds --capture, --help, --version, & a completions subcommand; Strict mode mirrors moreutils' bitwise-OR exit aggregation & stderr layout. Prebuilt binaries ship for five targets (Linux x86_64/aarch64, macOS x86_64/aarch64, Windows x86_64).

Part of the Rusty portfolio.

Install

cargo install rusty-pee
# or, with prebuilt binaries:
cargo binstall rusty-pee
# or, download directly from GitHub Releases:
# https://github.com/jsh562/rusty-pee/releases

To also install a pee binary alias (argv[0] auto-detect routes into Strict mode):

cargo install rusty-pee --features pee-alias

Usage

# Fan stdin out to multiple sinks that each consume the full stream
journalctl -p err | rusty-pee 'grep selinux > /tmp/sel.log' 'wc -l > /tmp/errcount.txt'

# Run two validators in parallel; aggregate their exit codes
producer | rusty-pee 'validator-A' 'validator-B'

# Capture mode: emit child stdouts in argv order after they exit
echo "alpha" | rusty-pee --capture 'cat' 'cat'

# Strict moreutils-compat mode (drop-in moreutils pee replacement)
producer | rusty-pee --strict 'validator-A' 'validator-B'
RUSTY_PEE_STRICT=1 producer | rusty-pee 'sink-A' 'sink-B'
producer | pee 'sink-A' 'sink-B'         # via pee-alias feature or argv[0] symlink

# Shell completions
rusty-pee completions bash                # > ~/.bash_completion.d/rusty-pee
rusty-pee completions zsh                 # > ~/.zfunc/_rusty-pee
rusty-pee completions fish                # > ~/.config/fish/completions/rusty-pee.fish
rusty-pee completions powershell

Library API

The library fans out to N Box<dyn Write + Send> sinks instead of subprocesses. Use it when you want pee's broadcast pattern inside a long-running program without spawning a shell per consumer.

use rusty_pee::{PeeBuilder, CompatibilityMode};
use std::io::Cursor;

let input = Cursor::new(b"alpha\nbravo\ncharlie\n".to_vec());
let mut sink_a: Vec<u8> = Vec::new();
let mut sink_b: Vec<u8> = Vec::new();

let mut pee = PeeBuilder::new()
    .sink(Box::new(&mut sink_a))
    .sink(Box::new(&mut sink_b))
    .compat(CompatibilityMode::Default)
    .build()?;

pee.run(input)?;

assert_eq!(sink_a, b"alpha\nbravo\ncharlie\n");
assert_eq!(sink_b, b"alpha\nbravo\ncharlie\n");
# Ok::<(), rusty_pee::Error>(())

For library-only consumers without CLI deps see the Cargo Features section.

Cargo Features

default enables full, which (for this single-capability port) resolves to the cli umbrella. pee-classic reproduces v0.1.x bare-port behavior matching upstream moreutils pee 1:1. To strip the CLI surface use default-features = false or --no-default-features & add the features you want.

rusty-pee is a single-capability port: its one documented job is "fan a single stdin stream out to N concurrent shell-spawned children, aggregate their exit codes, & surface failures cleanly". No optional feature leaves are carved beyond the required umbrellas; see docs/feature-layout.md for why.

Feature matrix

Feature Description Umbrella(s)
cli All CLI-only dependencies (clap, clap_complete, anyhow, signal-hook) and the binary entry point, signal-handler install, mode resolver, and Strict-mode pre-scanner. Library consumers strip via default-features = false. full, pee-classic, pee-minimal, pee-alias
pee-alias Installs an additional pee binary alongside rusty-pee. Both share source; argv[0] auto-detect routes pee invocations into Strict mode. (standalone, implies cli)
bench Pulls criterion and enables benches/throughput.rs. Dev-tooling only; outside the convention's leaf surface. Name preserved verbatim from v0.1.x. (standalone)
dev-helpers Gates the fake-pee-child [[bin]] used by integration tests. Never installed by cargo install; enable with cargo test --features dev-helpers. Dev-tooling only. (standalone)

Preset bundles

Bundle Composition Use case
pee-classic cli Drop-in upstream moreutils pee replacement. Strict mode is invoked via --strict, RUSTY_PEE_STRICT, or pee-alias argv[0] auto-detect.
pee-minimal cli Explicit minimal-CLI alias for users who prefer the <port>-minimal naming convention seen across other portfolio ports. Identical composition to pee-classic.

Keep-list workaround (Cargo features are union-only)

Cargo features cannot subtract from default. To get "everything except a specific feature," disable defaults & enumerate the features you want:

cargo install rusty-pee --no-default-features --features "cli"
# → bare CLI with no pee-alias binary, no bench tooling.

cargo install rusty-pee --no-default-features --features "cli pee-alias"
# → CLI + the pee alias binary.

For the common cases the named preset bundles are usually sufficient.

Library-only consumers

[dependencies]
rusty-pee = { version = "0.2", default-features = false }

This strips clap, clap_complete, anyhow, & signal-hook. The resulting build pulls only thiserror & the target-conditional always-on deps (libc on Unix, windows-sys on Windows; both required for cross-platform child-process control regardless of feature selection).

Convention authority

This layout follows the portfolio-wide Cargo Features Convention. The "why" lives in ADR-0006; the "what" lives in project-instructions.md §Cargo Feature Surface. Every Rusty port from v0.2 onward exposes the same umbrella set (default / full / cli / <port>-classic), per-port leaves named in kebab-case, & 2 to 4 preset bundles.

Compatibility

rusty-pee has two modes:

  • Default mode. clap-styled flag parser. --capture, --help, --version, & the completions subcommand are all available. Exit-code aggregation uses max(child_codes).
  • Strict mode (activated by --strict, RUSTY_PEE_STRICT=1, or invoking the binary as pee). Bitwise OR over WEXITSTATUS for exit aggregation. moreutils-format stderr. --capture, --help, --version, & completions MUST be rejected. Pinned upstream version: moreutils 0.69.

Pipeline-safety guarantee

Every live sink receives every byte the parent reads, in argv (CLI) / registration (library) order. When a child closes its stdin mid-stream (BrokenPipe), the parent drops it from the live-set & keeps feeding survivors. Memory MUST stay bounded at O(BUFSIZ × N), not O(input size).

Documented intentional divergences

  1. Exit-code aggregation. Default mode uses max(child_codes); Strict mode preserves moreutils' bitwise OR over WEXITSTATUS. Example: children exiting 1 & 2 produce Default exit=2, Strict exit=3.
  2. Hardcoded platform shell. /bin/sh -c (Unix) / cmd /C (Windows). No --shell <path> flag in v0.1.0.
  3. Unknown-flag stderr in Strict. Emits only the first unknown-flag error (rusty-pee: invalid option -- 'X'). moreutils' POSIX Getopt::Long iterates per-character; we don't replicate that for undocumented inputs.
  4. --capture, --help, --version, completions. Default-mode additions; rejected in Strict.
  5. --no-ignore-sigpipe / --no-ignore-write-errors. moreutils exposes these to opt OUT of default-on behavior; rusty-pee v0.1.0 freezes the defaults on & provides no negation surface.
  6. SIGTERM-to-SIGKILL grace period. Hardcoded 1 second (no flag, no env).

See docs/COMPATIBILITY.md for the full per-flag matrix.

pee-alias PATH-collision warning

Building with --features pee-alias installs a second binary named pee alongside rusty-pee. If moreutils is also on the same PATH, whichever directory comes first wins. Invoke rusty-pee (always unambiguous) or omit the pee-alias feature when moreutils is also present.

What's not shipped

  • --shell <path> override. Hardcoded /bin/sh -c on Unix, cmd /C on Windows. Add a wrapper script if you need a different shell.
  • --no-ignore-sigpipe / --no-ignore-write-errors negation flags. Defaults are frozen on; no negation surface in v0.1.0.
  • Configurable SIGTERM-to-SIGKILL grace period. Hardcoded 1 second.
  • Source-code derivation from moreutils. This is a clean-room reimplementation. The moreutils pee Perl source is GPL'd & untouched. Same posture as uutils/coreutils.

MSRV

Rust 1.85 (edition 2024). Re-verified against the portfolio's stable-minus-two policy at each release.

License

Dual-licensed under MIT or Apache-2.0 at your option.