Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.process

import std.process starts child processes from an argument vector and owns them until they are reaped. Arguments, variables, and directories are operating-system byte strings; pipes carry std.bytes data.

No shell takes part unless the program is one. An argument reaches the child exactly as given: spaces, quotes, $, ;, |, *, and newlines are ordinary bytes. shell is the one, named way to run shell syntax. std.shlex can split a shell-style argument string before passing each resulting word to arg; it does not execute or expand shell syntax.

Request encoding, pipe draining, status decoding, and the reaping rules are checked Plenty. Five native primitives spawn, hand over a pipe end, wait, signal, and poll. The module is available on the supported x86-64 Linux GNU target; elsewhere spawn reports IoError.Unsupported.

import std.process
from std.bytes import BytesError
from std.io import StreamError
from std.process import Stdio

def main() -> Result[(), AllocError | IoError | StreamError | BytesError]:
    mut echo = std.process.command("echo")?
    echo.arg("hello from a child")?
    echo.stdout(Stdio.Piped)
    mut child = echo.spawn()?
    output = child.output(4096)?
    print(output.status.success(), output.stdout.to_text()?.strip()?)?
    Ok(())
True hello from a child

Building a command

FunctionResultContract
command(program: &str)Result[Command, IoError]A command with no arguments
command_bytes(program: &list[u8])Result[Command, IoError]The same for a name that is not UTF-8
shell(script: &str)Result[Command, IoError]/bin/sh -c script

A program name containing / is a path. Any other name is looked up in the directories of the child’s PATH. The name is also the child’s argv[0].

shell passes script to /bin/sh as one argument. The script is shell syntax, so text placed in it is interpreted: never build one from untrusted input. Pass such data as an argument or a variable instead.

Command methods take self: &mut Command:

MethodResultEffect
arg(value: &str)Result[(), IoError]Append one argument
arg_bytes(value: &list[u8])Result[(), IoError]Append one argument of any bytes
arg_path(value: &Path)Result[(), IoError]Append a path losslessly
env(name: &str, value: &str)Result[(), IoError]Set a variable for the child
env_bytes(name: &list[u8], value: &list[u8])Result[(), IoError]The same for any bytes
env_remove(name: &str), env_remove_bytes(name: &list[u8])Result[(), IoError]The child does not inherit it
env_clear()()The child inherits nothing; earlier edits are dropped
current_dir(path: &Path)Result[(), IoError]The child’s working directory
stdin(mode: Stdio), stdout(mode: Stdio), stderr(mode: Stdio)()Choose a stream
  • A child inherits the parent’s environment and working directory unless the command says otherwise. Edits apply in order, so the last one for a name wins. Nothing changes in the parent.
  • A piece containing a NUL byte, an empty program or directory, and a variable name that is empty or contains = are IoError.InvalidInput. A failed method leaves the command exactly as it was.
  • An empty argument is valid and is passed as an empty argument.
  • Allocation failure is IoError.Data(DataError.Allocation(...)). Unrepresentable argument, environment and combined request sizes preserve AllocError.CapacityOverflow in that cause, before mutation or starting a child.
  • Appending a terminated byte string reserves only its new bytes and NUL, not bytes already held. Existing capacity can satisfy the operation without an allocation; failed growth leaves the original command unchanged.

Stdio selects each standard stream:

ModeThe child’s stream
Stdio.InheritThe parent’s own stream. The default
Stdio.Null/dev/null
Stdio.PipedA new pipe whose other end the Child owns

Spawning

command.spawn() borrows the command and returns Result[Child, IoError]. A command is reusable: each call starts another child.

A program that cannot be started is an error value, not a child that exits with a conventional code: a missing program or directory is IoError.System(2), a file without execute permission IoError.System(13). A failed spawn leaves no process and no open pipe.

Pipe ends are close-on-exec, so one child never inherits another child’s pipes. Other descriptors the program opened are close-on-exec as well.

Close-on-exec still leaves a short window: a child forked by one thread holds a copy of every descriptor open at that moment until its exec closes them. If another thread had a program open for writing then, running that program just after closing it fails with “text file busy” (IoError.System(26)). spawn retries that error after waits of 1, 2, 4, 8, 16, 32 and 64 ms, then reports it, so a busy program delays spawn by at most 127 ms. Spawns in one program take turns, and a retrying spawn keeps its turn while it waits, so no new child can pick up a descriptor meanwhile; other threads’ child operations other than a blocking wait wait with it. spawn takes no deadline or cancellation token. Waiting allocates nothing; each attempt repeats the brief host allocation described under Limits. A program another process keeps open for writing still fails after the last wait.

Child

A Child is the single owner of one process. It is not copyable.

