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, ErrorReason::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 ErrorReason::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 ErrorReason::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 ErrorReason::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 ErrorReason::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 cpu_affinity(self, cpus: impl IntoIterator<Item = usize>) -> Self

Restrict the child to the given logical CPU indices. Descendants inherit the mask, so this initially constrains the whole process tree (a child may still change its own affinity if the OS permits it).

On Linux the mask is applied with sched_setaffinity(2) in the pre-exec child, before user code runs. On Windows it is applied with SetProcessAffinityMask while the child is still suspended between job assignment and resume; the ConPTY path uses the same ordering. macOS, BSD, and other targets return ErrorReason::Unsupported rather than silently inheriting the parent’s mask.

An empty set, a Linux index beyond cpu_set_t, or a Windows index beyond the process mask width fails before the child runs. Windows’ mask API is limited to one processor group; indices are therefore bounded by the native pointer width, and the OS rejects processors unavailable to the current group. Repeated calls are last-write-wins; duplicates are removed and indices are stored in ascending order.

This owner-dependent configuration is refused by spawn_detached. On Windows it is also unavailable through to_tokio_command, because a raw command has no post-spawn suspended-child configuration seam; use a high-level run verb instead.

Source

pub fn io_priority(self, priority: IoPriority) -> Self

Set the Linux I/O-scheduling priority for this child, so background disk work can yield to foreground users.

Applied with ioprio_set(2) in the child before exec, on the same pre_exec seam as priority and umask. Linux is the only supported platform: Windows, macOS, BSD, and other Unix targets fail with ErrorReason::Unsupported rather than silently inheriting the caller’s I/O priority. See IoPriority for the Linux classes, data range, and privilege caveat.

This configuration is owner-dependent and is therefore refused by spawn_detached. 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 ErrorReason::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).

The reach of this hardening on the current platform is reported honestly by kill_on_parent_death_scope as a ParentDeathCleanupWholeTree on Windows, DirectChildOnly on Linux, Unsupported on macOS/BSD — so a caller can surface the real scope instead of overpromising a whole-tree guarantee. This is best-effort hardening for abrupt owner death only; ordinary graceful teardown (Drop) still kills the whole tree everywhere.

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 const fn kill_on_parent_death_scope() -> ParentDeathCleanup

The scope of whole-tree cleanup kill_on_parent_death actually achieves on this build’s target platform when the owner dies abruptly — a ParentDeathCleanup capability report, so a caller can state the real reach of the hardening rather than overpromising a whole-tree guarantee the OS cannot keep.

WholeTree on Windows (the kernel closes the Job Object handle on owner death and kill-on-close reaps the whole tree), DirectChildOnly on Linux (PR_SET_PDEATHSIG reaches only the direct child; grandchildren survive), and Unsupported on macOS / the BSDs (no pdeathsig equivalent). Fixed per target at build time — it does not depend on whether kill_on_parent_death was called or on any runtime state — so it is an associated function, not a method on a built command. See ParentDeathCleanup for the full contract; note it describes only the abrupt-death path, since ordinary graceful teardown (owner exits/panics, so Drop runs) kills the whole tree everywhere.

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 windows_graceful_ctrl_break(self) -> Self

Opt in to a graceful Windows teardown via a console CTRL_BREAK event, giving a console child a chance to shut down cleanly instead of being hard-killed at once.

A graceful timeout (timeout_grace) or a group shutdown already posts WM_CLOSE to any top-level window a live member owns, so a windowed child (Electron app, desktop tool, windowed service) drains cleanly with no opt-in. A console child has no window, and Windows has no POSIX signal to reach it, so without this opt-in its graceful teardown collapses to the atomic Job Object kill — there is nothing to trigger a clean exit. With this opt-in the direct child is spawned in its own console process group (CREATE_NEW_PROCESS_GROUP), and at graceful teardown it is sent GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) before the grace window: a child that installs a CTRL_BREAK handler (as many CLIs, Node, Python, and Go services do) can flush and exit within the grace. Any survivor still running when the grace elapses is then TerminateJobObject’d — the same hard-kill fallback as before, so containment is never weakened.

