Process
stdlib::process is a builder-style API for spawning external commands with argv-level control: captured output, env overrides, working directory, piped stdin, exit codes, and live read/write pipes. It uses fork+execvp on POSIX and CreateProcess on Windows — there is no shell intermediary, so there is no quoting hazard and no shell-injection surface.
Import
import stdlib::process::*;
Overview
The typical flow is: build a Command, configure it with chained methods, then call run() (synchronous, blocks until exit) or spawn() (non-blocking, returns a Child).
let cmd: *Command = Command::new("git");
cmd.arg("rev-parse").arg("HEAD").capture_out();
let r: !ProcessResult = cmd.run();
if r.ok {
println!("HEAD =", r.val.stdout.trim());
}
cmd.free();
Public surface at a glance
| Item | Kind | Summary |
|---|---|---|
ProcessResult |
struct | Outcome of a finished run (exit code, captured output, signal info). |
Command |
struct + builder | Configure and launch a subprocess. |
Child |
struct | Handle to a spawned, still-running process. |
ChildStdin / ChildStdout / ChildStderr |
struct | Live pipe handles exposed after spawn(). |
Command::new / arg / args / env / env_clear / cwd / stdin_str |
builder fn | Configure program, args, env, dir, stdin. |
Command::capture_out / capture_err / capture |
builder fn | Request output capture (consumed by run()). |
Command::pipe_stdin / pipe_stdout / pipe_stderr |
builder fn | Request live pipes (consumed by spawn()). |
Command::run |
fn | Run synchronously; returns !ProcessResult. |
Command::spawn |
fn | Start without waiting; returns !Child. |
Command::free |
fn | Release the builder. |
Child::wait / kill |
fn | Block for exit / signal the child. |
ChildStdin::write / close |
fn | Feed and close the child's stdin. |
ChildStdout::read / read_all / close |
fn | Read the child's stdout. |
ChildStderr::read / read_all / close |
fn | Read the child's stderr. |
process_kill |
fn | Signal an arbitrary PID. |
process_exists |
fn | Liveness check for a PID. |
Result and configuration types
ProcessResult
The outcome of a finished run. Returned by Command::run() and Child::wait().
pub struct ProcessResult {
pub exit_code: i32, // 0 = success
pub stdout: string, // empty if not captured
pub stderr: string, // empty if not captured
pub signaled: bool, // true if child died from a signal (POSIX)
pub signal: i32, // signal number; 0 if !signaled
}
| Field | Type | Meaning |
|---|---|---|
exit_code |
i32 |
Process exit status; 0 is success. On POSIX a signal death is reported as 128 + signal. |
stdout |
string |
Captured stdout, or "" if no capture flag was set. |
stderr |
string |
Captured stderr, or "" if no capture flag was set. |
signaled |
bool |
true if the child was killed by a signal (POSIX only). |
signal |
i32 |
The terminating signal number; 0 when signaled is false. |
Command
The builder. Construct with Command::new, configure with the chained methods below, then run. The fields are public but you normally only touch them through methods.
pub struct Command {
pub program: string,
pub args: *Vector<string>,
pub env_kv: *Vector<string>, // entries are "KEY=VAL" strings
pub cwd_path: string,
pub stdin_payload: string,
pub flags: i32, // bit 0 capture stdout, bit 1 capture stderr,
// bit 2 env_clear, bit 3 pipe stdin,
// bit 4 pipe stdout, bit 5 pipe stderr
}
Child
A handle to a spawned, still-running process. The pipe handles are non-null only when the matching pipe_* builder was used.
pub struct Child {
pub pid: i32,
pub stdin: *ChildStdin, // null unless pipe_stdin()
pub stdout: *ChildStdout, // null unless pipe_stdout()
pub stderr: *ChildStderr, // null unless pipe_stderr()
}
Pipe handle structs
Each wraps a single OS handle (HANDLE on Windows, fd on POSIX).
pub struct ChildStdin { pub handle: i64 }
pub struct ChildStdout { pub handle: i64 }
pub struct ChildStderr { pub handle: i64 }
Building a command
All builder methods return *Command, so they chain. Command::new allocates; release it with free() (ideally via defer).
new / free
| Function | Signature | Description |
|---|---|---|
new |
fn new(program: string) -> *Command |
Create a builder for program. Args/env/cwd/stdin default to inherit; output is forwarded to the parent unless a capture_* method is called. |
free |
fn free(self: *Command) |
Release the builder (its args + env_kv vectors and the struct). Pair with defer. |
let cmd: *Command = Command::new("echo");
defer cmd.free();
cmd.arg("hi").run();
Arguments, environment, and directory
| Method | Signature | Description |
|---|---|---|
arg |
fn arg(self: *Command, a: string) -> *Command |
Append one positional argument. |
args |
fn args(self: *Command, vs: *Vector<string>) -> *Command |
Append every string in vs as an argument (copies the entries; you still own vs). |
env |
fn env(self: *Command, key: string, value: string) -> *Command |
Set an env var for the child (override on top of the inherited env). Stored internally as "KEY=VAL". |
env_clear |
fn env_clear(self: *Command) -> *Command |
Discard the parent's environment; only vars set via env(k,v) reach the child. |
cwd |
fn cwd(self: *Command, path: string) -> *Command |
Set the child's working directory. |
stdin_str |
fn stdin_str(self: *Command, s: string) -> *Command |
Feed s to the child's stdin (run() only); the pipe is closed after writing, signaling EOF. |
let extras: *Vector<string> = Vector::new();
extras.push_all!("--verbose", "input.txt");
let cmd: *Command = Command::new("tool");
cmd.arg("build")
.args(extras)
.env_clear()
.env("PATH", "/usr/bin:/bin") // only PATH reaches the child now
.cwd("/tmp/repo")
.stdin_str("data\n")
.capture();
let r: !ProcessResult = cmd.run();
if r.ok { println!("exit:", r.val.exit_code); }
cmd.free();
extras.free();
Output capture
These set capture flags consumed by run(). Without any of them, the child's stdout/stderr inherit the parent's and ProcessResult.stdout/.stderr are "".
| Method | Signature | Description |
|---|---|---|
capture_out |
fn capture_out(self: *Command) -> *Command |
Capture stdout into ProcessResult.stdout (sets flag bit 0). |
capture_err |
fn capture_err(self: *Command) -> *Command |
Capture stderr into ProcessResult.stderr (sets flag bit 1). |
capture |
fn capture(self: *Command) -> *Command |
Capture both (equivalent to capture_out().capture_err(); sets bits 0+1). |
let cmd: *Command = Command::new("wc");
cmd.arg("-l").stdin_str("a\nb\nc\n").capture_out().capture_err();
let r: !ProcessResult = cmd.run();
if r.ok {
if r.val.exit_code == 0 {
println!("lines:", r.val.stdout.trim()); // "3"
} else {
println!("failed:", r.val.stderr);
}
}
cmd.free();
Capturing only stderr is the idiom for "run a compiler/linter and show its diagnostics on failure":
let cmd: *Command = Command::new("gcc");
cmd.arg("missing.c").capture_err();
let r: !ProcessResult = cmd.run();
if r.ok {
if r.val.exit_code != 0 {
println!("compile failed:", r.val.stderr.trim());
} else {
println!("ok");
}
} else {
println!("could not launch gcc:", r.err);
}
cmd.free();
Live pipe requests
These set flags honored only by spawn(). After spawn() the matching Child.stdin / .stdout / .stderr handle is non-null.
| Method | Signature | Description |
|---|---|---|
pipe_stdin |
fn pipe_stdin(self: *Command) -> *Command |
Open a live stdin pipe (flag bit 3); use child.stdin.write(s) / .close(). |
pipe_stdout |
fn pipe_stdout(self: *Command) -> *Command |
Open a live stdout pipe (flag bit 4); read with child.stdout.read(n) / .read_all(). |
pipe_stderr |
fn pipe_stderr(self: *Command) -> *Command |
Open a live stderr pipe (flag bit 5); read with child.stderr.read(n) / .read_all(). |
Running
run
pub fn run(self: *Command) -> !ProcessResult
Runs synchronously and blocks until the child exits. Honors capture_*, stdin_str, env, env_clear, cwd. Returns Err only on spawn failure; a non-zero exit is still ok(...).
let r: !ProcessResult = Command::new("echo").arg("hi").capture_out().run();
if r.ok {
println!("exit:", r.val.exit_code);
println!("out:", r.val.stdout.trim()); // "hi"
}
Because run() returns !ProcessResult, you can propagate spawn failures with ? and reserve Err for genuine launch problems, mapping a non-zero exit to your own error:
fn capture_uname() -> !string {
let cmd: *Command = Command::new("uname");
defer cmd.free();
let r: ProcessResult = cmd.arg("-s").capture_out().run()?;
if r.exit_code != 0 { return err("uname failed"); }
return ok(r.stdout.trim());
}
spawn
pub fn spawn(self: *Command) -> !Child
Starts the process without waiting and returns a Child. stdin/stdout/stderr inherit the parent's unless a pipe_* method was called. The capture_* flags and stdin_str are ignored on this path — use the live pipes instead.
let r: !Child = Command::new("sleep").arg("60").spawn();
if r.ok {
let ch: *Child = &r.val;
println!("pid:", ch.pid);
let k: ! = ch.kill(9); // SIGKILL on POSIX, TerminateProcess on Windows
if !k.ok { println!(k.err); }
let res: !ProcessResult = ch.wait();
if res.ok {
if res.val.signaled {
println!("killed by signal", res.val.signal);
} else {
println!("exited", res.val.exit_code);
}
}
}
Controlling a Child
wait / kill
| Method | Signature | Description |
|---|---|---|
wait |
fn wait(self: *Child) -> !ProcessResult |
Block until the child exits and collect its result. stdout/stderr are always empty (spawn doesn't capture). |
kill |
fn kill(self: *Child, sig: i32) -> ! |
Send signal sig. POSIX: kill(pid, sig). Windows: ignores sig and calls TerminateProcess. Returns Err on failure. |
let r: !Child = Command::new("true").spawn();
if r.ok {
let res: !ProcessResult = r.val.wait();
if res.ok { println!("exited", res.val.exit_code); }
}
Streaming pipes
When spawn() is called after pipe_stdin / pipe_stdout / pipe_stderr, the Child exposes live read/write handles. Every read/write/close method returns a Result and surfaces OS errors as err("...").
ChildStdin
| Method | Signature | Description |
|---|---|---|
write |
fn write(self: *ChildStdin, s: string) -> !i32 |
Write s to the child's stdin; returns bytes written. |
close |
fn close(self: *ChildStdin) -> ! |
Close the pipe (sends EOF). Idempotent — sets the handle to -1. |
ChildStdout
| Method | Signature | Description |
|---|---|---|
read |
fn read(self: *ChildStdout, n: i32) -> !string |
Read up to n bytes; returns "" on EOF. |
read_all |
fn read_all(self: *ChildStdout) -> !string |
Read everything until EOF. |
close |
fn close(self: *ChildStdout) -> ! |
Close the stdout pipe. |
ChildStderr
| Method | Signature | Description |
|---|---|---|
read |
fn read(self: *ChildStderr, n: i32) -> !string |
Read up to n bytes; returns "" on EOF. |
read_all |
fn read_all(self: *ChildStderr) -> !string |
Read everything written to stderr until EOF. |
close |
fn close(self: *ChildStderr) -> ! |
Close the stderr pipe. |
A full round-trip through cat — write input, close stdin for EOF, drain stdout, then wait():
let cmd: *Command = Command::new("cat");
cmd.pipe_stdin().pipe_stdout();
let r: !Child = cmd.spawn();
if r.ok {
let ch: *Child = &r.val;
if ch.stdin != null {
let w: !i32 = ch.stdin.write("hello\n");
if w.ok { println!("wrote", w.val); }
let c: ! = ch.stdin.close(); // EOF
if !c.ok { println!(c.err); }
}
if ch.stdout != null {
let all: !string = ch.stdout.read_all();
if all.ok { print!(all.val); }
let oc: ! = ch.stdout.close();
if !oc.ok { println!(oc.err); }
}
let done: !ProcessResult = ch.wait();
if done.ok { println!("exit", done.val.exit_code); }
}
cmd.free();
To stream live output without buffering everything, loop on bounded read(n) and stop when it returns an empty string (EOF):
let r: !Child = Command::new("ls").arg("-la").pipe_stdout().spawn();
if r.ok {
let ch: *Child = &r.val;
if ch.stdout != null {
while true {
let chunk: !string = ch.stdout.read(4096);
if !chunk.ok { println!(chunk.err); break; }
if chunk.val.len() == 0 { break; } // EOF
print!(chunk.val);
}
let c: ! = ch.stdout.close();
if !c.ok { println!(c.err); }
}
let done: !ProcessResult = ch.wait();
if done.ok { println!("exit", done.val.exit_code); }
}
Module-level helpers
These act on a raw PID, with no Command/Child involved.
| Function | Signature | Description |
|---|---|---|
process_kill |
fn process_kill(pid: i32, sig: i32) -> ! |
Send signal sig to pid. POSIX: kill(pid, sig). Windows: TerminateProcess (sig ignored). Returns Err on failure. |
process_exists |
fn process_exists(pid: i32) -> bool |
true if a live process has this PID. POSIX: kill(pid, 0). Windows: OpenProcess + GetExitCodeProcess(STILL_ACTIVE). |
if process_exists(1234) {
let r: ! = process_kill(1234, 15); // SIGTERM
if !r.ok { println!(r.err); }
} else {
println!("pid 1234 not running");
}
Caveats and platform notes
- No shell.
Command::new("ls -la")looks for a program literally named
ls -la. Pass the program and each argument separately: Command::new("ls").arg("-la"). To use shell features, invoke the shell explicitly:
let cmd: *Command = Command::new("sh");
cmd.arg("-c").arg("ls -la | wc -l").capture_out();
let r: !ProcessResult = cmd.run();
if r.ok && r.val.exit_code == 0 {
println!("entries:", r.val.stdout.trim());
}
cmd.free();
```
- **Streaming + `run()` don't mix.** `run()` ignores `pipe_*`; `spawn()` ignores
`capture_*` and `stdin_str`. Pick one model per command.
- **Signals are POSIX-shaped.** `signaled` / `signal` are only meaningful on
POSIX; on Windows `kill` always force-terminates regardless of `sig`.
- **Exit code `127`** from a `run()` that returned `ok` usually means the program
was not found or `chdir` to the `cwd` failed in the child (`execvp` / `chdir`
failure inside the forked child calls `_exit(127)`).
- **Free the builder.** `Command::new` allocates with `malloc`; always `free()`
it (use `defer cmd.free();` right after construction) to release the struct and
its internal vectors.
## See also
- [`stdlib::os`](09-os.md) — lower-level OS primitives (`os_shell` and friends).
- [`stdlib::env`](10-env.md) — read/inspect the parent's environment variables.
- [`fs-io`](08-fs-io.md) — `eprintln` and stream helpers for logging captured
output.