Skip to main content

Command

Struct Command 

Source
pub struct Command { /* private fields */ }
Expand description

A description of a child process to launch: program, arguments, working directory, environment, stdin source, and an optional timeout.

A single builder for everything a run needs. Build it, then either drive it to completion with a helper (output_string, run, …) or start it via a ProcessRunner for streaming/shared groups.

Implementations§

Source§

impl Command

Source

pub fn new(program: impl AsRef<OsStr>) -> Self

Start a command for program (resolved on PATH).

Source

pub fn arg(self, arg: impl AsRef<OsStr>) -> Self

Append a single argument.

Source

pub fn args<I, S>(self, args: I) -> Self
where I: IntoIterator<Item = S>, S: AsRef<OsStr>,

Append several arguments.

Source

pub fn current_dir(self, dir: impl AsRef<Path>) -> Self

Set the working directory for the child process.

Relative-path programs and current_dir: if the program passed to Command::new is a relative path (e.g. "./tool" or "../bin/x"), it is resolved against the caller’s current directory at spawn time — not against the directory set here. Use an absolute path for the program when combining current_dir with a relative-path executable. A bare-name program resolved via prefer_local doesn’t share this footgun: a relative prefer_local directory is always turned into an absolute path before being handed to the OS, so it can’t be reinterpreted against the directory set here.

Source

pub fn prefer_local(self, dir: impl Into<PathBuf>) -> Self

Probe dir for the program before the system PATH — for a locally-installed tool (a project’s node_modules/.bin, target/debug, a vendored toolchain) that a caller wants to run by bare name without hand-rolling a PATH override.

Repeated calls accumulate, in priority order: the directory from the first call is probed first, then the second, and so on, with the system PATH tried last as the final fallback. Resolution reuses the exact same PATHEXT-aware lookup as the PATH search (the same probe_dir helper — no separate copy), so a .exe/.cmd/.bat on Windows is found exactly as it would be on PATH.

Only affects a bare-name program. If the program passed to Command::new is a path — absolute, or relative with a separator ("./tool", "../bin/x") — prefer_local has no effect and the existing contract holds unchanged: such a program is never looked up here or on PATH.

Does not touch the child’s own PATH. This only changes where the parent looks to resolve the program for this one launch — the PATH the child sees in its own environment (via inheritance, env, or inherit_env) is neither rewritten nor extended. When the program is found under one of these directories, the child is simply spawned via that resolved absolute path instead of the bare name (so the OS never has to search anything); a grandchild the program itself spawns does not inherit this reach.

A relative dir here (e.g. "./node_modules/.bin") is probed against the process’s actual current directory, not against whatever is set via current_dir — and the resulting match is always made absolute (by joining it onto that same current directory) before being handed to the OS, so it can never be reinterpreted against the child’s own working directory once current_dir is also set.

If resolution fails everywhere, Error::NotFound’s searched includes these directories — first, in priority order — ahead of the PATH directories, so the diagnostic doesn’t hide that they were checked too.

Source