§Boundaries (read these)
  • Console-only. The event is delivered through the console this process shares with the child. A child spawned create_no_window (or DETACHED_PROCESS) does not share that console, so it never receives the CTRL_BREAK and simply rides the grace to the TerminateJobObject fallback. A GUI / service parent with no console of its own can’t deliver the event either.
  • CTRL_BREAK, not CTRL_C. CREATE_NEW_PROCESS_GROUP disables CTRL_C for the new group by default; CTRL_BREAK is always deliverable, which is why it is the event sent. timeout_signal (Unix’s signal choice) does not apply — Windows always sends CTRL_BREAK.
  • Direct child only. Only the process launched by this run is a group leader; its own descendants receive the event via the shared console and group, but an adopted child (not spawned here) is not addressed and falls back to the hard kill.
  • No-op off Windows. On Unix the graceful tier already sends a real signal, so this builder does nothing there.

Honored by the Command-driven launch paths (run helpers, start, the run-level graceful timeout_grace, and a shared ProcessGroup’s shutdown); the raw ProcessGroup::spawn escape hatch, which overwrites creation flags wholesale, does not participate.

Source

pub fn use_pty(self) -> Self

Available on crate feature pty only.

Spawn the child under a pseudo-terminal (PTY) instead of three independent pipes, so tools that demand a controlling terminal work.

Off by default. When set, the child is launched over a single PTY master — openpty on Unix, CreatePseudoConsole (ConPTY) on Windows — so isatty() reports a terminal and a program that refuses to run (or that hangs waiting for a prompt) without one behaves normally: an isatty()-gated agentic CLI, an ssh/sudo password/passphrase prompt, a credential helper. This is a minimal single-master-fd mode, not a general terminal emulator.

§stdout and stderr are merged

A PTY has one master fd carrying the child’s combined output, so in this mode stdout and stderr are merged and can no longer be separated. The merged stream is delivered exactly where stdout normally is (output_string, stdout_lines, on_stdout_line, stdout_tee, …); on_stderr_line is never called and stderr_tee never receives anything, because there is no separate stderr to deliver. ProcessResult::stderr is empty for a PTY run. If you need stdout and stderr apart, do not use PTY mode.

§Interactive input

keep_stdin_open plus take_stdin drive the master’s input side exactly as with a pipe, and a configured stdin source is written to it. On Unix the PTY line discipline’s terminal echo is disabled so a written password is not echoed back into the merged output (see ProcessStdin); the Windows ConPTY has no portable per-write echo control, so that guarantee is Unix-only.

§Line framing default (\r-aware) and output hygiene

A PTY child writes like a terminal: CRLF line endings, progress bars redrawn in place with a bare \r (no \n until the end), and VT/ANSI escape sequences (colors, cursor moves, alternate screen, OSC titles). Two deliberate, coded decisions handle this — one automatic, one opt-in:

  • Framing is auto \r-aware. Under use_pty the effective default line_terminator is CarriageReturn instead of Newline, so each progress redraw surfaces as its own line rather than piling into one ever-growing string a naive consumer never sees framed. This is a non-destructive reframing (it only changes where lines split), so it is the sensible default for the mode; a \r\n still counts as one terminator, so ordinary CRLF text reads unchanged. It is applied only when you have not pinned a terminator yourself — an explicit line_terminator (even Newline) always wins, order-independently of use_pty.
  • Escape sanitization stays opt-in. Stripping VT/ANSI escapes is destructive (it removes bytes from the captured output), so it is not turned on automatically — reach for sanitize_vt when you want the merged output de-escaped for line predicates and transcripts. Leaving it off keeps the PTY’s bytes verbatim in the backlog.
§Containment is unchanged

The PTY child is placed in the same Job Object / cgroup / process group as any other child, so whole-tree kill-on-drop, timeouts, and cancellation behave identically — the pseudo-terminal only changes the I/O wiring, never the teardown guarantee.

