Module process/command

process/command
Stability: unstable — there are two ways into a child's pipes and one of them has to go. The typed handles (`take_stdin`/`take_stdout`/`take_stderr`) landed 2026-09-10 and are the ones that put a child's streams inside `std/io`'s surface; the three bespoke methods they replace (`write_stdin`, `read_stdout_to_end`, `read_stderr_to_end`) are still exported beside them, so the same fd is reachable through two vocabularies with different ownership rules. Removing the older pair is breaking, so it has not happened yet, and freezing now would freeze both. The manual path is also missing the piece that makes it safe. Draining stdout to EOF and then stderr deadlocks on any child that fills the 64 KiB stderr pipe first, which is why `output()` spawns a task per pipe (`issues/fixed/command-output-drains-stdout-then-stderr-sequentially.md`). A caller who took the handles has to rebuild that concurrency by hand — Rust offers `Child::wait_with_output` for exactly this, and Yo's equivalent is only reachable if you did NOT take the handles. That helper is additive; the duplicate stream API is not. Freezing follows the removal of the bespoke trio, not a release count. Settled and not blocking: `kill` throws through `Exception` rather than returning an errno (D1, changed 2026-09-06), and `ExitStatus`/`Stdio`/the `Command` builder have been stable across the campaign. `kill` does take a raw signal NUMBER where Rust's takes none — deliberate, since the whole POSIX signal set is reachable and Windows has no equivalent to narrow it to. — stable modules only change additively; this one may still change.

High-level child-process spawning with builder-style configuration.

Wraps std/sys/process with a fluent API for constructing argv, capturing stdout/stderr through pipes, and waiting for the child to exit.

Example

{ Command } :: import "std/process/command";
{ Exception, AnyError } :: import "std/error";

main :: (fn(io : Io, exn : Exception) -> unit)({
  cmd := Command.new(`echo`);
  cmd.arg(`hello`);
  cmd.arg(`world`);
  out := io.await(cmd.output(io), { io, exn });
  assert(out.status.success(), "echo should succeed");
});

Streaming a child's pipes

Command.spawn returns a Child; take_stdin/take_stdout/take_stderr hand you the parent ends as Writer/Reader handles, so all of std/io's surface — write_all, read_to_string, read_exact, BufReader.lines — applies to a child's streams:

{ Command, Stdio } :: import("std/process/command");
{ BufReader } :: import("std/io/buffered");

cmd := Command.new(`sort`);
cmd.stdin(Stdio.Piped);
cmd.stdout(Stdio.Piped);
child := io.await(cmd.spawn(io), ie);
match(child.take_stdin(), .Some(w) => {
  io.await(w.write_string(`b\na\n`, io), ie);
  io.await(w.close(io), ie);            // EOF — or just drop `w`
}, .None => ());
text := match(
  child.take_stdout(),
  .Some(r) => io.await(r.read_to_string(io), ie),
  .None => String.new()
);
io.await(child.wait(io), ie);

Taking a stream MOVES its fd out of the Child, so wait() will not close it and the handle's own close (or its drop) does. Read a piped stdout before wait() when the child may produce more than a pipe buffer holds, or the child blocks writing while you block waiting.

Stability

unstable — there are two ways into a child's pipes and one of them has to go. The typed handles (take_stdin/take_stdout/take_stderr) landed 2026-09-10 and are the ones that put a child's streams inside std/io's surface; the three bespoke methods they replace (write_stdin, read_stdout_to_end, read_stderr_to_end) are still exported beside them, so the same fd is reachable through two vocabularies with different ownership rules. Removing the older pair is breaking, so it has not happened yet, and freezing now would freeze both.

The manual path is also missing the piece that makes it safe. Draining stdout to EOF and then stderr deadlocks on any child that fills the 64 KiB stderr pipe first, which is why output() spawns a task per pipe (issues/fixed/command-output-drains-stdout-then-stderr-sequentially.md). A caller who took the handles has to rebuild that concurrency by hand — Rust offers Child::wait_with_output for exactly this, and Yo's equivalent is only reachable if you did NOT take the handles. That helper is additive; the duplicate stream API is not. Freezing follows the removal of the bespoke trio, not a release count.

Settled and not blocking: kill throws through Exception rather than returning an errno (D1, changed 2026-09-06), and ExitStatus/Stdio/the Command builder have been stable across the campaign. kill does take a raw signal NUMBER where Rust's takes none — deliberate, since the whole POSIX signal set is reachable and Windows has no equivalent to narrow it to.