pub fn env(self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self

Set an environment variable for the child. To remove an inherited variable, use env_removevalue here is always a value, never None.

Secrets. env is the right channel for a token or password: env values are redacted from this command’s Debug (only names appear) and are never emitted via tracing or in cassette recordings, and the child receives the value intact. Prefer it — or stdin, the strongest — over a command-line arg: argv is reduced to a count in Debug too, but is world-readable through the OS process table (/proc/<pid>/cmdline, ps) and is exposed verbatim by command_line and cassette recording. An env value is not world-readable, but is still visible to the same user and root via /proc/<pid>/environ and is inherited by every descendant process; stdin exposes the secret to neither.

processkit deliberately ships no Secret wrapper type — pair env with the secrecy/zeroize crates for a typed, memory-scrubbed secret at your own call sites and pass the exposed value here. Scrubbing only covers your copy: once passed, processkit holds a plain OsString for the command’s lifetime and the child receives cleartext (a core dump can expose either). For a secret recomputed per operation — resolved when each command is built and reused across that command’s retries, not regenerated per attempt — use CliClient::default_env_fn.

Source

pub fn env_remove(self, key: impl AsRef<OsStr>) -> Self

Remove an environment variable inherited from the parent.

Source

pub fn envs<I, K, V>(self, vars: I) -> Self
where I: IntoIterator<Item = (K, V)>, K: AsRef<OsStr>, V: AsRef<OsStr>,

Set multiple environment variables at once. Order is preserved; later entries win on a duplicated key.

use processkit::Command;
Command::new("tool").envs([("FOO", "1"), ("BAR", "2")]);
Source

pub fn env_clear(self) -> Self

Clear all inherited environment variables before applying any set here.

Opts out of client env defaults: a command that clears its environment is treated as having taken full control of it, so a CliClient’s default_env/ default_env_fn is not gap-filled into it (a client default would otherwise pierce the clean slate). Set any var you still want with an explicit env.

Source

pub fn inherit_env<I, S>(self, names: I) -> Self
where I: IntoIterator<Item = S>, S: AsRef<OsStr>,

Inherit only the named variables from the parent environment — an allow-list on top of an implied env_clear.

The named vars are copied from the parent environment at each spawn (vars the parent lacks are skipped); explicit env / env_remove overrides still apply afterwards. Repeated calls extend the allow-list. Works on every platform.

A client default_env for an allow-listed key is not applied — the command chose to inherit that key from the parent, and a client default must not override it. A client default for a key not in the list still fills (an explicit override layered on top, orthogonal to parent inheritance) — so a client-wide safety default reaches the command. Use env_clear instead to opt out of client env defaults entirely.

Source

pub fn uid(self, uid: u32) -> Self

Run the child as this user id (Unix privilege drop).

Applied by the OS between fork and exec; combine with gid — the group id is set before the user id (once the uid drops, changing gid is no longer permitted), an ordering the standard library guarantees. On non-Unix targets the run fails with Error::Unsupported — a requested privilege drop is never silently skipped.

Linux cgroup caveat: under the cgroup v2 mechanism (Mechanism::CgroupV2) the child joins its cgroup after the OS has dropped the uid, by writing the auto-created (and therefore not target-uid-writable) cgroup.procs — so the spawn currently fails with a permission error rather than producing an uncontained child. Privilege drop composes cleanly with the POSIX process-group mechanism (macOS/BSD, or Linux without cgroup delegation); making it compose with cgroups (e.g. chowning the cgroup to the target uid) is tracked future work.

Source

pub fn gid(self, gid: u32) -> Self

Run the child under this group id (Unix privilege drop) — see uid for ordering and platform notes.

Source

pub fn groups(self, gids: impl AsRef<[u32]>) -> Self

Set the child’s supplementary groups (Unix privilege drop), replacing the inherited set.

This is the missing third leg of a correct privilege drop: dropping the uid/gid alone leaves the child holding the parent’s supplementary groups (often root’s), so it could still reach group-owned resources the target user shouldn’t. Pass the target user’s groups (or [] to drop all extras) alongside uid/gid.

Ordering is handled for you: the OS applies setgroupssetgidsetuid (groups and gid must be set while still privileged, before the uid drops). On non-Unix targets the run fails with Error::Unsupported — never silently skipped. The Linux cgroup-v2 caveat from uid applies unchanged.

Source

pub fn setsid(self) -> Self

Detach the child into a new session (Unix setsid()): no controlling terminal, its own session and process group.

Containment is preserved: the group tracks the new session’s process group (whose id is the child’s pid), so kill-on-drop and the teardown verbs still reach it. On non-Unix targets the run fails with Error::Unsupported.

Honored by the Command-driven launch paths (run/output_*/ start, ProcessGroup::start, pipelines); the low-level raw-command ProcessGroup::spawn escape hatch bypasses these builders.

Source

pub fn priority(self, priority: Priority) -> Self

Launch this child at a lower (or higher) CPU-scheduling priority — for background/batch work that shouldn’t starve the foreground, or a task that should win over it.

Applied on both platforms via the existing spawn seams: Unix setpriority in the same pre_exec hook that carries uid/gid/setsid; Windows a priority-class flag OR’d into creation_flags, the same seam as create_no_window. Unlike the privilege builders this never yields Error::Unsupported — see Priority for why both platforms cover every variant, and the Unix caveat that lowering nice below its inherited value — Priority::AboveNormal/ Priority::High always, and even Priority::Normal under a positively-niced parent — needs CAP_SYS_NICE/root there.

Last-write-wins with an earlier call, like timeout.

Source

pub fn umask(self, mask: u32) -> Self

Set the file-mode creation mask for the child (Unix umask(2)), controlling the default permissions of files it creates.

Applied via pre_exec, alongside setsid/ groups — another knob on that same seam. On non-Unix targets the run fails with Error::Unsupported rather than silently ignoring the requested mask. Only the low permission bits are meaningful (as with the umask(2) syscall itself); pass the value you would give the umask shell builtin, e.g. 0o022.

Source

pub fn kill_on_parent_death(self) -> Self

Kill the direct child if this process dies abruptly — including a SIGKILL of the parent, where Drop never runs to tear the group down. An opt-in hardening on top of the unconditional kill-on-drop containment, best-effort by design:

PlatformEffect
WindowsAlready guaranteed regardless of this knob: the kernel closes the Job Object handle when the parent dies, and kill-on-close takes the whole tree. Documented no-op.
Linuxprctl(PR_SET_PDEATHSIG, SIGKILL) on the direct child only — grandchildren are not covered (with the parent gone, nothing tears the cgroup/pgroup down).
macOS / BSD / otherNo pdeathsig equivalent — does nothing (the graceful-exit guarantee via Drop still holds).

Two honest Linux caveats:

  • The death signal fires when the spawning thread dies, not only the process — on a multi-threaded tokio runtime, a worker thread retired while the child lives would kill it early (for the strongest guarantee spawn from a current-thread runtime). The parent-died-before-arming race is closed in the child by re-checking getppid() against the spawner’s pid captured before the fork — safe in containers where the spawner itself is PID 1.
  • The kernel clears PR_SET_PDEATHSIG across an execve of a set-uid / set-gid binary (a security measure), so this is silently void for a sudo … / setuid child — it inherits the pdeathsig for the tiny window before execve, then loses it. Contain such a child with the kill-on-drop group (the default) rather than relying on this knob.

(Idea borrowed from execa’s cleanup-on-exit, mapped to native primitives.)

Source

pub fn create_no_window(self) -> Self

Spawn without a console window (Windows CREATE_NO_WINDOW) — for a GUI app launching a CLI tool without a flashing terminal.

On non-Windows targets this is a harmless no-op (purely cosmetic — no console windows exist to suppress). Honored by the Command-driven launch paths; the raw ProcessGroup::spawn escape hatch still overwrites creation flags (see its docs).

Source

pub fn stdin(self, stdin: Stdin) -> Self

Provide standard input for the child (see Stdin).

Source

pub fn pipe(self, next: Command) -> Pipeline

Chain this command’s stdout into next’s stdin — the first link of a shell-free Pipeline. Keep chaining with Pipeline::pipe (or the | operator), then drive the whole thing with Pipeline::output_string / Pipeline::run.

Source

pub fn unchecked_in_pipe(self) -> Self

Exempt this command, as a pipeline stage, from pipefail attribution: its unclean exit (non-zero code, signal kill — including SIGPIPE — or its own per-stage timeout kill) is skipped when the chain decides what to report, and never shields a checked stage’s failure. The motivating pattern is producer | head -1: the consumer exits early, the producer dies of SIGPIPE/EPIPE, and without this marker strict pipefail reports that perfectly normal death as the chain’s failure. (Design borrowed from duct’s unchecked() — the idea, not the code.)

Outside a Pipeline this is a no-op: a single run’s status is already plain data in its ProcessResult, and ensure_success stays opt-in — unchecked does not relax it, nor a whole-chain Pipeline::timeout.

Source

pub fn timeout(self, timeout: Duration) -> Self

Kill the run if it exceeds timeout.

Clears a prior no_timeout — the last of the two wins.

Source

pub fn no_timeout(self) -> Self

Run without a timeout, and — unlike simply leaving the timeout unset — opt out of any client-wide default_timeout gap-fill. Use this to say “this one long-running command is deliberately unbounded” against a client that otherwise imposes a deadline on every call (a tail -f, a watch loop, an interactive session).

A plain Command (no client) is already unbounded by default, so this is only meaningful when the command is run through a CliClient with a default_timeout. Clears a prior timeout — the last of the two wins.

Source

pub fn timeout_opt(self, timeout: Option<Duration>) -> Self

Set the timeout from an optional Duration, folding the timeout / no_timeout split into a single composable verb for config-driven call sites. Some(d) is exactly timeout(d); None is exactly no_timeout().

Reach for it when you hold an Option<Duration> (a parsed config value, a caller-supplied override) instead of the match cfg { Some(d) => c.timeout(d), None => c.no_timeout() } dance. Mind the None mapping: it means deliberately unbounded — opting out of a client-wide default_timeout gap-fill, not “leave the timeout unset for a default to fill”. Like the two verbs it folds, it is last-write-wins with any earlier timeout call.

Source

pub fn timeout_grace(self, grace: Duration) -> Self

Make the timeout graceful: at the deadline the run’s tree is sent SIGTERM (or the signal chosen via timeout_signal, with the process-control feature), given up to grace to exit, then SIGKILLed. Without it the deadline hard-kills at once. No effect unless timeout is also set.

Windows has no signal tier: the deadline kills the job atomically regardless of grace. Either way timed_out stays true (the deadline was exceeded), graceful or not.

Source

pub fn timeout_signal(self, signal: Signal) -> Self

Available on crate feature process-control only.

The signal sent at the start of a graceful timeout_grace window (default Signal::Term). Unix-only in effect; ignored on Windows (no signal tier).

This builder lives behind the process-control feature because the Signal type does. Without process-control the graceful timeout always uses SIGTERM (the default); the feature is only needed to choose a different teardown signal — promoting Signal into the base API would enlarge the always-on surface for a niche knob.

Source

pub fn ok_codes(self, codes: impl IntoIterator<Item = i32>) -> Self

Treat these exit codes (not just 0) as success for the checking verbs — run (and run_unit/checked via ProcessRunnerExt) and ProcessResult::ensure_success / is_success. For tools whose non-zero exit is a normal result — grep (1 = no match), diff (1 = differs), rsync’s code families — so callers don’t hand-match.

An empty set is ignored — a no-op that leaves the previously configured codes (or the default [0]) in place, rather than resetting to [0], since an empty accepted-set would make every exit a failure. Does not change exit_code (always the raw code) or probe (always the 0/1 convention).

Source

pub fn cancel_on(self, token: CancellationToken) -> Self

Tie this run to token: cancelling it kills the process tree and makes every consuming path (run/output_string/output_bytes/wait/ exit_code/probe/profile/finish and the streamed finishers) resolve to Error::Cancelled. In a Pipeline, a token on any stage cancels that stage and the cancellation errors the whole pipeline (the private pipeline group tears the other stages down).

Unlike timeout — which is captured in the ProcessResult (timed_out) without erroring on the non-checking paths — a cancellation is always an error, on every path. When both fire, cancellation wins (it is checked first — except in first_line’s narrow tie where the deadline watchdog closes the stream in the same poll the token fires, which surfaces as Timeout). An already-cancelled token short-circuits before spawning. On a private group the whole tree is killed; on a shared group (ProcessGroup::start) only the direct child is, like timeout. Both wait_any and first_line surface a mid-run cancel as Err(Cancelled) — their streaming race resolves the cancellation and tears the child down — as does an already-cancelled token via the pre-spawn short-circuit. A mid-run cancel during wait_for_line, by contrast, closes the stream and surfaces as that probe’s Error::NotReady, not Cancelled — the consuming finisher afterwards still reports Cancelled.

A cancelled run is never retried: retry policies and Supervisor restarts both treat Error::Cancelled as terminal — the token stays cancelled forever, so another attempt could only fail the same way.

On a Command this replaces any previously set token (last write wins) — contrast the gap-fill containers Pipeline::cancel_on and CliClient::default_cancel_on, which leave an explicit per-element token intact.

Source

pub fn retry( self, max_attempts: u32, backoff: Duration, retry_if: impl Fn(&Error) -> bool + Send + Sync + 'static, ) -> Self

Retry the run while retry_if accepts the error, up to max_attempts total attempts, sleeping a fixed backoff between tries. For exponential backoff + cap + jitter, use retry_with.

Applies to the success-checking helpers — run/run_unit/checked/exit_code/probe/parse/try_parse — on Command, on ProcessRunnerExt, and on CliClient: the ones that surface failure as an Error the classifier can inspect (e.g. a transient network failure in stderr, or Error::Timeout). The non-erroring output_string/output_bytes paths don’t retry.

Each attempt re-executes the whole command — a fresh process. Only retry operations that are safe to repeat: a side effect that already landed before the failure (a git push that reached the server, then dropped the connection) will be replayed. Prefer to gate retries on a classifier that matches pre-effect failures (DNS/connection errors, Error::Timeout while still connecting) rather than any non-zero exit.

A timeout bounds each attempt, not the whole retried operation — there is no total wall-clock ceiling across retries (worst case ≈ attempts × timeout + the sum of the backoffs). Bound the total with cancel_on (a Cancelled is terminal — never retried).

Because the command is replayed from scratch, a one-shot stdin source (Stdin::from_reader / from_lines) can’t survive a retry: its payload is consumed by the first attempt and can’t be re-fed. So such a command is not retried at all — the first attempt’s error is returned as-is (retrying would either replay empty stdin or spuriously classify the re-consume), and the retry policy is inert for it. Use a reusable source (from_string/from_bytes/from_file/from_iter_lines) when retrying. (A one-shot source re-run outside this retry loop — a Supervisor incarnation, a pipeline re-run — does fail loud with Error::Io InvalidInput at launch instead.)

Inert outside the success-checking verbs. A retry policy is honored only by the verbs listed above. It is ignored by:

  • Supervisor — supervision is keep-alive restarting with its own RestartPolicy / backoff / storm handling, a different concern from replay-to-success; configure restarts there, not via retry.
  • output_all — a bounded fan-out that collects every outcome as data (no per-command retry); wrap each command’s verb yourself if a batch element must retry.
  • the raw Pipeline verbs — a stage’s retry does not re-run that stage within the chain.

Counting: max_attempts is the total number of runs (so retry(3, …) runs at most three times: the first plus two more). max_attempts of 0 and 1 both mean a single run with no retry — a command always runs at least once, so 0 does not mean “never run”. For exponential backoff + cap + jitter instead of a fixed delay, use retry_with, which takes a RetryPolicy — note that a RetryPolicy counts max_retries (the runs after the first), so retry(3, …) corresponds to RetryPolicy::new().max_retries(2).

Source

pub fn retry_with( self, policy: RetryPolicy, retry_if: impl Fn(&Error) -> bool + Send + Sync + 'static, ) -> Self

Retry on a rich RetryPolicyexponential backoff + cap + jitter — instead of the fixed (max_attempts, backoff) of retry. The per-command analogue of CliClient::default_retry, with the same applicability and replay caveats as retry. Note RetryPolicy counts max_retries (after the first attempt), whereas retry counts max_attempts (total).

Source

pub fn retry_never(self) -> Self

Opt out of retries entirely: run this command exactly once and suppress any client-wide default_retry gap-fill.

The explicit, symmetric counterpart to no_timeout: a bare Command already retries nothing, so this is only meaningful against a CliClient whose default_retry would otherwise be filled in — it pins “run this one command once, whatever the client policy”. Tidier than, and behaviorally identical to, the retry(1, Duration::ZERO, |_| false) idiom (one attempt, a classifier that accepts nothing). Last-write-wins with any earlier retry / retry_with.

Source

pub fn keep_stdin_open(self) -> Self

Leave stdin open after start so the child can be driven interactively via RunningProcess::take_stdin. Takes precedence over a stdin source — when set, that source is ignored and the pipe is handed to the caller instead.

The open pipe lives until the caller takes it (take_stdin) or a consuming verb runs: at consume time an untaken pipe is closed (nothing could ever write to it again), so a stdin-reading child sees EOF instead of blocking — combining keep_stdin_open with a bulk helper (output_string, run, …) without ever taking the writer is equivalent to not setting it. A writer the caller did take is unaffected and keeps the pipe until dropped or finished.

Mutually exclusive with inherit_stdin — a child cannot both be handed an interactive stdin pipe and share the parent’s stdin; setting both is rejected at launch (see inherit_stdin).

Source

pub fn inherit_stdin(self) -> Self

Give the child the parent’s own standard input — it reads directly from whatever this process’s stdin is connected to (a terminal, a file, a pipe) rather than from a crate-managed pipe.

This is the stdin counterpart of stdout(StdioMode::Inherit) / stderr(StdioMode::Inherit): the child shares the parent stream instead of the crate mediating it. Reach for it when a child must talk to the real terminal — git commit opening $EDITOR, a tool prompting the user for a password or a yes/no, or simply forwarding the parent’s piped stdin straight through to the child. Until a pseudo-terminal exists (a future direction, not yet provided) this covers the common non-tty-negotiating interactive cases without the crate having to pump bytes.

Because the child reads the parent’s stdin directly, the crate neither feeds nor captures that input, and there is no writer to take_stdin (it returns None, as for a non-keep_stdin_open run). stdout/stderr are unaffected — capture and streaming of the child’s output keep working exactly as before.

§Mutually exclusive with a mediated stdin

Inheriting the parent’s stdin cannot be combined with either way the crate would otherwise drive stdin — a configured stdin source (Stdin::from_string/from_bytes/from_file/from_reader/from_lines, or an explicit Stdin::empty()) or keep_stdin_open’s interactive pipe. Setting inherit_stdin and one of those is a contradiction (feed the child a source and let it read the terminal?), so it is rejected at the launch boundary with a typed Error::Io (InvalidInput) — the same failure mode as the other stdin misconfiguration the crate refuses (re-running a consumed one-shot source) — rather than silently letting one win. Drop the other stdin knob to resolve it.

Source

pub fn on_stdout_line<F>(self, handler: F) -> Self
where F: Fn(&str) + Send + Sync + 'static,

Invoke handler for each decoded stdout line as it is read (in addition to capture/streaming). Runs on the pump task; keep it cheap. A handler that panics is caught and disabled for the rest of the run — the child is still drained and the result still carries every line (the panic is reported as a tracing warn when that feature is on).

Ordering guarantees: invocations are FIFO within a stream; there is no ordering between stdout and stderr handlers (two independent pumps). On the consuming verbs (run/output_*/wait/profile/ finish) all handler invocations happen-before the awaited future resolves — a progress bar can be finalized the moment the call returns. (One documented exception: when a leaked pipe is held open past the child’s death, teardown aborts the pump after a bounded grace, cutting any not-yet-delivered lines along with their handler calls.) On a streamed run, stdout handlers quiesce when the stdout_lines stream ends.

At most one handler per stream: a repeat call replaces the previous one (builder semantics, like timeout). To fan out, compose inside a single closure.

Requires stdout to be Piped (the default): the handler runs on the capture pump, so it never fires under stdout(Inherit) / stdout(Null).

Byte cap caveat: a single line whose length exceeds a byte cap (with_max_bytes) is never assembled, so the handler never sees it either — it is silently skipped for every sink (handler, tee, and capture buffer alike), counted only via the truncation/dropped() signal. If every line matters, leave the byte cap unset, or use a line cap instead.

Source

pub fn on_stderr_line<F>(self, handler: F) -> Self
where F: Fn(&str) + Send + Sync + 'static,

Invoke handler for each decoded stderr line as it is read.

Same contract as on_stdout_line: runs on the pump task, and a repeat call replaces the previous handler.

Source

pub fn stdout(self, mode: StdioMode) -> Self

Set how the child’s standard output stream is connected (default: StdioMode::Piped).

  • Piped (default) — captured into a pipe; all output-retrieval verbs (output_string, stdout_lines, …) read from it.
  • Inherit — the child shares the parent’s stdout; output appears in the terminal/log but is not captured.
  • Null — suppressed entirely (redirected to /dev/null).

With Inherit/Null there is no pipe to read, so the bulk capture verbs (output_string/output_bytes) error rather than return silently-empty output, and the streaming verbs (stdout_lines/ output_events) yield an empty stream. Use a discard verb (wait) to run a command whose stdout you don’t want to capture.

Source

pub fn stderr(self, mode: StdioMode) -> Self

Set how the child’s standard error stream is connected (default: StdioMode::Piped).

Same semantics as stdout: Piped captures, Inherit passes through, Null suppresses.

Source

pub fn stdout_tee<W>(self, writer: W) -> Self
where W: AsyncWrite + Send + Unpin + 'static,

Tee every decoded stdout line to writer as it is produced — capture and stream to writer simultaneously.

writer is an async sink (tokio::io::AsyncWrite); each decoded line is written to it followed by \n. The write is awaited on the capture pump, so a slow sink applies backpressure (the pump slows, the OS pipe fills, the child blocks on its next write) rather than blocking the runtime. The sink must make forward progress, though: a destination that blocks forever (not merely slow) stalls the pump — no further lines are buffered and a live stdout_lines/output_events consumer parks — until the run’s teardown grace aborts the pump. A write error disables the tee for the rest of the run — surfaced as a tracing warn under the tracing feature, not silently swallowed — and capture is unaffected.

Runs independently of on_stdout_line: set both and both fire per line (the tee no longer replaces the handler). A second stdout_tee replaces an earlier one.

Shared across clones and attempts. The sink is held in an Arc<Mutex<…>>, so cloning the Command shares one sink — and a Command is cloned for every Pipeline stage, every Supervisor incarnation, and every retry attempt. Concurrent clones (pipeline stages running at once) interleave their lines into it; sequential re-runs (retries, restarts) append — a retried command’s sink accumulates the failed attempt’s output followed by the successful one’s, with no delimiter. For per-run or per-attempt separation, tee to distinct sinks (a fresh Command per run) or have the sink write its own delimiters.

The tee fires before the buffer policy decides retention, so it sees every decoded line — including ones the capture buffer then drops or rejects, e.g. output past a fail_loud line ceiling (that ceiling bounds retained memory, not what streams past). One exception: a single line whose length exceeds a byte cap (with_max_bytes) is never assembled, so it is neither retained nor teed — nor delivered to on_stdout_line: the byte cap silently skips that line for every sink alike, counted only via the truncation/dropped() signal. Leave the byte cap unset (or use a line cap) if every line must reach the tee. The discard verbs (wait / profile) apply a large internal in-flight byte cap for the same memory bound, so a line exceeding it is likewise not teed under those verbs.

Requires stdout to be Piped (the default): the tee fires from the capture pump, so it is a no-op under stdout(Inherit) / stdout(Null), which run no pump. It is likewise inert under output_bytes, which captures stdout raw (no line pump) — reach for a stdout tee with the line verbs (output_string, start + stdout_lines, output_events).

Source

pub fn stderr_tee<W>(self, writer: W) -> Self
where W: AsyncWrite + Send + Unpin + 'static,

Tee every decoded stderr line to writer as it is produced.

Same contract as stdout_tee — an async tokio::io::AsyncWrite sink, awaited on the pump (backpressure, not runtime-blocking), independent of on_stderr_line, and requiring stderr to be Piped.

Source

pub fn output_buffer(self, policy: OutputBufferPolicy) -> Self

Cap the in-memory backlog of captured output lines (see OutputBufferPolicy). The pump still drains the pipe; only retention is bounded.

Source

pub fn stdout_encoding(self, encoding: &'static Encoding) -> Self

Decode stdout with encoding instead of UTF-8 (e.g. encoding_rs::SHIFT_JIS).

Source

pub fn stderr_encoding(self, encoding: &'static Encoding) -> Self

Decode stderr with encoding instead of UTF-8.

Source

pub fn encoding(self, encoding: &'static Encoding) -> Self

Decode both stdout and stderr with encoding.

Source

pub fn line_terminator(self, terminator: LineTerminator) -> Self

Choose where the line pump splits both streams into lines (see LineTerminator). The default is LineTerminator::Newline — split on \n only, unchanged from before this knob existed.

Pass LineTerminator::CarriageReturn to also treat a bare \r as a line terminator, so carriage-return progress output (curl/pip/apt: a bar redrawn in place with \r, no \n until the end) streams live, one frame at a time instead of piling up as a single line that only surfaces at EOF. In that mode each \r-delimited frame is a line for every line sink alike — stdout_lines / output_events, the on_stdout_line/on_stderr_line handlers, the stdout_tee/stderr_tee sinks, and output_string — so there is a single, shared notion of a line. A \r\n pair stays one terminator (no empty line between them), and the OutputBufferPolicy byte cap now bounds an individual runaway frame rather than dropping the whole stream.

Set it per stream with stdout_line_terminator / stderr_line_terminator when only one stream carries progress output (progress usually lands on stderr, while stdout stays newline-structured data).

Source

pub fn stdout_line_terminator(self, terminator: LineTerminator) -> Self

Choose where the line pump splits stdout into lines (see LineTerminator); the stderr framing is left untouched. See line_terminator for both streams at once.

Source

pub fn stderr_line_terminator(self, terminator: LineTerminator) -> Self

Choose where the line pump splits stderr into lines (see LineTerminator); the stdout framing is left untouched. Handy when progress output lands on stderr while stdout stays newline-structured.

Source

pub fn program(&self) -> &OsStr

The program to launch.

Source

pub fn arguments(&self) -> &[OsString]

The arguments, in order.

Source

pub fn command_line(&self) -> String

Render this command as a single shell-quoted line for display — logs, error messages, a dry-run echo. Quoting is per-platform (POSIX single-quote / Windows double-quote) and is for readability, not execution: the crate never invokes a shell, and the rendering is not guaranteed to round-trip through one. Do not feed the output back to a shell to re-run the command — the escaping targets human legibility, not any specific shell’s parsing rules.

The line includes the arguments, which may carry secrets (a --token=… flag). Unlike the tracing feature — which never logs argv — this is opt-in: render it only into a sink you control.

Source

pub fn working_dir(&self) -> Option<&Path>

The working-directory override, if one was set.

Source

pub fn env_overrides(&self) -> &[(OsString, Option<OsString>)]

The environment overrides, in order (a None value removes the variable).

Source

pub fn stdin_source(&self) -> Option<&Stdin>

The configured stdin source, if any.

Source

pub fn configured_timeout(&self) -> Option<Duration>

The configured deadline, if any — Some(d) for a timeout(d), None for both an unset timeout and an explicitly no_timeout (neither imposes a deadline).

Source

pub async fn start(&self) -> Result<RunningProcess>

Start the command and return a live RunningProcess backed by a fresh private group. Use this for streaming stdout (RunningProcess::stdout_lines) or inspecting the process while it runs; keep the handle in scope, as dropping it tears the tree down.

§Errors

The launch surface shared by every run verb on Command:

  • Error::NotFound — the program could not be located (not installed, not on PATH, or the given path does not resolve to an executable).
  • Error::Spawn — the program was located but the OS refused to start it (permission denied, a missing or non-directory working directory, a Windows .cmd/.bat that needs cmd.exe, …).
  • Error::Unsupported — a requested POSIX-only primitive (running as another user/group, a new session via setsid, or a umask) is not available on this platform.
  • Error::Cancelled — the cancel_on token was already cancelled before the spawn.
  • Error::Io — the private ProcessGroup backing the run could not be created, or a one-shot streaming stdin source (Stdin::from_reader / Stdin::from_lines) was already consumed by a previous run.
  • Error::ResourceLimit — a resource cap configured on the run’s group could not be enforced.
Source

pub async fn output_string(&self) -> Result<ProcessResult<String>>

Run to completion and capture stdout as text, stderr, and the exit code. A non-zero exit is reported, not raised — call ProcessResult::ensure_success to turn it into an error.

§Errors

The launch failures listed on start. A non-zero exit, a timeout, and a signal-kill are captured in the returned ProcessResult rather than raised (call ensure_success to promote them); beyond launch, only Error::Cancelled (a cancellation is always raised), Error::OutputTooLarge (a fail-loud buffer overflowed), Error::Stdin (a non-broken-pipe stdin failure on an otherwise-successful run), and Error::Io surface.

Source

pub async fn output_bytes(&self) -> Result<ProcessResult<Vec<u8>>>

Run to completion and capture stdout as raw bytes (plus stderr/exit code).

§Errors

Identical to output_string — a non-zero exit, a timeout, or a signal-kill is captured in the ProcessResult, not raised — except that a fail-loud Error::OutputTooLarge applies to the raw stdout byte ceiling.

Source

pub async fn exit_code(&self) -> Result<i32>

Run to completion and return just the exit code (output is discarded). A run that yields no code surfaces as an error — a timeout as Error::Timeout, a signal-kill as Error::Signalled — consistent with ProcessRunnerExt::exit_code and CliClient::exit_code.

§Errors

The launch failures listed on start, plus — when the run produced no code — Error::Timeout (the deadline elapsed), Error::Signalled (killed by a signal), or Error::Cancelled. A non-zero exit is returned as the code, not raised.

Source

pub async fn run(&self) -> Result<String>

Run to completion, requiring an accepted exit (0 by default, widened by ok_codes), and return trimmed stdout. Any other code is Error::Exit.

§Errors

The launch failures listed on start, plus the success-checking failures: Error::Exit (a non-accepted exit code), Error::Signalled (a signal-kill), Error::Timeout (the deadline elapsed — raised here, unlike on output_string), Error::Cancelled, Error::OutputTooLarge (a fail-loud buffer truncated the presented stdout), and Error::Stdin (a non-broken-pipe stdin failure on an otherwise-successful run).

Source

pub async fn checked(&self) -> Result<ProcessResult<String>>

Run to completion, require an accepted exit, and return the full captured ProcessResult (untrimmed stdout) — the building block when you need the whole result after success-checking rather than trimmed stdout (run) or the raw result (output_string). Consistent with ProcessRunnerExt::checked and CliClient::checked.

§Errors

The same success-checking surface as runError::Exit / Error::Signalled / Error::Timeout / Error::Cancelled / Error::Stdin, atop the launch failures on start — except that, as the lenient building block, checked does not fail loud on a bounded-buffer truncation (inspect ProcessResult::truncated yourself), so it never returns Error::OutputTooLarge.

Source

pub async fn run_unit(&self) -> Result<()>

Run for the side effect: require an accepted exit (0, or any code in ok_codes) and discard the output. Consistent with ProcessRunnerExt::run_unit and CliClient::run_unit.

§Errors

The same surface as checked (the launch failures on start plus Error::Exit / Error::Signalled / Error::Timeout / Error::Cancelled / Error::Stdin); only the captured output is discarded.

Source

pub async fn probe(&self) -> Result<bool>

Run a predicate command and read its exit code as a boolean: exit 0Ok(true), exit 1Ok(false), anything else → Err (any other code as Error::Exit, a timeout as Error::Timeout, a signal-kill as Error::Signalled). For tools whose exit code is the answer — git diff --quiet, git show-ref --verify --quiet, grep -q, …

§Errors

Any exit code other than 0/1 becomes Error::Exit, and — atop the launch failures on start — a run that produced no code errors as Error::Timeout, Error::Signalled, or Error::Cancelled. The strict 0/1 contract holds regardless of the command’s ok_codes.

Source

pub async fn parse<T, F>(&self, parse: F) -> Result<T>
where T: Send, F: FnOnce(&str) -> T + Send,

Run (requiring an accepted exit) and feed stdout to an infallible parse closure, returning the parsed value. Fails loud on a bounded-buffer truncation so the parser never sees a clipped tail. Consistent with ProcessRunnerExt::parse and CliClient::parse.

§Errors

The success-checking surface of run (the launch failures on start, plus Error::Exit / Error::Signalled / Error::Timeout / Error::Cancelled / Error::Stdin), plus Error::OutputTooLarge when a fail-loud buffer truncated the stdout the parser would see. The parse closure is infallible, so it adds no error.

Source

pub async fn try_parse<T, F>(&self, parse: F) -> Result<T>
where T: Send, F: FnOnce(&str) -> Result<T> + Send,

Run (requiring an accepted exit) and feed stdout to a fallible parse closure (the JSON-deserialization shape; a failure becomes Error::Parse or whatever the closure returns). Fails loud on truncation. Consistent with ProcessRunnerExt::try_parse and CliClient::try_parse.

§Errors

Everything parse can return, plus whatever the fallible parse closure yields on malformed output — typically Error::Parse.

Source

pub async fn first_line<F>(&self, predicate: F) -> Result<Option<String>>
where F: Fn(&str) -> bool + Send,

Return the first stdout line matching predicate (or the first line when the predicate is trivial), then tear the process down.

§Errors

The launch failures listed on start, plus Error::Timeout when a timeout is set and its deadline elapses mid-stream (which tears the process down), Error::Cancelled, or Error::Io while streaming. A stream that ends with no match is Ok(None), not an error.

Source

pub fn resolve_program(&self) -> Result<PathBuf>

Resolve this command’s program to a concrete executable path without launching it — a spawn-free preflight, for a doctor / early-diagnosis check (“is git installed?”) that must have no side effects. Unlike probe (which actually runs the tool), this only locates it — no process is ever started.

Resolution is byte-for-byte the same as the one the real launch performs, because it reuses the same internal logic — not a second copy: a bare name is resolved against this command’s prefer_local directories first (in priority order), then the PATH, honoring PATHEXT on Windows and the execute bit on Unix; a path-form program (absolute, or relative with a separator) is probed directly, exactly as the OS receives it. When the command has relocated the child’s PATH (env/env_remove of PATH, env_clear, or inherit_env), the lookup runs against that effective child PATH, so preflight never disagrees with what the spawn would actually find.

On success returns the resolved absolute path. This is a synchronous, cheap filesystem probe (a few stats) — no async runtime is required.

§Errors

Error::NotFound when the program can’t be located — not installed, not on PATH, or a path that doesn’t resolve to an executable. Its searched field lists the directories that were checked (prefer_local first, then PATH) for a bare-name lookup, and is None for a path-form program; is_not_found classifies it, exactly as it would for the same missing program on a real run.

Trait Implementations§

Source§

impl BitOr for Command

a | b — sugar for Command::pipe. Parenthesize the chain before a terminal verb, since method calls bind tighter than |.

Source§

type Output = Pipeline

The resulting type after applying the | operator.
Source§

fn bitor(self, rhs: Command) -> Pipeline

Performs the | operation. Read more
Source§

impl BitOr<Command> for Pipeline

pipeline | c — sugar for Pipeline::pipe, so a | b | c chains left-associatively into one pipeline.

Source§

type Output = Pipeline

The resulting type after applying the | operator.
Source§

fn bitor(self, rhs: Command) -> Pipeline

Performs the | operation. Read more
Source§

impl Clone for Command

Source§

fn clone(&self) -> Command

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Command

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<R: ProcessRunner> IntoCommand<R> for Command

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Any for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Source§

fn type_name(&self) -> &'static str

Source§

impl<T> AnySync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more