Available only with the pty crate feature. A note for callers who leave it unset: without this the existing three-pipe behavior — including the Newline framing default — is byte-for-byte unchanged.

§Terminal identity and environment overrides

At spawn, ProcessKit identifies the terminal it creates: Unix children receive TERM=xterm-256color, and every PTY child receives COLUMNS and LINES matching the initial PTY geometry (80×24 by default, or the size set with pty_size). Windows does not synthesize TERM: ConPTY exposes VT handling through the Windows console APIs, so any inherited TERM remains governed by the normal environment rules.

These values are defaults, not forced overrides. Explicit env or env_remove operations for TERM, COLUMNS, or LINES always win, including with env_clear or inherit_env.

Source

pub fn pty_size(self, cols: u16, rows: u16) -> Self

Available on crate feature pty only.

Set the initial window sizecols columns by rows rows — of the pseudo-terminal opened by use_pty.

The size matters to terminal-aware children: it drives line wrapping, progress-bar/TUI layout, and pager behavior, and is the geometry a child reads back with an isatty/TIOCGWINSZ-style query. Without this the PTY opens at the conventional 80×24 default. At spawn, the child’s COLUMNS and LINES environment defaults are set to the same values; explicit env / env_remove operations for either name win. A later RunningProcess::resize_pty changes the live terminal geometry but cannot rewrite an already-running process’s environment.

§Only meaningful with use_pty

This is a PTY-only knob. On a command that is not use_pty it is a documented no-op — the three-pipe launch has no terminal to size, so the value is simply never read (it is not silently applied anywhere, and it is not an error to set; it just does nothing). Order-independent: pty_size(..).use_pty() and use_pty().pty_size(..) are equivalent.

§Live resize

To change the size of an already-running session (e.g. propagating a host window resize / SIGWINCH), use RunningProcess::resize_pty.

Available only with the pty crate feature.

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 inactivity_timeout(self, idle: Duration) -> Self

Kill the run when neither stdout nor stderr produces bytes for idle.

Unlike timeout, this is a resettable deadline: every successful read from either output stream grants a fresh idle window. The initial window starts when the child is spawned, so a process that never writes output is still bounded. Teardown uses the same timeout_grace, timeout_signal (when the process-control feature is enabled), and whole-tree containment path as the absolute timeout.

The result is reported as Outcome::InactivityTimedOut, distinct from Outcome::TimedOut. A zero duration is valid and expires as soon as the watchdog is polled.

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 either timeout or inactivity_timeout graceful: when the winning watchdog fires 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 watchdog hard-kills at once. No effect unless at least one timeout is set.

Windows has no POSIX signal tier, but two best-effort soft triggers run before the atomic kill when the tree can act on one: WM_CLOSE is posted to every top-level window owned by a live member (a windowed child — Electron app, desktop tool, windowed service — can then close and drain within grace), and a child opted into windows_graceful_ctrl_break is sent a console CTRL_BREAK. A tree with neither a window nor that opt-in has nothing to trigger a soft exit, so the deadline hard-kills the job at once, grace unused — timings unchanged from before this soft tier. Any survivor still running when grace elapses is TerminateJobObject’d. 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 ErrorReason::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 ErrorReason::NotReady, not Cancelled — the consuming finisher afterwards still reports Cancelled.

A cancelled run is never retried: retry policies and Supervisor restarts both treat ErrorReason::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 ErrorReason::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, ErrorReason::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).

