Skip to content

std/process ​

Command execution and child process management.

milo
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 ​

milo
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 ​

milo
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 ​

milo
pub struct Child

Bidirectional 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 ​

milo
fn Child.close(self: &mut Child): void

Close any still-open parent-side fds. Call after wait().

Child.closeStdin ​

milo
fn Child.closeStdin(self: &mut Child): void

Close the child's stdin, sending EOF. Idempotent.

Child.pid ​

milo
fn Child.pid(self: &Child): i32

The 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 ​

milo
fn Child.readStderr(self: &Child, buf: *u8, len: i64): i64

Low-level single read into a raw buffer. Prefer stderr() for streaming.

Child.readStdout ​

milo
fn Child.readStdout(self: &Child, buf: *u8, len: i64): i64

Low-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 ​

milo
fn Child.signal(self: &Child, sig: i32): Result<i32> // 
fn Child.signal(self: &Child, _sig: i32): Result<i32> // windows

Child.spawn ​

milo
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 ​

milo
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 ​

milo
fn Child.stderrFd(self: &Child): i32

The parent end of the child's stderr pipe, or -1 when merged, not a pipe, or taken.

Child.stdinFd ​

milo
fn Child.stdinFd(self: &Child): i32

The parent end of the child's stdin pipe, or -1 when stdin is not a pipe or closed.

Child.stdout ​

milo
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 ​

milo
fn Child.stdoutFd(self: &Child): i32

The parent end of the child's stdout pipe, or -1 when not a pipe or taken.

Child.takeStderr ​

milo
fn Child.takeStderr(self: &mut Child): OwnedFd

As takeStdout, for a separate stderr pipe (a -1 handle when stderr was merged).

Child.takeStdin ​

milo
fn Child.takeStdin(self: &mut Child): OwnedFd

Hand 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 ​

milo
fn Child.takeStdout(self: &mut Child): OwnedFd

Hand 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 ​

milo
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 ​

milo
fn Child.writeStdin(self: &Child, buf: *u8, len: i64): i64

Only 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 ​

milo
fn Child.writeStdinStr(self: &Child, s: &string): i64

Command ​

milo
pub struct Command

Builds 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: string
  • argv: 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: bool
  • inMode: Stdio
  • outMode: Stdio
  • errMode: Stdio

Command.arg ​

milo
fn Command.arg(self: Command, value: &string): Command

Append 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 ​

milo
fn Command.args(self: Command, values: &Vec<string>): Command

Append several arguments.

Command.dir ​

milo
fn Command.dir(self: Command, path: &string): Command

Run 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 ​

milo
fn Command.env(self: Command, name: &string, value: &string): Command

Set name in the child's environment. Overrides an inherited value; the parent's own environment is untouched, unlike Env.set.

Command.envClear ​

milo
fn Command.envClear(self: Command): Command

Start 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 ​

milo
fn Command.envRemove(self: Command, name: &string): Command

Remove name from the child's environment.

Command.new ​

milo
fn Command.new(program: &string): Command

A 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 ​

milo
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 ​

milo
fn Command.stderr(self: Command, mode: Stdio): Command

Where the child's stderr goes. Merge (into stdout) by default.

Command.stdin ​

milo
fn Command.stdin(self: Command, mode: Stdio): Command

Where the child's stdin comes from. Pipe by default.

Command.stdout ​

milo
fn Command.stdout(self: Command, mode: Stdio): Command

Where the child's stdout goes. Pipe by default.

Process ​

milo
pub struct Process

A child started by Process.spawn. wait for its exit status.

Process.pid ​

milo
fn Process.pid(self: &Process): i32

The 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 ​

milo
fn Process.signal(self: &Process, sig: i32): Result<i32> // 
fn Process.signal(self: &Process, _sig: i32): Result<i32> // windows

Send signal sig to the child. 0 on success.

Process.spawn ​

milo
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 ​

milo
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 ​

milo
pub enum Stdio

Where 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 ​

milo
pub fn buildCmdLine(program: &string, args: &Vec<string>): *u8

Only 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 ​

milo
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 ​

milo
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 ​

milo
pub fn processAlive(pid: i32): bool

Whether 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 ​

milo
pub fn quoteArg(arg: &string): string

Only on windows.

run ​

milo
pub fn run(cmd: &string): Result<i32>

Execute a shell command and return its exit code. Example: let code = run("ls -la")!

terminateProcess ​

milo
pub fn terminateProcess(pid: i32): bool

Ask 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.