rusty-sponge 0.1.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

A Rust port of the moreutils sponge utility: soak up all of stdin and write it atomically to a file, so you can safely do cmd file | rusty-sponge file without the shell-truncation race that breaks the equivalent cmd file > file. Static binaries on Linux, macOS, and Windows; works with or without a Rust toolchain via cargo install or cargo binstall. Default mode adds a few niceties moreutils doesn't have (--help, --version, completions, RUSTY_SPONGE_SPILL_MB env override); Strict mode reverts every observable surface to byte-identical moreutils behavior for drop-in migration.

Part of the Rusty portfolio — a collection of small Rust ports of utilities missing from the Rust ecosystem.

Install

With a Rust toolchain

cargo install rusty-sponge

To also install the sponge binary alias (auto-enables Strict mode on invocation):

cargo install rusty-sponge --features sponge-alias

Without a Rust toolchain (prebuilt binaries via cargo-binstall)

cargo binstall rusty-sponge

Direct download

Per-target archives are attached to each GitHub Release. Linux x86_64/aarch64, macOS x86_64/aarch64, Windows x86_64. Each archive contains the binary plus pre-generated shell-completion scripts for bash, zsh, fish, and PowerShell.

Usage

# In-place file rewrite (sponge's headline use case)
sort file.txt | rusty-sponge file.txt

# Pipeline batching to stdout (no file argument)
producer | rusty-sponge | consumer

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

# Strict moreutils-compat mode (rejects `--help`/`--version`/`completions`, mirrors stderr layout)
some-command | rusty-sponge --strict file
RUSTY_SPONGE_STRICT=1 some-command | rusty-sponge file
some-command | sponge file    # via the sponge-alias feature or a symlink — argv[0] auto-detect

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

# 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

Compatibility statement (vs moreutils sponge)

Byte-level fidelity is verified by snapshot tests against captured moreutils-sponge output under a pinned environment (LC_ALL=C.UTF-8). The snapshot reference is moreutils at a pinned upstream commit recorded in fixtures/README.md.

Atomic-safety guarantee (FR-006): When rusty-sponge writes to a regular non-symlink file, it writes to a sibling tempfile in the target's parent directory and 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 (FR-010) — 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 path triggers (FR-025) — non-atomic copy + truncate-and-rewrite is used as a last resort. Both fallback paths match moreutils behavior; they are documented limitations, not bugs.

Documented intentional divergences from moreutils sponge (also enumerated in docs/COMPATIBILITY.md — generated from the CLI definition and drift-tested in CI):

  1. --help / --version flags: not present in moreutils. Default-mode additions; rejected in Strict mode.
  2. completions subcommand: not present in moreutils. Default-mode addition; rejected in Strict mode.
  3. RUSTY_SPONGE_SPILL_MB env var: not defined by moreutils (which sizes its spill heuristic dynamically from available RAM). Honored in Default mode; ignored in Strict mode.
  4. Spill threshold default: 128 MiB (compile-time constant) vs moreutils' dynamic ½-available-RAM. Trades RAM-aware sizing for predictability; configurable via the env var or library builder.

In Strict mode, exit codes, stderr diagnostic text, and the -h usage layout match moreutils. See docs/COMPATIBILITY.md for the full per-flag matrix and exit-code table.

Library API

The crate exposes a public Rust API for programmatic use. The canonical surface is byte-typed (preserves non-UTF-8 payload bytes per FR-012); the builder produces a Sponge runtime that owns the buffer and the atomic-rename procedure.

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>(())

To use the library without pulling in the CLI dependencies:

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

Stability commitment

Lockstep SemVer: the library and binary share a single crate version. Within the 0.x series, minor version bumps may introduce breaking changes per standard Cargo semantics — pin to the patch version (= "0.1.0") if breakage is a concern. Once 1.0 lands, the API is frozen to additive-only changes guarded by #[non_exhaustive] on every public enum and struct.

MSRV

Minimum supported Rust version: 1.85.

This is an upward deviation from the Rusty portfolio's standard "current stable minus two minor releases" rule, forced by the crate's use of Rust edition 2024 (which requires 1.85+). The portfolio rule remains in effect for ports not using edition 2024; this crate's MSRV will advance with edition adoption, not with the rolling N-2 cadence.

Relationship to moreutils

rusty-sponge is a clean-room Rust reimplementation of the moreutils sponge utility. It contains no source code from moreutils — only a from-scratch Rust implementation that observes the documented behavior of moreutils sponge and reproduces it.

The moreutils sponge C source is © Colin Watson and Tollef Fog Heen (2006) and licensed under the GNU GPL (v2 or later). That license governs the C source code. Behavioral interfaces (flag set, buffering semantics, atomic-rename pattern) are not copyrightable, so a clean-room reimplementation under a different license is well-established practice — the same posture as uutils/coreutils (MIT-licensed reimplementation of GPL-licensed GNU coreutils).

rusty-sponge does not distribute or derive from the moreutils source code. Snapshot tests in this repository compare rusty-sponge runtime output against captured moreutils sponge runtime output (captured by running moreutils against fixtures and recording bytes) — that is not source-code derivation either. The captured output bytes are facts, not creative expression.

If you want the original moreutils sponge, install it via your platform's package manager (apt install moreutils, brew install moreutils, etc.) — that is unaffected by this port's existence.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.