Types

Stdio enum
Stdio

What to do with one of the child's standard streams (plans/archive/STD_API_AUDIT.md §7 P0 item 5).

Variants

VariantFieldsDescription
Inherit

Share the parent's stream (the default).

Piped

Connect a pipe; the parent end is on the returned Child.

Null

Redirect to /dev/null.

Command object
Command

Builder for a child-process invocation.

Construct with Command.new(program), then chain builder methods (arg, args, env, env_clear, stdin, stdout, stderr — each returns Self, and Command is a ref type, so chaining and statement-style calls both work) before status() / output() / spawn().

Fields

NameTypeDescription
_programString
_argsArrayList(String)
_env_overridesArrayList(String)
_env_clearbool
_stdinStdio
_stdoutStdio
_stderrStdio
_cwdOption(String)
impl(Command, ...)
new : (Command) fn(program : String) -> Command

Create a new Command invoking program. The program name is also used as argv[0] unless a different first arg is pushed manually before invocation.

Parameters

NameTypeNotes
programString

Returns: Command

arg : (Command) fn(self : Command, a : String) -> Command

Append a single argument to the argv list.

Parameters

NameTypeNotes
selfCommand
aString

Returns: Command

args : (Command) fn(self : Command, more : ArrayList(String)) -> Command

Append multiple arguments to the argv list.

Parameters

NameTypeNotes
selfCommand
moreArrayList(String)

Returns: Command

env : (Command) fn(self : Command, key : String, value : String) -> Command

Set an environment variable for the child (overrides an inherited one).

Parameters

NameTypeNotes
selfCommand
keyString
valueString

Returns: Command

current_dir : (Command) fn(self : Command, dir : String) -> Command

Run the child with dir as its working directory. POSIX uses posix_spawn_file_actions_addchdir_np (spawn reports ENOSYS on a libc without it — macOS 10.15+/glibc 2.29+/musl 1.2.5+ all have it); Windows passes lpCurrentDirectory.

Parameters

NameTypeNotes
selfCommand
dirString

Returns: Command

env_clear : (Command) fn(self : Command) -> Command

Stop the child inheriting this process's environment, so it starts from an EMPTY one plus whatever env() supplied — Rust's Command::env_clear.

It does not clear the env() entries already recorded, and Rust's does. This sets a flag that is consulted at spawn time and only suppresses the walk over environ; the overrides are collected separately and always survive. So cmd.env(A, 1).env_clear() runs the child with A=1 here, and with nothing at all in Rust — call order makes no difference in either direction.

With neither this nor any env() call, no envp is constructed at all and the child inherits by the platform's own mechanism.

Parameters

NameTypeNotes
selfCommand

Returns: Command

stdin : (Command) fn(self : Command, mode : Stdio) -> Command

Configure the child's stdin.

Parameters

NameTypeNotes
selfCommand
modeStdio

Returns: Command

stdout : (Command) fn(self : Command, mode : Stdio) -> Command

Configure the child's stdout.

Parameters

NameTypeNotes
selfCommand
modeStdio

Returns: Command

stderr : (Command) fn(self : Command, mode : Stdio) -> Command

Configure the child's stderr.

Parameters

NameTypeNotes
selfCommand
modeStdio

Returns: Command

impl(Command, ...)
status : (Command) fn(self : Command, io : Io) -> Impl : (Future[Future](ExitStatus) IoExn : IoExn)

Spawn the child with stdio inherited from the parent, wait for it to exit, and return its ExitStatus.

Parameters

NameTypeNotes
selfCommand
ioIo

Returns: Impl : (Future[Future](ExitStatus) IoExn : IoExn)

output : (Command) fn(self : Command, io : Io) -> Impl : (Future[Future](Output) IoExn : IoExn)

Spawn the child with stdout and stderr captured through pipes. Waits for the child to exit and returns the exit status plus the captured bytes.

Parameters

NameTypeNotes
selfCommand
ioIo

Returns: Impl : (Future[Future](Output) IoExn : IoExn)

impl(Command, ...)
spawn : (Command) fn(self : Command, io : Io) -> Impl : (Future[Future](Child) IoExn : IoExn)

Spawn the child WITHOUT waiting. Streams configured Stdio.Piped get pipes whose parent ends live on the returned Child; Inherit shares the parent's stream; Null redirects to /dev/null. Call wait() to reap.

Parameters

NameTypeNotes
selfCommand
ioIo

Returns: Impl : (Future[Future](Child) IoExn : IoExn)

