std/process
Command execution and child process management.
from "std/process" import { Process, run, capture }run and capture go through the shell. Process.spawn starts a program by path with no arguments. Command is the general form: arguments passed verbatim (no shell), working directory, environment, and where each standard stream goes.
Command
from "std/process" import { Command, Stdio }
let child = Command.new("git")
.arg("status")
.dir(repoPath)
.env("GIT_PAGER", "cat")
.stderr(Stdio.Pipe)
.spawn()!Pointing a direction-specific Stdio mode at the wrong stream is a spawn error naming the stream, and no child is created: a redirection that cannot be honoured never lands on your terminal instead.
Command.env vs Env.set
Command.env changes one child's environment. Env.set (in std/env) changes this process's environment, which every later child inherits. Prefer Command.env — it has no thread-safety caveat and no effect on anything but the child you are starting.
Example
from "std/process" import { Command, Process, Stdio, run, capture }
fn main(): i32 {
// Run and get exit code
let code = run("echo hello")!
// Capture output
let output = capture("uname -s")!
print(output)
// Spawn and wait
let proc = Process.spawn("/usr/bin/true")! // a program path: no arguments, no shell
let exitCode = proc.wait()!
// A child with its own cwd, environment and log file
var child = Command.new("make")
.arg("-j4")
.dir("build")
.env("CC", "clang")
.stdout(Stdio.Write("build.log"))
.stderr(Stdio.Merge)
.spawn()!
let status = child.wait()!
child.close()
return 0
}API reference
std/process has one implementation per target (default, windows); the resolver picks the one for the target you build for.
Child
pub struct ChildBidirectional pipe to a spawned process: parent writes the child's stdin and reads its stdout/stderr. Built for stdio-framed protocols (DAP, LSP) where the parent drives a long-lived adapter over its standard streams.
Reads are blocking. When mergeStderr is false the caller MUST drain both stdout and stderr or the child can deadlock filling an unread pipe; DAP puts protocol on stdout and logs on stderr, so mergeStderr=true is the safe default.
Child.close
fn Child.close(self: &mut Child): voidClose any still-open parent-side fds. Call after wait().
Child.closeStdin
fn Child.closeStdin(self: &mut Child): voidClose the child's stdin, sending EOF. Idempotent.
Child.pid
fn Child.pid(self: &Child): i32The OS process id. These accessors are read-only: the fields are private so a Child cannot be built around a pid or fds this program does not own, and an fd read here stays owned by the Child (takeStdout/takeStderr hand one over). The fd accessors are @unsafe: the number outlives nothing and names whatever reused it once the Child closes or hands the pipe over.
Child.readStderr
fn Child.readStderr(self: &Child, buf: *u8, len: i64): i64Low-level single read into a raw buffer. Prefer stderr() for streaming.
Child.readStdout
fn Child.readStdout(self: &Child, buf: *u8, len: i64): i64Low-level single read into a raw buffer. Prefer stdout() for streaming. Only valid when stdout is a pipe (the default) rather than redirected elsewhere.
Child.signal
fn Child.signal(self: &Child, sig: i32): Result<i32> //
fn Child.signal(self: &Child, _sig: i32): Result<i32> // windowsChild.spawn
fn Child.spawn(program: &string, args: &Vec<string>, mergeStderr: bool): Result<Child>Spawn with both stdio pipes and nothing else configured. Shorthand for Command.new(program).args(args).stderr(...) — use Command when the child needs a working directory, an environment, or a stream pointed anywhere but a pipe.
Child.stderr
fn Child.stderr(self: &Child): Channel<string>Stream the child's stderr the same way. Only valid when stderr is a separate pipe (not merged into stdout).
Child.stderrFd
fn Child.stderrFd(self: &Child): i32The parent end of the child's stderr pipe, or -1 when merged, not a pipe, or taken.
Child.stdinFd
fn Child.stdinFd(self: &Child): i32The parent end of the child's stdin pipe, or -1 when stdin is not a pipe or closed.
Child.stdout
fn Child.stdout(self: &Child): Channel<string>Stream the child's stdout as an iterable channel, pumped on a background green task — the uniform async-read API shared with pty/socket/pipe. for chunk in child.stdout(); closes at EOF (child exits / closes stdout). Only valid when stdout is a pipe (the default).
Child.stdoutFd
fn Child.stdoutFd(self: &Child): i32The parent end of the child's stdout pipe, or -1 when not a pipe or taken.
Child.takeStderr
fn Child.takeStderr(self: &mut Child): OwnedFdAs takeStdout, for a separate stderr pipe (a -1 handle when stderr was merged).
Child.takeStdin
fn Child.takeStdin(self: &mut Child): OwnedFdHand the stdin pipe to the caller, as takeStdout does stdout: the Child stops tracking it (closeStdin and close skip it), and the child sees EOF on stdin once the returned handle and every dup of it have dropped.
Child.takeStdout
fn Child.takeStdout(self: &mut Child): OwnedFdHand the stdout pipe to the caller: the Child stops tracking it, and the returned handle closes it when dropped. For a pump task that must be the fd's last user.
Child.wait
fn Child.wait(self: &Child): Result<i32>Block until the child exits and return its status. Inside a green task only this task parks (see waitpidGreen); the rest of the scheduler keeps running.
Child.writeStdin
fn Child.writeStdin(self: &Child, buf: *u8, len: i64): i64Only valid while stdin is a pipe: Stdio.Inherit/Null/Read leave nothing for the parent to write, and closeStdin() has already sent EOF.
Child.writeStdinStr
fn Child.writeStdinStr(self: &Child, s: &string): i64Command
pub struct CommandBuilds a child process: program, arguments, working directory, environment, and where each standard stream goes. Each method takes the builder and hands it back, so calls chain; spawn borrows, so one Command can start several children.
The default is stdin and stdout on pipes with stderr merged into stdout. When stderr is not merged, drain both streams, or the child can deadlock filling an unread pipe.
Fields:
program: stringargv: Vec<string>workDir: string: "" means "the parent's working directory", which is not a path a child can meaningfully be given, so there is no ambiguity with a real value.envChanges: Vec<EnvChange>inheritEnv: boolinMode: StdiooutMode: StdioerrMode: Stdio
Command.arg
fn Command.arg(self: Command, value: &string): CommandAppend one argument. Arguments are passed to the child verbatim — there is no shell in the way, so quoting and globbing are neither needed nor performed.
Command.args
fn Command.args(self: Command, values: &Vec<string>): CommandAppend several arguments.
Command.dir
fn Command.dir(self: Command, path: &string): CommandRun the child in path instead of the parent's working directory. A path the child cannot enter fails the child with exit code 126 (the shell's "found but could not execute"), reported by wait() like any other post-fork failure.
Command.env
fn Command.env(self: Command, name: &string, value: &string): CommandSet name in the child's environment. Overrides an inherited value; the parent's own environment is untouched, unlike Env.set.
Command.envClear
fn Command.envClear(self: Command): CommandStart from an empty environment instead of inheriting the parent's. Variables set with env afterwards — or before — still apply; this only drops what was inherited. Note that a child with no PATH cannot itself find programs by name.
Command.envRemove
fn Command.envRemove(self: Command, name: &string): CommandRemove name from the child's environment.
Command.new
fn Command.new(program: &string): CommandA command that runs program (PATH-searched, like the shell) with no arguments. The program name is borrowed and copied in: it is usually a long-lived constant.
Command.spawn
fn Command.spawn(self: &Command): Result<Child>Start the child and return immediately. On POSIX a program that does not exist is reported by the child's exit status (127) rather than here — the exec only happens after the fork, so spawn cannot know.
Command.stderr
fn Command.stderr(self: Command, mode: Stdio): CommandWhere the child's stderr goes. Merge (into stdout) by default.
Command.stdin
fn Command.stdin(self: Command, mode: Stdio): CommandWhere the child's stdin comes from. Pipe by default.
Command.stdout
fn Command.stdout(self: Command, mode: Stdio): CommandWhere the child's stdout goes. Pipe by default.
Process
pub struct ProcessA child started by Process.spawn. wait for its exit status.
Process.pid
fn Process.pid(self: &Process): i32The OS process id. Read-only: the fields are private so a Process cannot be built around a pid this program never spawned and then waited on or signalled.
Process.signal
fn Process.signal(self: &Process, sig: i32): Result<i32> //
fn Process.signal(self: &Process, _sig: i32): Result<i32> // windowsSend signal sig to the child. 0 on success.
Process.spawn
fn Process.spawn(path: &string): Result<Process>Start the program at path in the background, with no arguments and no shell. For arguments, a working directory or redirection, use Command.
Process.wait
fn Process.wait(self: &Process): Result<i32>Block until the child exits and return its status. Inside a green task only this task parks (see waitpidGreen); the rest of the scheduler keeps running.
Stdio
pub enum StdioWhere one of a child's standard streams comes from, or goes to.
Three of these are direction-specific and a spawn that mixes them up is a hard error naming the stream, never a stream silently left on the parent's terminal.
Reading a stream that is not a pipe is a contract violation: writeStdin, readStdout and stdout() require their fd, checked in debug builds.
Variants:
Pipe: A pipe the parent holds the other end of: Child.writeStdin / stdout() / stderr().Inherit: The parent's own stream, unchanged — the child shares the terminal.Null: /dev/null (NUL on Windows).Merge: stderr only: whatever stdout was pointed at, so both arrive interleaved on one fd.Read(string): stdin only: read the file at this path.Write(string): stdout/stderr only: create or truncate the file at this path.Append(string): stdout/stderr only: create or append to the file at this path.
Functions
buildCmdLine
pub fn buildCmdLine(program: &string, args: &Vec<string>): *u8Only on windows.
Build a single CreateProcess command line: program arg1 arg2 ..., each token quoted if it contains whitespace or a quote. Returned buffer is malloc'd + NUL-terminated; CreateProcessA may write into it, so it must be writable (not a string literal).
capture
pub fn capture(cmd: &string): Result<string>Run cmd through the shell and return its stdout and stderr, merged. Errs if the command exits non-zero.
exePath
pub fn exePath(): Result<string>Absolute path of the running executable, so a shipped binary can locate assets next to itself instead of relative to whatever cwd the caller happened to be in. Prefer @embedFile() for assets small enough to inline; this is for the rest.
processAlive
pub fn processAlive(pid: i32): boolWhether a process with this pid exists and this process may signal it. Sends nothing (signal 0), so it is safe on any pid, including one that is not our child and so cannot be waited on. A pid can be reused once its process is reaped.
quoteArg
pub fn quoteArg(arg: &string): stringOnly on windows.
run
pub fn run(cmd: &string): Result<i32>Execute a shell command and return its exit code. Example: let code = run("ls -la")!
terminateProcess
pub fn terminateProcess(pid: i32): boolAsk a process you did not spawn to exit (SIGTERM; TerminateProcess on Windows). For a process you spawned, use its Child, which cannot be forged. Here the pid is all you have, so it may have been reused by an unrelated process: check processAlive and anything else you know about it first. True if the signal was delivered.