rusty-sponge 0.2.0

Soak up stdin and write it atomically to a file — a Rust port of moreutils `sponge` with strict-compat mode, configurable spill-to-tempfile, and a typed library API.
Documentation

rusty-sponge

Soak up stdin & rewrite the file atomically. Rust port of moreutils sponge(1).

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

Lets you write cmd file | rusty-sponge file without the shell-truncation race that breaks the equivalent cmd file > file. Default mode adds --help, --version, completions, & a RUSTY_SPONGE_SPILL_MB env override. Strict mode reverts every observable surface to byte-equal moreutils sponge for drop-in migration.

Part of the Rusty portfolio.

Install

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

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

cargo install rusty-sponge --features sponge-alias

Usage

# In-place rewrite without the shell-truncation race (sponge's headline use case)
sort file.txt | rusty-sponge file.txt

# Filter a config file & write it back safely
grep -v '^#' config.yaml | rusty-sponge config.yaml

# Pipeline batching to stdout (no file argument; useful as a flow barrier)
producer | rusty-sponge | consumer

# Append mode (read existing file first, then append stdin, then atomic rename)
echo "new line" | rusty-sponge -a logfile

# Configurable spill threshold (Default mode only; default 128 MiB)
RUSTY_SPONGE_SPILL_MB=8 huge-producer | rusty-sponge target.bin

# Strict moreutils-compat mode (drop-in moreutils sponge replacement)
some-command | rusty-sponge --strict file
RUSTY_SPONGE_STRICT=1 some-command | rusty-sponge file
some-command | sponge file               # via sponge-alias feature or argv[0] symlink

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

Library API

The crate exposes a byte-typed runtime. The builder owns the buffer & the atomic-rename procedure. Use it inside a daemon when you want sponge's crash-safety without spawning a child process per write.

use rusty_sponge::{SpongeBuilder, Target, CompatibilityMode};
use std::io::Cursor;
use std::path::PathBuf;

let mut sponge = SpongeBuilder::new()
    .target(Target::File(PathBuf::from("output.txt")))
    .append(false)
    .spill_threshold(64 * 1024 * 1024)
    .compat(CompatibilityMode::Default)
    .build()?;

sponge.run(Cursor::new(b"hello\nworld\n"))?;
# Ok::<(), rusty_sponge::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. sponge-classic reproduces v0.1.x bare-port behavior matching upstream moreutils sponge 1:1. To strip the CLI surface use default-features = false or --no-default-features & add the features you want.

rusty-sponge is a single-capability port: its one documented job is "soak up stdin & write it atomically to a file". 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, sponge-classic, sponge-minimal, sponge-alias
sponge-alias Installs an additional sponge binary alongside rusty-sponge. Both share source; argv[0] auto-detect routes sponge 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)

Preset bundles

Bundle Composition Use case
sponge-classic cli Drop-in upstream moreutils sponge replacement. Strict mode is invoked via --strict, RUSTY_SPONGE_STRICT, or sponge-alias argv[0] auto-detect.
sponge-minimal cli Explicit minimal-CLI alias for users who prefer the <port>-minimal naming convention seen across other portfolio ports (figlet-minimal, ts-minimal, pwgen-minimal). Identical composition to sponge-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-sponge --no-default-features --features "cli"
# → bare CLI with no sponge-alias binary, no bench tooling.

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

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

Library-only consumers

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

This strips clap, clap_complete, anyhow, & signal-hook. The resulting build pulls only tempfile, thiserror, & the windows-sys target-conditional dep (Windows only). The CI test-no-default job runs cargo tree --no-default-features on every PR & fails the build if any CLI-only dep leaks back in.

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-sponge has two modes:

  • Default mode. clap-styled flag parser. --help, --version, the completions subcommand, & the RUSTY_SPONGE_SPILL_MB env override are all available. Spill threshold defaults to 128 MiB (compile-time constant) so RAM sizing is predictable across hosts.
  • Strict mode (activated by --strict, RUSTY_SPONGE_STRICT=1, or invoking the binary as sponge). Byte-equal stdout, stderr, exit codes, & the -h usage layout against moreutils sponge at the pinned upstream commit recorded in fixtures/README.md. --help, --version, & completions MUST be rejected. RUSTY_SPONGE_SPILL_MB MUST be ignored.

Atomic-safety guarantee

When rusty-sponge writes to a regular non-symlink file, it writes to a sibling tempfile in the target's parent directory & atomically renames into place. Mid-write failures (SIGKILL, power loss, disk full) leave the original file byte-identical to its pre-invocation state. This is the property the original sponge was invented to provide.

The guarantee does NOT apply when:

  1. The target is a symlink or non-regular file. The linked file is written through with O_WRONLY+O_TRUNC, matching moreutils' S_ISREG && !S_ISLNK short-circuit.
  2. The cross-volume / shared-handle atomic-rename fallback triggers. Non-atomic copy + truncate-and-rewrite runs as a last resort.

Both fallback paths match moreutils behavior. They are documented limitations, not bugs.

Documented intentional divergences

  1. --help / --version. Default-mode additions; rejected in Strict.
  2. completions subcommand. Default-mode addition; rejected in Strict.
  3. RUSTY_SPONGE_SPILL_MB env var. Honored in Default; ignored in Strict.
  4. Spill threshold default: 128 MiB compile-time constant vs moreutils' dynamic ½-available-RAM heuristic. Trades RAM-aware sizing for predictability; configurable via env var or library builder.

See docs/COMPATIBILITY.md for the full per-flag matrix & exit-code table.

What's not shipped

  • moreutils' dynamic ½-available-RAM spill heuristic. Replaced with the 128 MiB compile-time constant for predictability across hosts. Users override via RUSTY_SPONGE_SPILL_MB or the library spill_threshold builder setter.
  • Source-code derivation from moreutils. This is a clean-room reimplementation. The moreutils sponge C source is GPL'd & untouched. Snapshot tests compare runtime output bytes only, which are facts, not creative expression. Same posture as uutils/coreutils.

If you want the original moreutils sponge, install it via your platform package manager (apt install moreutils, brew install moreutils). It coexists fine with this port.

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.