A one-shot stdin source (Stdin::from_reader / from_lines) feeds a single run, so a retry re-feeds it only when the failed attempt is guaranteed not to have consumed it. The launch reserves that payload transactionally and commits it only once a child exists, so a failure before any child was spawnedNotFound, Spawn (e.g. a transient ETXTBSY that is_transient accepts), or Unsupported — rolls the reservation back and leaves the payload intact: such a command is retried (subject to the classifier) and the next attempt feeds the untouched source. Any other error may have reached a live child that already consumed the source — a non-zero Exit, Timeout, Signalled, a stdin-write Stdin failure, OutputTooLarge, or the ambiguous Io (which arises both before and after a child) — so the first attempt’s error is returned as-is, not retried (a retry would either replay empty stdin or spuriously classify the re-consume). Use a reusable source (from_string/from_bytes/from_file/ from_iter_lines) to retry unconditionally. (A one-shot source re-run outside this retry loop — a Supervisor incarnation, a pipeline re-run — does fail loud with ErrorReason::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 ErrorReason::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/ events) yield an empty stream. Use a discard verb (wait) to run a command whose stdout you don’t want to capture. Calling this after stdout_file restores a normal stdio mode.

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_file(self, path: impl AsRef<Path>) -> Self

Redirect stdout directly to path, creating or truncating the file at spawn time. The child owns the descriptor, so no parent-side pump or output buffer is involved.

Capture and streaming verbs require a pipe and therefore reject this configuration; use wait or another discard verb. For a shared supervisor log across restarts, use stdout_file_append.

Source

pub fn stdout_file_append(self, path: impl AsRef<Path>) -> Self

Redirect stdout directly to path, creating it when absent and appending on every spawn. This is useful for a Supervisor whose incarnations should share one log file.

Source

pub fn stdout_file_truncate(self, path: impl AsRef<Path>) -> Self

Explicit spelling of stdout_file, for code that selects append versus truncate at the call site.

Source

pub fn stderr_file(self, path: impl AsRef<Path>) -> Self

Redirect stderr directly to path, creating or truncating the file at spawn time. The child owns the descriptor, so no parent-side pump or output buffer is involved.

Source

pub fn stderr_file_append(self, path: impl AsRef<Path>) -> Self

Redirect stderr directly to path, creating it when absent and appending on every spawn. See stdout_file_append for the restart-log use case.

Source

pub fn stderr_file_truncate(self, path: impl AsRef<Path>) -> Self

Explicit spelling of stderr_file, for code that selects append versus truncate at the call site.

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/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; drain instead honors this configured byte cap, so a line exceeding the configured max_bytes is the one skipped there.

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, 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 stdout_raw_tee<W>(self, writer: W) -> Self
where W: AsyncWrite + Send + Unpin + 'static,

Tee the child’s stdout to writer byte for byte, before any decoding or line splitting — a transparent passthrough that hands the consumer the child’s exact bytes.

Where stdout_tee writes decoded lines (each plus a \n), this writes each chunk exactly as read from the pipe, ahead of the decoder. That is the difference between a log tee and a faithful wrapper: the raw tee neither loses nor invents a single byte, so a consumer can forward the stream live and hash/capture the exact output. Concretely, unlike the decoded tee it preserves:

  • non-UTF-8 bytes — a child writing binary to stdout (git archive, tar -cz -, ffmpeg … -) is teed verbatim, not mangled into U+FFFD replacement characters;
  • CRLF and lone \r — no newline normalization, no line framing;
  • a missing final newline — the last bytes are teed as-is, never given a fabricated \n;
  • an unterminated promptPassword: (no newline) reaches the sink the moment it is read, rather than waiting in the decode buffer until EOF the way a decoded line does, so an interactive child does not read as hung;
  • a line the capture policy drops — a line past a with_max_bytes byte cap is skipped from every decoded sink (the buffer, the handler, and stdout_tee), but its bytes still reach the raw tee whole — the digest a wrapper computes over the raw tee covers the child’s actual output, not a truncated re-encoding.

Chunks arrive in FIFO order within the stream. This is strictly additive: the decoded-line path — capture buffer, its OutputBufferPolicy, the dropped() truncation accounting, on_stdout_line, and stdout_tee — is unchanged whether or not a raw tee is set, and all configured sinks fire independently.

§Backpressure and memory