ExitStatus struct
ExitStatus

The result of a finished child process.

Holds the raw waitpid status code along with helpers to extract the exit code and termination signal.

Fields

NameTypeDescription
rawi32

Encoded waitpid status. Use code() / signal() helpers instead of reading this directly when possible.

impl(ExitStatus, ...)
code : (ExitStatus) fn(self : ExitStatus) -> Option(i32)

The process exit code (0..255 on Unix), or .None when the process was TERMINATED BY A SIGNAL — a signal death is not an exit code of 0 (plans/archive/STD_API_AUDIT.md §4 process row).

Parameters

NameTypeNotes
selfExitStatus

Returns: Option(i32)

signal : (ExitStatus) fn(self : ExitStatus) -> i32

Returns the signal number that terminated the process, or 0 if the process exited normally.

Parameters

NameTypeNotes
selfExitStatus

Returns: i32

success : (ExitStatus) fn(self : ExitStatus) -> bool

Returns true if the child exited with status code 0.

Parameters

NameTypeNotes
selfExitStatus

Returns: bool

Output object
Output

Captured output of a child process.

Returned by Command.output. Holds the exit status plus the bytes captured from the child's stdout and stderr.

Fields

NameTypeDescription
statusExitStatus

Spawn the child with stdio inherited from the parent, wait for it to exit, and return its ExitStatus.

stdoutArrayList(u8)

Configure the child's stdout.

stderrArrayList(u8)

Configure the child's stderr.

Child object
Child

A running child process, returned by Command.spawn. Pipe fds exist for the streams configured Stdio.Piped; wait() reaps the child.

Fields

NameTypeDescription
_pidi32
_stdin_fdOption(i32)
_stdout_fdOption(i32)
_stderr_fdOption(i32)
_reapedbool
impl(Child, ...)
pid : (Child) fn(self : Child) -> i32

The child's process id.

Parameters

NameTypeNotes
selfChild

Returns: i32

take_stdin : (Child) fn(self : Child) -> Option(ChildStdin)

Take the parent's write end of the child's stdin pipe as a Writer. .None if stdin was not Stdio.Piped, or if it has already been taken or closed.

Taking MOVES the fd out of the Child: wait() no longer closes it, the returned handle owns it, and write_stdin/close_stdin on this Child throw BadFileDescriptor from then on. Dropping the handle closes the pipe, which is the EOF the child is usually waiting for.

Parameters

NameTypeNotes
selfChild

Returns: Option(ChildStdin)

take_stdout : (Child) fn(self : Child) -> Option(ChildStdout)

Take the parent's read end of the child's stdout pipe as a Reader, so read_to_string, read_exact and BufReader apply. .None if stdout was not Stdio.Piped, or if it has already been taken.

Taking MOVES the fd out of the Child, exactly as take_stdin does.

Parameters

NameTypeNotes
selfChild

Returns: Option(ChildStdout)

take_stderr : (Child) fn(self : Child) -> Option(ChildStderr)

Take the parent's read end of the child's stderr pipe as a Reader. .None if stderr was not Stdio.Piped, or if it has already been taken.

Parameters

NameTypeNotes
selfChild

Returns: Option(ChildStderr)

write_stdin : (Child) fn(self : Child, data : ArrayList(u8), io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)

Write bytes into the child's piped stdin. Requires stdin(Stdio.Piped). Throws BadFileDescriptor once take_stdin() has moved the fd out.

Parameters

NameTypeNotes
selfChild
dataArrayList(u8)
ioIo

Returns: Impl : (Future[Future](usize) IoExn : IoExn)

close_stdin : (Child) fn(self : Child, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)

Close the child's stdin pipe — the EOF the child is usually waiting for.

Parameters

NameTypeNotes
selfChild
ioIo

Returns: Impl : (Future[Future](unit) IoExn : IoExn)

read_stdout_to_end : (Child) fn(self : Child, io : Io) -> Impl : (Future[Future](ArrayList(u8)) IoExn : IoExn)

Read the child's piped stdout to EOF. Requires stdout(Stdio.Piped).

Parameters

NameTypeNotes
selfChild
ioIo

Returns: Impl : (Future[Future](ArrayList(u8)) IoExn : IoExn)

read_stderr_to_end : (Child) fn(self : Child, io : Io) -> Impl : (Future[Future](ArrayList(u8)) IoExn : IoExn)

Read the child's piped stderr to EOF. Requires stderr(Stdio.Piped).

Parameters

NameTypeNotes
selfChild
ioIo