MethodResultContract
id()i64The process id
take_stdin(), take_stdout(), take_stderr()Option[File]Move a pipe end out, once
close_stdin()Result[(), IoError]End the child’s input
wait()Result[ExitStatus, IoError]Close stdin, block until the child ends, reap it
try_wait()Result[Option[ExitStatus], IoError]Reap if ended; Nothing while running
wait_timeout(budget: &Duration, cancel: &CancellationToken)Result[Option[ExitStatus], WaitError]Nothing when the budget ran out
signal(number: i32)Result[(), IoError]Send signal 1 to 64
terminate()Result[(), IoError]SIGTERM: a request the child may ignore
kill()Result[(), IoError]SIGKILL: cannot be caught
communicate(input: &[u8], limit: NonNegative[i64])Result[Output, StreamError]Feed stdin, collect both outputs, wait
output(limit: NonNegative[i64])Result[Output, StreamError]communicate with no input

Methods that reap or move a pipe take self: &mut Child; id and the signal methods borrow.

Pipes

A taken pipe end is an ordinary File: stdin is write-only, stdout and stderr are read-only, and none can seek. Use Bytes.read_file and Bytes.write_file directly, or wrap the end with std.io.from_file for the retry helpers and buffering.

  • A read returns the bytes that have arrived, possibly fewer than asked, and 0 at end of input, which is when the child and every process that inherited the stream have closed it.
  • A write to a child that no longer reads is IoError.System(32) (EPIPE). The program is not sent SIGPIPE for it.
  • The child sees end of input only when stdin is closed. wait and communicate close a stdin the Child still holds; a taken end is the caller’s to close.

Reading one pipe to the end while the child blocks writing another, or writing a large input while the child blocks writing output, is a deadlock: a pipe holds 64 KiB. Use communicate, which serves all three in one loop.

Waiting and status

ExitStatus describes how the child ended:

MethodResultContract
success()boolExit code 0
code()Option[i32]The exit code, 0 to 255; Nothing if a signal ended it
signal()Option[i32]The terminating signal; Nothing if it exited
  • The first successful wait, try_wait, or wait_timeout that sees the child ended reaps it. The status is kept, and later calls return it without asking the operating system.
  • wait blocks without a limit. It does not drain pipes: a child blocked writing to a full pipe that nobody reads never ends.
  • wait_before(deadline: &Deadline, cancel: &CancellationToken) polls try_wait with pauses of at most 10 ms. wait_timeout constructs one total deadline from a duration and delegates to it. Both leave stdin open and the child running when cancelled or timed out. WaitError.Cancelled only ends this wait; it never sends a signal. WaitError.Io preserves operating-system errors; an overflowing budget is WaitError.Io(IoError.InvalidInput).
  • Signals reach only a child that has not been reaped. Afterwards the methods return Ok(()) and send nothing, so a number can never reach an unrelated process that received the same id. A number outside 1 to 64 is IoError.InvalidInput.
  • A signalled child still has to be waited for.

Collecting output

communicate writes input to a piped stdin, appends piped stdout and stderr to two new buffers as data arrives, closes each stream at its end, then waits. Output has public fields status: ExitStatus, stdout: Bytes, and stderr: Bytes.

  • Streams that are not piped, or whose end was taken, are skipped and their buffers stay empty.
  • limit bounds each output separately. A stream that produces more is StreamError.OutOfRange; at most limit + 1 of its bytes were buffered. The child is then still running and still owned: kill or wait for it.
  • A child that stops reading ends the feed without an error. Its status says what happened.
  • Interrupted calls are retried. Other failures are the StreamError of the failing transfer.
  • Memory is the two outputs, grown in 8 KiB steps. Input is written in 4 KiB pieces and is not copied. A next-buffer extent that cannot fit i64 returns StreamError.Allocation(AllocError.CapacityOverflow) before growing or reading that chunk, including when the extra limit-probe byte is unrepresentable.

Readiness flags and process identifiers use bit masks. Argument counts, status subtraction and pipe cursors use explicit arithmetic opt-outs justified by successful storage growth, validated statuses and concrete file progress. Caller arithmetic modes do not change these boundaries.

Ownership and reaping

Plenty has no user destructors, so dropping a Child closes the pipe ends it still holds and does nothing else: the process is neither stopped nor reaped. Its runtime record, and its zombie once it ends, last until the program exits. A build with runtime allocation checks reports that record as a leak. Every child should end in a wait, try_wait, or wait_timeout that returns a status.

A child that outlives the program is adopted by the system as usual.

Limits

  • Linux x86-64 GNU only. Starting a child uses the host allocator briefly for its argument vector; that one allocation is outside fallible accounting.
  • No redirection of a stream to a file or to another child, and no merging of stderr into stdout. shell can do both.
  • Timed child waits poll; scheduling and lock contention can delay return beyond the 10 ms polling interval.
  • No process groups, sessions, user or group changes, resource limits, or detached children.
  • communicate holds whole outputs in memory. For streaming, take the pipes and give each its own thread.

Acknowledgements