writer is an async tokio::io::AsyncWrite. Each raw write is awaited on the capture pump, the same backpressure seam as stdout_tee: a slow sink slows the pump, the OS pipe fills, and the child blocks on its next write. Nothing is buffered in the crate between the pipe and the sink, so a lagging raw consumer cannot grow unbounded in-flight memory — the bound is the OS pipe, not a heap queue. A destination that blocks forever (not merely slow) stalls the pump until teardown’s grace aborts it, exactly as for the decoded tee. A write error disables the raw tee for the rest of the run (a tracing warn under the tracing feature, not silently swallowed); the decoded path is unaffected. The sink is flushed once at stream end, so a buffering writer (BufWriter, a file) commits its tail.

Shared across clones and attempts, like stdout_tee: the sink is held in an Arc<Mutex<…>>, so cloned Commands (pipeline stages, supervisor incarnations, retry attempts) share one sink — concurrent clones interleave their bytes, sequential re-runs append. Tee to distinct sinks for per-run separation. A second stdout_raw_tee replaces an earlier one.

§Requires a piped stdout; inert on the raw-capture verb

The raw tee fires from the line capture pump, so — like stdout_tee — it is a no-op under stdout(Inherit) / stdout(Null) and a stdout_file redirect (all of which run no capture pump); the builder accepts the combination but simply never invokes the sink, rather than rejecting it. It is likewise inert under output_bytes, whose own return value already is the exact raw stdout (a separate raw drain, no line pump) — reach for the raw tee with the line/streaming verbs (output_string, start + stdout_lines / events, wait / drain) when you need the raw bytes alongside decoded lines.

§Record/replay caveat

On a live run the tee is byte-exact. On a ScriptedRunner double or a cassette replay there is no child: the scripted feeder writes the canned/recorded text back as UTF-8, so the raw tee receives those bytes — byte-exact only insofar as the recorded (already-decoded) text round-trips, the same fidelity limit that makes output_bytes unsupported on a cassette. Rely on byte accuracy only against a real process.

Source

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

Tee the child’s stderr to writer byte for byte, before any decoding or line splitting.

Same contract as stdout_raw_tee — verbatim bytes (non-UTF-8, CRLF, missing final newline, and lines the buffer policy drops all preserved), FIFO order, awaited on the pump (backpressure, bounded memory), flushed at stream end, independent of stderr_tee/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.

This policy governs the capturing verbs (output_string and a streamed finish) and, for its byte ceiling only, the in-flight bound of drain. The discard verbs wait / profile ignore it entirely (they pin a fixed internal in-flight cap); drain is the discard path that honors this byte cap — retaining nothing, but bounding held memory by the configured max_bytes rather than the child’s output size. max_lines never affects the discard paths (they retain nothing).

Source

pub fn capture_policy(self, policy: impl CapturePolicy + 'static) -> Self

Install a CapturePolicy — a typed redaction-at-capture seam that shapes each decoded line of both streams just before it is retained, so the value the policy returns (not the raw line) is what lands in the capture backlog and therefore in output_string / ProcessResult and the streaming verbs (stdout_lines / events).

This is the capture-shaping counterpart to the observing on_stdout_line/on_stderr_line handlers: those see each line but run alongside capture and cannot change what is retained. Use it to scrub a secret a child echoes to its output before it settles in the captured result — completing the crate’s secret-hygiene posture (a cassette stores env names only; Debug redacts env values).

A single whole-command knob (like output_buffer): the policy is handed the OutputStream each line came from, so one implementation can treat stdout and stderr differently. A repeat call replaces the previous policy (builder semantics).

§Scope

The seam shapes only the capture backlog. The per-line handlers, the decoded stdout_tee/stderr_tee, the byte-plane stdout_raw_tee/stderr_raw_tee, and the raw stdout of output_bytes are independent and see the line un-redacted — if you also tee to a log, redact in that sink too. A line past an OutputBufferPolicy byte cap (with_max_bytes) is never assembled, so — like the handlers/tee — the policy never sees it. See CapturePolicy for the full contract (including its fail-closed panic behavior).

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 / 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).

§Interaction with use_pty