Returns: Impl : (Future[Future](ArrayList(u8)) IoExn : IoExn)

kill : (Child) fn(self : Child, signum : i32, exn : Exception) -> unit

Send a signal to the child (e.g. 15 = SIGTERM, 9 = SIGKILL). A failure — ESRCH once the child has been reaped, EPERM — is thrown as an IoError through exn, Rust's Child::kill -> io::Result<()>. Until 2026-09-06 this handed back the raw -errno as an i32 (issues/fixed/child-kill-returned-a-raw-errno.md).

Parameters

NameTypeNotes
selfChild
signumi32
exnException

Returns: unit

wait : (Child) fn(self : Child, io : Io) -> Impl : (Future[Future](ExitStatus) IoExn : IoExn)

Wait for the child to exit and return its status. Closes any pipe fds still open on this handle.

Parameters

NameTypeNotes
selfChild
ioIo

Returns: Impl : (Future[Future](ExitStatus) IoExn : IoExn)

ChildStdin object
ChildStdin

The parent's write end of a child's stdin pipe — a Writer, so write_all, write_string and the rest of std/io's writer surface work on it. Take it off a Child with take_stdin(); closing it (explicitly, or by dropping it) is the EOF the child is usually waiting for.

Fields

NameTypeDescription
_fdi32
_closedbool

Trait Implementations

Dispose IoTraits
impl(ChildStdin, ...)
fd : (ChildStdin) fn(self : ChildStdin) -> i32

The underlying file descriptor.

Parameters

NameTypeNotes
selfChildStdin

Returns: i32

is_closed : (ChildStdin) fn(self : ChildStdin) -> bool

Whether the handle has been closed.

Parameters

NameTypeNotes
selfChildStdin

Returns: bool

write : (ChildStdin) fn(self : ChildStdin, buf : *(u8), size : usize, io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)

Write up to size bytes. Returns the number written.

Parameters

NameTypeNotes
selfChildStdin
buf*(u8)
sizeusize
ioIo

Returns: Impl : (Future[Future](usize) IoExn : IoExn)

close : (ChildStdin) fn(self : ChildStdin, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)

Close the read end. Safe to call multiple times. Sets the flag after the close, for the reason ChildStdin.close records.

Parameters

NameTypeNotes
selfChildStdin
ioIo

Returns: Impl : (Future[Future](unit) IoExn : IoExn)

impl(ChildStdin, IoTraits)
impl(ChildStdin, ...)
write_string : (ChildStdin) fn(self : ChildStdin, data : String, io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)

Write a String, in full. File.write_string spells the same convenience.

Parameters

NameTypeNotes
selfChildStdin
dataString
ioIo

Returns: Impl : (Future[Future](usize) IoExn : IoExn)

impl(ChildStdin, Dispose(...))
dispose : (ChildStdin) fn(self : ChildStdin) -> unit

Release the resources self owns — a file descriptor, a socket, a lock, a buffer the allocator handed out. Called automatically when the last reference to the value goes away, so an implementor never calls it directly and must tolerate being the only one who ever does.

It must be safe to run exactly once: the runtime calls it at refcount zero, and a type that also exposes an explicit close/release is responsible for making the second call a no-op.

Parameters

NameTypeNotes
selfChildStdin

Returns: unit

Methods
flush : (ChildStdin) fn(self : ChildStdin, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)

A no-op that always succeeds: a pipe write goes straight to the kernel, so there is nothing held here to push. Everything write reported as written is already visible to the child, and flushing is NOT what delivers EOF — close (or dropping the handle) is.

Parameters

NameTypeNotes
selfChildStdin
ioIo

Returns: Impl : (Future[Future](unit) IoExn : IoExn)

write_all : (ChildStdin) fn(self : ChildStdin, buf : *(u8), size : usize, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)

Parameters

NameTypeNotes
selfChildStdin
buf*(u8)
sizeusize
ioIo

Returns: Impl : (Future[Future](unit) IoExn : IoExn)

ChildStdout object
ChildStdout

The parent's read end of a child's stdout pipe — a Reader, so read_to_end, read_to_string, read_exact and BufReader work on it. Take it off a Child with take_stdout().

Fields

NameTypeDescription
_fdi32
_closedbool

Trait Implementations

Dispose IoTraits
impl(ChildStdout, ...)
fd : (ChildStdout) fn(self : ChildStdout) -> i32

The underlying file descriptor.

Parameters

NameTypeNotes
selfChildStdout

Returns: i32

