cargo-each: run a command over Cargo workspace members or JSON Lines
records.
cargo-each resolves either a package selection expressed with the same
selectors as cargo build or a stream of JSON objects. It optionally
narrows Cargo selections with package predicates and runs a command once per
member, matching Cargo target, whole selected set, or JSON record. It
replaces hand-rolled shell loops with one cross-platform command.
cargo-each ships as an executable only; it is a cargo subcommand, not a
library dependency.
cargo install cargo-each
cargo each [SELECTION | JSON INPUT] [FILTERS] [EXECUTION] -- <COMMAND> [ARG...]
Everything after -- is the command template; cargo-each spawns it
directly (argv, not a shell string) after substituting placeholders.
Input is either a Cargo workspace selection or JSON Lines records; the two
sources are deliberately mutually exclusive.
-p/--package <SPEC>— select a member. Repeatable.SPECis a package name, aname@versionspec, or a Unix glob (tokio-*).--package-file <PATH>— read package specs from a UTF-8 file, one per nonempty line. Repeatable; specs are unioned with--package. An empty file explicitly selects no members. One leading UTF-8 byte-order mark is ignored.--workspace/--all— select every workspace member.--exclude <SPEC>— drop a member (with--workspace). Repeatable.--none— explicitly select zero members (a no-op that exits 0).
When nothing is named the default is cargo default-members, exactly
like cargo build; pass --workspace for every member. A selector that
matches no member is an error, so typos fail loudly. Package files contain
package specs only: comments, command-line tokens, malformed input, and
missing, unreadable, or non-UTF-8 files are errors.
--filter accepts Boolean expressions using not, and, or, and
parentheses, with conventional precedence. Repeated --filter expressions
are AND-combined; use not to exclude matching members. Metadata values
containing whitespace or Boolean syntax can be double-quoted. Expression
atoms:
lib/bin/target-kind:<kind>— target-kind membership.doctest— Cargo reports at least one target with doctests enabled.publishable— Cargo permits publishing the package.feature:<name>— the package declares the feature.dep:<name>— the member declares<name>as a dependency.metadata:<dotted.key>—package.metadata.<dotted.key>is present.metadata:<dotted.key>=<value>— that key equals<value>(numeric compare when both sides parse as a number, else string compare).
- per-package (default): run the command once per selected member, in name order, substituting the per-package placeholders below.
--once: run the command exactly once when the set is non-empty (skip when empty), using the{packages}placeholder to inject the selection.--json-lines/--json-lines-file: bypass Cargo selection and run once per JSON object, substituting top-level string fields through{json:key}.--each-target <KIND>: run once per matching Cargo target, using{target}plus the package placeholders. Repeated kinds are OR-combined;--target-required-featurefurther narrows targets.
--keep-going runs every invocation and exits non-zero if any failed
(default is fail-fast). --jobs <N|auto> bounds concurrent per-package,
per-target, or JSON-record work. Omitting it runs exactly one invocation at
a time; auto resolves once to the machine’s available parallelism.
Detection failure is reported explicitly without falling back.
--timeout <DURATION> terminates
each invocation’s Windows job object or Unix process group independently
(250ms, 30s, or 2m). Unix descendants can escape a process group by
starting a new session, so timeout cleanup is best-effort for those escaped
descendants.
--chdir runs each per-package or per-target command from that member crate
root; --dry-run prints commands without running them.
Substituted inside each command argument:
{name}— bare package name (per-package and per-target).{spec}—name@version(per-package and per-target).{version}— package version (per-package and per-target).{manifest}— absolute memberCargo.tomlpath (per-package and per-target).{target}— Cargo target name (per-target).{packages}— the cargo selection flags for the resolved set (--workspacefor the whole workspace, else--package name@version …); valid only in--oncemode and only as a standalone argument.{workspace-rust-version}— the root[workspace.package].rust-version, or root[package].rust-versionin a single-package repository; valid in Cargo-backed per-package, per-target, and--oncemodes. Expands to an empty string when the root declaration is absent.{json:key}— the top-level string field namedkeyin the current record; valid only in JSON-record mode.
Using a placeholder in the wrong mode is a usage error. Every {json:…}
sequence is validated as a JSON placeholder. Other unrecognized {…}
sequences (a typo, or a literal brace an argument needs) pass through
verbatim to the spawned command — there is no brace-escape, so this
passthrough is part of the contract.
An empty resolved selection (via --none, or a filter that removes every
member) is a successful no-op: cargo-each prints a one-line note and
exits 0. This is what lets callers drop bespoke nothing-to-do guards.
Workspace Rust-version resolution is lazy: it runs only when the command
uses {workspace-rust-version} and the resolved plan has work. An absent
root declaration expands to an empty string. When a root floor exists,
every member’s resolved minimum must be present and no newer than that
floor; malformed or inconsistent declarations remain errors. Substitution
is textual, so an empty value does not remove its surrounding argv element.
Placeholder mode validation still runs before an empty-plan no-op.
JSON-record mode requires one JSON object per nonempty input line and does
not load Cargo metadata. Referenced {json:key} fields must exist and be
strings. Invalid input is rejected before any command is spawned, records
retain input order and duplicates, and an empty record set is a successful
no-op.
The effective worker count is the requested --jobs value capped by plan
size. An effective count of one uses sequential
execution with inherited standard input, output, and error even when the
requested value was larger. A genuinely parallel count disconnects child input and
buffers stdout and stderr; complete blocks are emitted in deterministic
plan order. Fail-fast stops launching after the first
observed failure, waits for running work, and chooses the final failure by
plan order. --keep-going runs the complete plan. Worker panics and
unexpected worker-channel disconnections become infrastructure-failure
outcomes instead of blocking the scheduler. Worker launch failures retain
output already collected at earlier plan indices. Parallel work runs in
plan-contiguous waves capped by the effective worker count; each completed
wave is emitted and dropped before the next wave starts, bounding retained
temporary-file storage. Untimed effective-one execution uses an ordinary
child, preserving terminal foreground behavior and Ctrl-C delivery; a
post-spawn wait failure makes one best-effort child-termination request.
Timed and genuinely parallel commands use a job or process group. Without
--timeout, cargo-each observes only the leader and does not kill background
descendants. Every genuinely parallel invocation redirects stdout and
stderr directly to separate unique temporary files.
Child writers and parent readers are separately reopened so parent seeks
cannot move descendant write positions. Cargo-each records each file’s
current length when the leader completes (or immediately after the timeout
termination request returns), then reads exactly that finite snapshot in
plan order without loading unbounded output into memory. Later writes by
background or escaped descendants are outside the snapshot. RAII removes
cargo-each’s directory entry, but a preserved descendant can keep the
backing storage allocated and growing until its inherited writer closes.
Capture create, reopen, length, seek, and read failures are infrastructure
failures.
At a timeout, cargo-each makes one termination request for the Windows job
or Unix process group and returns without waiting for operating-system
teardown to finish. A failed termination request is an infrastructure
failure; otherwise the invocation is reported as timed out. On Unix, an
uncollected timed-out leader may remain as a zombie until cargo-each exits,
consuming one temporary process-table entry per timed-out invocation.
Child commands inherit PATH explicitly. On Windows this makes relative
program lookup honor the inherited PATH order instead of preferring an
unrelated executable beside cargo-each.
Exit 0 means all work succeeded or there was no work. In fail-fast mode a
command failure returns its code, a timeout returns 1, and usage,
configuration, spawn, or post-spawn infrastructure failures return 2.
Under --keep-going, any failure maps the aggregate result to 1.
Run a per-manifest tool over every library crate:
cargo each --workspace --filter lib -- \
cargo check-external-types --manifest-path {manifest}
Run one clippy invocation over a computed subset, skipping when it is empty:
cargo each -p crate-a -p crate-b --once -- \
cargo clippy {packages} --all-targets -- -D warnings
Run every Cargo test target that requires the loom feature:
cargo each --workspace --each-target test --target-required-feature loom -- \
cargo test -p {name} --test {target} --features loom
This crate was developed as part of The Oxidizer Project. Browse this crate's source code.