A PTY child writes progress as bare \r redraws, so use_pty (with the pty feature) makes the effective default terminator CarriageReturn instead of Newline, so a naive PTY consumer gets framed progress rather than one ever-growing line (see use_pty for the rationale). Calling this method — with either variant, including an explicit Newline — opts out of that auto-default and pins your choice, order-independently of use_pty.

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 (and for how it interacts with the use_pty auto-default).

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 sanitize_vt(self) -> Self

Enable the opt-in VT/ANSI output sanitizer on both streams’ capture backlog: each decoded line is stripped of terminal escape sequences and lone control codes before it is retained, so a line-oriented consumer sees readable text instead of \x1b[31m…-mucked strings.

The motivating case is a PTY agent CLI (use_pty, the pty feature) whose merged output is full of colors, cursor moves, alternate-screen switches, and OSC title/hyperlink escapes: with this on, the line predicates (wait_for_line / first_line), output_string / ProcessResult, and the streaming verbs (stdout_lines / events) all carry the de-escaped text. It drops CSI (ESC [ … final), OSC (ESC ] … BEL/ST), DCS/SOS/PM/APC string escapes, other two-/n-byte ESC escapes, and lone C0 control bytes / DELkeeping the horizontal tab \t.

§Scope (the same boundary as capture_policy)

Sanitization shapes only the capture backlog, exactly like capture_policy. The observing per-line handlers (on_stdout_line/on_stderr_line), the decoded stdout_tee/stderr_tee, the byte-plane stdout_raw_tee/stderr_raw_tee, and the raw output_bytes stream are independent and keep seeing the un-sanitized bytes — if you also tee to a log and want it clean, sanitize in that sink. When combined with capture_policy, sanitization runs first so a secret-scrubbing policy matches on already-cleaned text (a token cannot hide behind a color escape). A line past an OutputBufferPolicy byte cap is judged on its raw length (before this transform) and, if over-cap, is never assembled — so, like the handlers/tee, sanitization never sees it.

Off by default and strictly additive: an existing run that never calls this captures byte-for-byte as before. Set it per stream with stdout_sanitize_vt / stderr_sanitize_vt.

Source

pub fn stdout_sanitize_vt(self) -> Self

Enable the sanitize_vt VT/ANSI sanitizer on stdout only; stderr capture is left verbatim.

Source

pub fn stderr_sanitize_vt(self) -> Self

Enable the sanitize_vt VT/ANSI sanitizer on stderr only; stdout capture is left verbatim.

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 fn configured_inactivity_timeout(&self) -> Option<Duration>

The configured output-inactivity window, if any.

Source

pub fn configured_ok_codes(&self) -> Option<&[i32]>

The exit codes explicitly configured via ok_codes, if any — None when unset, in which case the default {0} applies (see ok_codes_vec for the always-populated effective set). Mirrors configured_timeout: the raw configured state, not a resolved default — lets ScriptedRunner::when predicates and other inspection code (see the “Public accessors” note above) tell “left at the default” apart from “explicitly set to {0}”.

Source

pub fn configured_cpu_affinity(&self) -> Option<&[usize]>

The canonical logical CPU set configured via cpu_affinity, if any. The slice is sorted and deduplicated; Some([]) preserves an explicitly empty (invalid) request so inspection can distinguish it from an unset affinity before launch.

Source

pub fn to_tokio_command(&self) -> Result<Command>

Lower this builder to a raw tokio::process::Command — the escape hatch for a platform knob ProcessKit deliberately doesn’t model.

Prefer the typed verbs. Almost every launch should go through run/output_string/output_bytes/start or a ProcessGroup: those drive the async output pump, capture, timeouts/cancellation, and the graceful-teardown machinery for you. This bridge exists for the rare case where you need to set something on the OS command that the builder has no typed knob for (a niche creation flag, your own pre_exec), without re-deriving the crate’s launch wiring by hand.

The returned command carries everything this builder resolves at the OS level: the (optionally prefer_local-resolved) program and arguments, the working directory, the layered environment (env_clear/inherit_env/ env/env_remove), the platform launch hooks (Unix priority/cpu_affinity/umask/privilege-drop/ setsid pre_exec hooks; Windows creation flags), and stdio wired to match the builder’s stdout/stdin configuration (piped for capture by default). Mutate the returned command, then hand it to ProcessGroup::spawn to keep containment.

What you keep, and what you give up. Spawning the result through ProcessGroup::spawn still enrolls the child in the group’s Job/cgroup/process-group, so containment is preserved (kill-on-drop and the group-level teardown verbs still reach it). You give up the high-level machinery keyed off this builder that lives above the OS command: the async output pump and capture, the ProcessResult/RunningProcess verbs, and the per-run timeout/cancel_on/ timeout_grace/ windows_graceful_ctrl_break wiring — you drive the bare tokio::process::Child (draining its pipes, reaping it) yourself. On Windows, ProcessGroup::spawn re-sets the child’s creation flags to make containment race-free, so a creation flag left on this command is overwritten by that path (see its docs) — reach for the typed create_no_window on a high-level launch path instead.

§Errors

The same preflight failures a normal launch would raise while resolving the program / opening a stdout_file redirect (ErrorReason::Io), plus ErrorReason::Unsupported for a Linux-only I/O-priority request on another platform, affinity on a target other than Linux/Windows, or Windows affinity (which requires the typed suspended-child launch seam and cannot be encoded in a raw command).

Source

pub fn spawn_detached(&self) -> Result<DetachedChild>

Spawn the child deliberately released from this crate’s kill-on-drop containment, handing back a DetachedChild whose lifetime is entirely yours: the crate will never kill, reap, time out, or capture it.

§Warning — this inverts the crate’s headline guarantee

Every other run/start verb keeps the child in a kill-on-drop container, so nothing is orphaned. spawn_detached is the crate’s one deliberate escape hatch for the legitimate handoff cases — daemonizing, a nohup-style long-lived helper meant to outlive the launcher — where the child must survive its owner. Reach for it only when you truly want that; for everything else, start/run/output_* are what you want.

The returned DetachedChild is a separate, non-interchangeable type (not a RunningProcess) carrying nothing but the pid — no kill, no timeout, no capture, no teardown — precisely because it is no longer contained.

§Detach happens at birth
  • Unix — the child is launched into a new session (setsid), so it has no controlling terminal and its own session/process group; it is not tracked by any of this crate’s kill-on-drop groups.
  • Windows — the child is not assigned to this crate’s Job Object, so closing/dropping any handle here cannot kill it. It is not made to break away from an external Job Object the OS already places it in — see below.
§Still bound by a host container (by design)

“Detached” means detached from this crate’s per-run containment — not from a broader host container your process already lives in. If your process runs under an external Windows Job Object or a Linux cgroup (a CI runner, a systemd scope, this crate’s own supervisor), the child may still inherit and be bound by it. spawn_detached deliberately does not attempt a job breakaway or cgroup escape: that would be hostile to whoever set up the host containment (and on Windows would simply fail the spawn where breakaway is disallowed).

§stdio: null, or a file — never a pipe

A detached child has no owner draining its output, so a pipe would deadlock it the moment the buffer fills after you go away (the classic daemon bug). stdout/stderr are therefore null by default; the only alternative is a file redirect (stdout_file / stderr_file and their _append forms). stdin is always null. A pipe or an inherited parent fd is rejected (see below).

§Rejected configuration — a loud, typed refusal

A detached child has no owner to enforce a timeout, no pump to capture output, and no interactive stdin, so a Command carrying any of those knobs is refused with ErrorReason::Unsupported naming it — never silently ignored (the same “fail loud, don’t drop a requested behavior” contract as uid/gid/umask off Unix). The refused knobs are: timeout/timeout_grace, retry/retry_with, cancel_on, kill_on_parent_death (its exact opposite), windows_graceful_ctrl_break, keep_stdin_open/inherit_stdin/ a configured stdin source, any capture wiring (on_stdout_line/on_stderr_line, the tee sinks, a capture_policy), an inherited stdout/stderr connection, and (with the pty feature) use_pty. Program/argument/env/working-directory and the privilege-drop knobs (uid/gid/ groups/umask/priority) are honored — a detached daemon may still drop privileges.

§Not async

Detaching is fire-and-forget: there is nothing to await and no tokio runtime is required, so this is a plain synchronous spawn (like the low-level ProcessGroup::spawn escape hatch), callable from daemonizing code that runs before any runtime exists.

§Errors
  • ErrorReason::Unsupported — an incompatible knob (above), or a POSIX-only privilege primitive requested off Unix.
  • ErrorReason::NotFound — the program could not be located.
  • ErrorReason::Spawn — the program was located but the OS refused to start it (bad working directory, permission denied, a Windows .cmd/.bat that needs cmd.exe, …).
use processkit::Command;

// Launch a long-lived helper that must outlive this process, logging to a
// file (never a pipe — there is no owner left to drain one).
let child = Command::new("my-daemon")
    .arg("--serve")
    .stdout_file("/var/log/my-daemon.log")
    .spawn_detached()?;
println!("detached daemon pid = {}", child.pid());
// Dropping `child` does NOT kill the daemon — it keeps running.
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:

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 ErrorReason::Cancelled (a cancellation is always raised), ErrorReason::OutputTooLarge (a fail-loud buffer overflowed), ErrorReason::Stdin (a non-broken-pipe stdin failure on an otherwise-successful run), and ErrorReason::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 ErrorReason::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 ErrorReason::Timeout, a signal-kill as ErrorReason::Signalled — consistent with ProcessRunnerExt::exit_code and CliClient::exit_code.

§Errors

The launch failures listed on start, plus — when the run produced no code — ErrorReason::Timeout (the deadline elapsed), ErrorReason::Signalled (killed by a signal), or ErrorReason::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 ErrorReason::Exit.

§Errors

The launch failures listed on start, plus the success-checking failures: ErrorReason::Exit (a non-accepted exit code), ErrorReason::Signalled (a signal-kill), ErrorReason::Timeout (the deadline elapsed — raised here, unlike on output_string), ErrorReason::Cancelled, ErrorReason::OutputTooLarge (a fail-loud buffer truncated the presented stdout), and ErrorReason::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 runErrorReason::Exit / ErrorReason::Signalled / ErrorReason::Timeout / ErrorReason::Cancelled / ErrorReason::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 ErrorReason::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 ErrorReason::Exit / ErrorReason::Signalled / ErrorReason::Timeout / ErrorReason::Cancelled / ErrorReason::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 ErrorReason::Exit, a timeout as ErrorReason::Timeout, a signal-kill as ErrorReason::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 ErrorReason::Exit, and — atop the launch failures on start — a run that produced no code errors as ErrorReason::Timeout, ErrorReason::Signalled, or ErrorReason::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 ErrorReason::Exit / ErrorReason::Signalled / ErrorReason::Timeout / ErrorReason::Cancelled / ErrorReason::Stdin), plus ErrorReason::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 ErrorReason::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 ErrorReason::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 ErrorReason::Timeout when a timeout is set and its deadline elapses mid-stream (which tears the process down), ErrorReason::Cancelled, or ErrorReason::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 which list the spawn searches.

A resolved hit is exactly what a run spawns, at that same path — on Windows including a bare name found only through a non-.exe PATHEXT extension (.cmd/.bat/.com/…): the launch substitutes the resolved absolute path (the OS’s own bare-name search appends only .exe), so such a hit spawns instead of raising ErrorReason::Spawn. The one residual asymmetry is a preflight miss on Windows: the OS can still locate a bare name through the application directory, the current directory, or the system directories — routes this PATH-based model doesn’t cover — so a miss there is not proof a run couldn’t launch it. Unix (execvp, PATH-only) has no such gap.

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

§Errors

ErrorReason::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