is_closed : (ChildStdout) fn(self : ChildStdout) -> bool

Whether the handle has been closed.

Parameters

NameTypeNotes
selfChildStdout

Returns: bool

read : (ChildStdout) fn(self : ChildStdout, buf : *(u8), size : usize, io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)

Read up to size bytes. Returns the number read; 0 means EOF.

Parameters

NameTypeNotes
selfChildStdout
buf*(u8)
sizeusize
ioIo

Returns: Impl : (Future[Future](usize) IoExn : IoExn)

close : (ChildStdout) fn(self : ChildStdout, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)

Close the read end. Safe to call multiple times. Sets the flag after the close, for the reason ChildStdin.close records.

Parameters

NameTypeNotes
selfChildStdout
ioIo

Returns: Impl : (Future[Future](unit) IoExn : IoExn)

impl(ChildStdout, IoTraits)
impl(ChildStdout, Dispose(...))
dispose : (ChildStdout) fn(self : ChildStdout) -> unit

Release the resources self owns — a file descriptor, a socket, a lock, a buffer the allocator handed out. Called automatically when the last reference to the value goes away, so an implementor never calls it directly and must tolerate being the only one who ever does.

It must be safe to run exactly once: the runtime calls it at refcount zero, and a type that also exposes an explicit close/release is responsible for making the second call a no-op.

Parameters

NameTypeNotes
selfChildStdout

Returns: unit

Methods
read_to_end : (ChildStdout) fn(self : ChildStdout, io : Io) -> Impl : (Future[Future](ArrayList(u8)) IoExn : IoExn)

Parameters

NameTypeNotes
selfChildStdout
ioIo

Returns: Impl : (Future[Future](ArrayList(u8)) IoExn : IoExn)

read_to_string : (ChildStdout) fn(self : ChildStdout, io : Io) -> Impl : (Future[Future](String) IoExn : IoExn)

Parameters

NameTypeNotes
selfChildStdout
ioIo

Returns: Impl : (Future[Future](String) IoExn : IoExn)

ChildStderr object
ChildStderr

The parent's read end of a child's stderr pipe. Structurally identical to ChildStdout and distinct for the same reason Rust keeps them apart: the type says which stream you are holding.

Fields

NameTypeDescription
_fdi32
_closedbool

Trait Implementations

Dispose IoTraits
impl(ChildStderr, ...)
fd : (ChildStderr) fn(self : ChildStderr) -> i32

The underlying file descriptor.

Parameters

NameTypeNotes
selfChildStderr

Returns: i32

is_closed : (ChildStderr) fn(self : ChildStderr) -> bool

Whether the handle has been closed.

Parameters

NameTypeNotes
selfChildStderr

Returns: bool

read : (ChildStderr) fn(self : ChildStderr, buf : *(u8), size : usize, io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)

Read up to size bytes. Returns the number read; 0 means EOF.

Parameters

NameTypeNotes
selfChildStderr
buf*(u8)
sizeusize
ioIo

Returns: Impl : (Future[Future](usize) IoExn : IoExn)

close : (ChildStderr) fn(self : ChildStderr, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)

Close the read end. Safe to call multiple times. Sets the flag after the close, for the reason ChildStdin.close records.

Parameters

NameTypeNotes
selfChildStderr
ioIo

Returns: Impl : (Future[Future](unit) IoExn : IoExn)

impl(ChildStderr, IoTraits)
impl(ChildStderr, Dispose(...))
dispose : (ChildStderr) fn(self : ChildStderr) -> unit

Release the resources self owns — a file descriptor, a socket, a lock, a buffer the allocator handed out. Called automatically when the last reference to the value goes away, so an implementor never calls it directly and must tolerate being the only one who ever does.

It must be safe to run exactly once: the runtime calls it at refcount zero, and a type that also exposes an explicit close/release is responsible for making the second call a no-op.

Parameters

NameTypeNotes
selfChildStderr

Returns: unit

Methods
read_to_end : (ChildStderr) fn(self : ChildStderr, io : Io) -> Impl : (Future[Future](ArrayList(u8)) IoExn : IoExn)

Parameters

NameTypeNotes
selfChildStderr
ioIo

Returns: Impl : (Future[Future](ArrayList(u8)) IoExn : IoExn)

read_to_string : (ChildStderr) fn(self : ChildStderr, io : Io) -> Impl : (Future[Future](String) IoExn : IoExn)

Parameters

NameTypeNotes
selfChildStderr
ioIo

Returns: Impl : (Future[Future](String) IoExn : IoExn)