rusty-sponge
Soak up stdin & rewrite the file atomically. Rust port of moreutils sponge(1).
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
# or, with prebuilt binaries:
# 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):
Usage
# In-place rewrite without the shell-truncation race (sponge's headline use case)
|
# Filter a config file & write it back safely
|
# Pipeline batching to stdout (no file argument; useful as a flow barrier)
| |
# Append mode (read existing file first, then append stdin, then atomic rename)
|
# Configurable spill threshold (Default mode only; default 128 MiB)
RUSTY_SPONGE_SPILL_MB=8 |
# Strict moreutils-compat mode (drop-in moreutils sponge replacement)
|
RUSTY_SPONGE_STRICT=1 |
|
# Shell completions
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 ;
use Cursor;
use PathBuf;
let mut sponge = new
.target
.append
.spill_threshold
.compat
.build?;
sponge.run?;
# Ok::
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:
# → bare CLI with no sponge-alias binary, no bench tooling.
# → CLI + the sponge alias binary.
For the common cases the named preset bundles are usually sufficient.
Library-only consumers
[]
= { = "0.2", = 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, thecompletionssubcommand, & theRUSTY_SPONGE_SPILL_MBenv 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 assponge). Byte-equal stdout, stderr, exit codes, & the-husage layout against moreutilsspongeat the pinned upstream commit recorded infixtures/README.md.--help,--version, &completionsMUST be rejected.RUSTY_SPONGE_SPILL_MBMUST 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:
- 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_ISLNKshort-circuit. - 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
--help/--version. Default-mode additions; rejected in Strict.completionssubcommand. Default-mode addition; rejected in Strict.RUSTY_SPONGE_SPILL_MBenv var. Honored in Default; ignored in Strict.- 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_MBor the libraryspill_thresholdbuilder setter. - Source-code derivation from moreutils. This is a clean-room reimplementation. The moreutils
spongeC source is GPL'd & untouched. Snapshot tests compare runtime output bytes only, which are facts, not creative expression. Same posture asuutils/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.