No description
  • Rust 96%
  • Shell 4%
Find a file
2026-03-29 20:58:33 -07:00
src run-with-cache in rust 2026-03-29 20:58:33 -07:00
tests run-with-cache in rust 2026-03-29 20:58:33 -07:00
.gitignore run-with-cache in rust 2026-03-29 20:58:33 -07:00
build.sh run-with-cache in rust 2026-03-29 20:58:33 -07:00
Cargo.lock run-with-cache in rust 2026-03-29 20:58:33 -07:00
Cargo.toml run-with-cache in rust 2026-03-29 20:58:33 -07:00
contain.sh run-with-cache in rust 2026-03-29 20:58:33 -07:00
README.md run-with-cache in rust 2026-03-29 20:58:33 -07:00
rust-toolchain.toml run-with-cache in rust 2026-03-29 20:58:33 -07:00

run-with-cache

run-with-cache is a single Rust binary for running commands with on-disk caching.

It replaces both legacy shell scripts:

  • run-with-cache
  • run-with-fail-cache

The same binary can do both jobs:

  • normal output caching
  • failure memoization with --failure-only

Install

cargo install --git <git-url>

Tutorial

The idea

run-with-cache sits in front of another command.

On the first run, it executes the command normally. On later runs, if the relevant inputs still match, it replays the cached result instead of rerunning the command.

That is most useful when the wrapped command is:

  • expensive
  • noisy
  • flaky
  • repeated often with the same inputs

The basic shape

Wrapper options go before the first --. The wrapped command starts after that --.

run-with-cache [wrapper options] -- command arg1 arg2

Everything after the first -- belongs to the wrapped command, including later -- values.

run-with-cache --some-wrapper-flag -- the_real_command --flag value -- nested --flag

That means this works exactly the way you want:

run-with-cache --cache-dir ~/.cache/my-tool -- \
  docker run --rm image-name -- \
  nested-command --final-flag

Your first cache

Run something that prints a value:

run-with-cache -- date +%s

Run it again immediately:

run-with-cache -- date +%s

The second run returns the same value because the first result was cached.

A more realistic example

Cache a recursive search:

run-with-cache -- rg TODO src

This is useful when:

  • the codebase is large
  • you repeat the same query often
  • the underlying files have not changed in ways that affect the cache key

Core Concepts

1. Cache location

By default, the cache lives under XDG_CACHE_HOME/run-with-cache or ~/.cache/run-with-cache.

You can override it:

run-with-cache --cache-dir /tmp/run-cache -- rg TODO src

This is useful for:

  • per-project caches
  • CI jobs
  • debugging cache contents

2. Cache lifetime

Cached results expire after --expire-seconds.

run-with-cache --expire-seconds 60 -- curl -fsS https://example.com/data.json

That says:

  • reuse the cached result for up to 60 seconds
  • rerun after that

3. Wrapper options vs wrapped command options

This is the most important syntax rule.

This is correct:

run-with-cache --expire-seconds 300 -- cargo test -- --nocapture

This means:

  • --expire-seconds 300 configures run-with-cache
  • cargo test -- --nocapture is passed through untouched

4. Dry runs

Use --dry-run to ask whether a command would run.

run-with-cache --dry-run -- cargo check

Possible outcomes:

  • Cached. Would not run.
  • Not cached. Would run.

5. Clearing cache entries

Use --clear to drop the cache for this command before running it again.

run-with-cache --clear -- cargo test

This is useful when:

  • you want a clean rerun
  • you suspect a stale cache entry
  • you changed something that is not part of the cache key yet

6. Disabling cache behavior

Disable both reads and writes:

run-with-cache --disable -- cargo build

Disable only reads:

run-with-cache --disable-read -- cargo build

Disable only writes:

run-with-cache --disable-write -- cargo build

These are useful when testing behavior or comparing cached vs uncached performance.

What Forms the Cache Key

By default, the cache key is built from:

  • the wrapped command and arguments
  • the selected working directory
  • selected environment variables, if any
  • declared dependency files, if any
  • optional network state, if enabled
  • declared created outputs, if any

If any of those inputs change, the cache key changes too.

1. File dependencies with --depends

Use --depends when a command depends on files that are not obvious from the command line.

run-with-cache --depends Cargo.lock -- cargo build

You can repeat it:

run-with-cache \
  --depends Cargo.lock \
  --depends rust-toolchain.toml \
  -- cargo test

Good uses:

  • config files
  • lockfiles
  • generated inputs
  • templates

2. Metadata hashing vs content hashing

Dependency files are hashed by metadata by default. That keeps the common case fast.

If you want stronger invalidation, use full content hashing:

run-with-cache --hash-mode content --depends config.json -- my-command

Use metadata when:

  • speed matters most
  • file size and mtime are enough

Use content when:

  • correctness matters more than speed
  • your workflow can preserve mtimes unexpectedly

3. Environment variables with --include-env

Sometimes a command changes behavior based on environment variables.

run-with-cache --include-env RUSTFLAGS -- cargo build

Or several:

run-with-cache --include-env RUSTFLAGS,CARGO_HOME -- cargo build

That makes the cache distinct for each relevant environment value.

Example:

SPECIAL_MODE=fast run-with-cache --include-env SPECIAL_MODE -- ./script.sh
SPECIAL_MODE=safe run-with-cache --include-env SPECIAL_MODE -- ./script.sh

Those are two different cache entries.

4. Network-sensitive commands with --network

If your workflow maintains cached network state files under XDG_CACHE_HOME/state, you can include them:

run-with-cache --network -- some-network-aware-command

This uses:

  • XDG_CACHE_HOME/state/nmap-scan
  • XDG_CACHE_HOME/state/ip-state

Use this only if your environment actually manages those files.

Output-Aware Caching

Some commands matter not just for stdout/stderr, but also for files they create.

1. Declaring outputs with --creates

Use --creates when the wrapped command is expected to create or refresh files.

run-with-cache --creates target/report.json -- ./generate-report.sh

This does two things:

  • verifies that the file exists after the command succeeds
  • includes that file in the final cache identity

You can declare several outputs:

run-with-cache \
  --creates dist/app.js \
  --creates dist/app.css \
  -- ./build-assets.sh

2. touch=1

Some tools produce a valid output without updating its mtime in the normal way. Use touch=1 if the file should count as refreshed even if the tool itself does not touch it clearly.

run-with-cache --creates build/output.bin,touch=1 -- ./package.sh

3. precious=1

If a command fails after creating a file and that file should not be deleted, mark it precious.

run-with-cache --creates logs/partial.txt,precious=1 -- ./unstable-job.sh

Without precious=1, newly created outputs from a failed run are removed when they appear to have been created during that failed run.

4. Empty output as failure with --no-empty

Some commands are only useful if they print something.

run-with-cache --no-empty -- rg TODO src

If stdout is empty, the run is treated as a failure.

Failure Memoization

The old run-with-fail-cache behavior is now:

run-with-cache --failure-only -- command ...

This means:

  • successful runs are not cached as failures
  • failing exit codes are remembered
  • if the same failing command is run again with the same inputs, the failure is returned immediately

Example:

run-with-cache --failure-only -- cargo test

This is useful when:

  • a known-bad command is expensive
  • you want to avoid repeating the same failing work
  • you are polling something that often fails in the same way

Caching Failed Output Too

In normal mode, failed results are not stored unless you ask for that.

Use --preserve-failed if you want to cache a failing command's stdout, stderr, and exit code:

run-with-cache --preserve-failed -- ./failing-script.sh

This is useful when the failure output itself is stable and expensive to reproduce.

Working Directory Rules

The wrapped command runs from one of these locations:

  • current directory if you use --preserve-cwd
  • --cwd <path> if you set one
  • otherwise the git repo root if one is found
  • otherwise your home directory if available

Preserve the current directory

run-with-cache --preserve-cwd -- make test

Force a specific directory

run-with-cache --cwd ./backend -- cargo test

This matters because working directory selection is part of the cache identity.

Examples By Use Case

Cache a slow cargo check

run-with-cache \
  --depends Cargo.lock \
  --depends rust-toolchain.toml \
  --include-env RUSTFLAGS \
  -- cargo check

Cache a generated artifact

run-with-cache \
  --creates target/schema.json \
  -- ./scripts/generate-schema.sh

Cache an API fetch for a short period

run-with-cache \
  --expire-seconds 30 \
  -- curl -fsS https://example.com/api/items

Memoize a failing integration test

run-with-cache \
  --failure-only \
  --depends docker-compose.yml \
  -- cargo test integration_test_name -- --nocapture

Wrap a command that itself uses --

run-with-cache \
  --expire-seconds 300 \
  -- cargo test my_test -- --exact --nocapture

Include environment-sensitive behavior

FEATURE_SET=lite run-with-cache --include-env FEATURE_SET -- ./bench.sh
FEATURE_SET=full run-with-cache --include-env FEATURE_SET -- ./bench.sh

Rebuild only when input files change

run-with-cache \
  --hash-mode content \
  --depends templates/email.html \
  --depends config/production.toml \
  --creates build/email.txt \
  -- ./render-email.sh

Legacy Environment Variables

The Rust binary still supports the legacy CACHE_* environment variables where they still make sense.

Example:

CACHE_DIR=/tmp/run-cache \
CACHE_DEPENDS=Cargo.lock:rust-toolchain.toml \
CACHE_ENV_VARS=RUSTFLAGS,CARGO_HOME \
CACHE_PWD=1 \
run-with-cache -- cargo test

Supported variables:

  • CACHE_DIR
  • CACHE_EXPIRE_SECONDS
  • CACHE_NETWORK
  • CACHE_PRESERVE_FAILED
  • CACHE_MD5
  • CACHE_DRY_RUN
  • CACHE_CLEAR
  • CACHE_DISABLE
  • CACHE_DISABLE_READ
  • CACHE_DISABLE_WRITE
  • CACHE_DEPENDS
  • CACHE_CREATES
  • CACHE_NO_EMPTY
  • CACHE_ALLOW_EXPLICIT
  • CACHE_ENV_VARS
  • CACHE_CWD
  • CACHE_PWD

Prefer CLI flags for new usage. They are easier to read and compose.

Practical Advice

Start simple

Start with this:

run-with-cache -- your-command here

Then add only what you need:

  • --depends for hidden file inputs
  • --include-env for env-sensitive behavior
  • --creates for produced files
  • --failure-only for failure memoization

Good candidates

  • cargo check
  • cargo test with fixed inputs
  • code generators
  • expensive searches
  • API fetches with short TTLs

Bad candidates

  • commands that must always observe live external state
  • commands with side effects you always want to happen
  • commands whose true inputs are unknown and not declared

Notes

  • This crate forbids unsafe.
  • No unsafe is used in the implementation.
  • The old strace hook was intentionally dropped because it was Linux-specific, inconsistent in the shell version, and unnecessary for a portable cache core.