Skip to content

std.process

std.process is the typed process API for command-line programs and subprocess management. It adds ergonomic process exit helpers and provides a small portable subprocess surface. Current-process arguments live only in std.env.

Migration status: RFC 0021 removes the current transitional std.process.args re-export. RFC 0022 also removes the current cwd and chdir forwarding experiments. RFC 0023 replaces the transitional Options.cwd empty-string sentinel with the owned WorkingDirectory.Start | Path(^fs.AbsolutePath) policy described below. RFC 0024 replaces Options.env, Env, Var, inherit_env, and replace_env with the immutable start-snapshot and owned replacement model documented below. RFC 0068's launch-only process_start directory source is separate: it may back any explicitly named root Dir, whereas WorkingDirectory.Start only selects the immutable child-process startup policy and grants no Dir. The source inventory remains transitional until those migrations are implemented.

The subprocess API is intentionally narrower than libuv: it supports explicit executable paths, argument lists, process-start-snapshot or completely replaced child environments, child working-directory policy, inherited/ignored/pipe stdio, async wait, and blocking run. User/group IDs, detached processes, Windows verbatim/hide, PATH lookup, and callback-style spawn APIs are not part of the current API.

Exit

std.process does not re-export process arguments or live current-directory operations. Use std.env.args() for the opaque immutable process-start argument view. Portable safe code has no process.cwd or process.chdir.

Zynx
import std.process;

fn main() {
	process.exit_success();
}

exit(code), exit_success(), and exit_failure() terminate the current process and never return.

Spawning

Zynx
// RFC 0065 target; implementation pending.
import std.process;

fn main() {
	let args: ^[^str] = try ^["hello"];
	let options = process.Options {
		args: args,
		stdin: process.Stdio.Inherit,
		stdout: process.Stdio.Inherit,
		stderr: process.Stdio.Inherit,
	};

	let child = try process.spawn("/bin/echo", options);
	let status = try await (<-child.child).wait();
	assert process.success(status);
}

Under the accepted target, spawn(path) starts path with no extra arguments, the immutable process-start environment snapshot, and the immutable start working directory policy. It does not read either live process-global value at spawn time. spawn(path, options) starts a child with explicit options. The path is the executable path passed to the host; no PATH search is performed.

Options.args excludes the executable path. For example, spawning /bin/echo with ["hello"] produces argv equivalent to ["/bin/echo", "hello"].

Options retains the concept of selecting a working directory only for the new child. Applying that policy never temporarily changes the parent. The current owning-text cwd sentinel is transitional and is replaced by the accepted policy below.

Accepted child-directory policy

Zynx
enum WorkingDirectory {
	Start,
	Path(^fs.AbsolutePath),
}

struct Options {
	directory: WorkingDirectory = .Start,
}

.Start means an independent immutable process-start directory snapshot used only while constructing a child. It never means reading a live parent CWD, passing an inheritance sentinel to the host, observing a previous native chdir, choosing the directory at spawn time, or selecting a named Dir capability. If the target cannot apply the snapshot, spawning fails explicitly.

.Path owns a complete ^fs.AbsolutePath:

Zynx
let raw: fs.Path in _ = try fs.Path("/srv/project/build");
let owned: ^fs.Path = try raw.clone();
let directory: ^fs.AbsolutePath = try fs.AbsolutePath(<-owned);

let options = process.Options {
	directory: .Path(<-directory),
};

This is an adapter for native programs that expect a traditional working directory, not a filesystem capability. POSIX can sometimes preserve an open directory's identity while spawning, but Windows child creation accepts a path, so portable WorkingDirectory must expose path lookup semantics. Namespace mutation may therefore retarget .Path before the host resolves it.

There are no .Inherit, .Current, .None or .Dir variants, nullable/text sentinels, or parent-CWD mutation. WorkingDirectory.Start cannot be converted to Dir or satisfy an @host requirement. RFC 0068's launch-only process_start directory source may explicitly satisfy any named Dir requirement, but it neither depends on the name start nor alters this independent child policy. Explicit child capability transfer is a separate contract.

Environment

The accepted target has exactly two child-environment policies:

Zynx
enum Environment {
	Start,
	Replace(^EnvironmentVariables),
}

struct Options {
	environment: Environment = .Start,
}

.Start means the exact immutable process-start snapshot exposed by std.env.variables(). It never means consulting the live native environment, using a host inheritance sentinel, or observing an earlier unsafe FFI mutation. Every spawn receives an explicitly materialized native environment. A materialization failure fails spawn; it must not silently fall back to host inheritance.

.Replace supplies the complete child environment. It is not an overlay on .Start. An empty EnvironmentVariables collection therefore means a truly empty portable environment; there is no separate .Empty variant.

Zynx
import std.env;
import std.process;

fn main() {
	let variables = try process.EnvironmentVariables([
		process.EnvironmentVariable {
			name: try env.Name("MODE"),
			value: try env.Value("test"),
		},
		process.EnvironmentVariable {
			name: try env.Name("PORT"),
			value: try env.Value("8080"),
		},
	]);

	let options = process.Options {
		environment: .Replace(<-variables),
	};
	_ = options;
}

The default remains the compact process.Options {}. An explicitly empty replacement is constructed with process.EnvironmentVariables([]) and .Replace; no separate variant is needed.

EnvironmentVariable owns one ^env.Name and one ^env.Value:

Zynx
struct EnvironmentVariable {
	let name: ^env.Name,
	let value: ^env.Value,
}

EnvironmentVariables(variables: ^[^EnvironmentVariable]) consumes the list and constructs an opaque immutable collection. Names are non-empty and contain neither = nor NUL; values may be empty but contain no NUL. env.Name and env.Value preserve POSIX bytes, validate Windows WTF-8, and require strict UTF-8 on WASI as documented by std.env.

The collection rejects duplicate names with env.EnvironmentError.DuplicateName:

  • POSIX and WASI compare exact target bytes.
  • Windows uses the target's case-insensitive name comparison, so PATH and Path are duplicates.

There is no first-wins, last-wins, or hidden overwrite policy. The collection cannot be mutated after construction. A process-start snapshot may retain host duplicates because it is a lossless input record; newly constructed replacement state cannot contain that ambiguity.

The runtime always derives .Start and .Replace from owned immutable Zynx records. POSIX materializes an explicit envp; Windows materializes the required sorted UTF-16 block; WASI uses its explicit host interface. The portable model does not synthesize Windows =C: per-drive-CWD bookkeeping entries.

There are no .Inherit, .Current, .Merge, or .Empty variants, inherit_env/replace_env helpers, nullable sentinels, or safe global environment mutation. To derive a replacement from .Start, user code explicitly iterates env.variables(), clones selected names and values, and constructs a new collection.

Stdio

Stdio has exactly three portable policies. Inherit passes through the corresponding process standard stream, Ignore connects an appropriate platform sink/source, and Pipe creates a nominal parent-side pipe. There is no integer-handle payload or fd_stdio escape hatch.

Zynx
// RFC 0065 target; implementation pending.
import std.process;

fn main() {
	let args: ^[^str] = try ^["hello"];
	let options = process.Options {
		args: args,
		stdin: process.Stdio.Inherit,
		stdout: process.Stdio.Pipe,
		stderr: process.Stdio.Inherit,
	};

	let child = try process.spawn("/bin/echo", options);
	// `child.stdout` is `^PipeReader?` and is non-null exactly for `.Pipe`.
	_ = try await (<-child.child).wait();
}

SpawnResult makes pipe presence explicit in its type:

Zynx
struct SpawnResult {
	child: ^Child,
	stdin: ^PipeWriter?,
	stdout: ^PipeReader?,
	stderr: ^PipeReader?,
}

Each optional is non-null exactly when the corresponding option was Stdio.Pipe; there is no closed placeholder value.

PipeReader implements io.Reader; PipeWriter implements io.Writer. Both are nominal owners and release their host resource on drop. Neither exposes fd, take_fd, open, an integer constructor, or a zombie state.

Waiting

Child.wait(^self) -> ^Future<ExitStatus throws(ProcessError | io.IOError)> waits asynchronously for process exit and consumes the wait owner. The returned future retains the host resource through true completion. Dropping or cancelling the future releases it only after the backend proves quiescence; it does not kill the child.

run(path, options) spawns a child and waits synchronously. It rejects Stdio.Pipe options with ProcessError.Invalid because a blocking run cannot drive pipes without risking deadlock.

Use success(status), code(status), and signal(status) to inspect ExitStatus.

Ownership, Allocation, Errors, And Unspecified Behavior

Child, PipeReader, and PipeWriter own host resources and release them on drop. They cannot be observed as open/closed wrappers or converted to integers. Under the accepted target, Options owns its argument values, an optional replacement environment collection, and the optional absolute path carried by WorkingDirectory.Path. Passing it bare borrows the options for the spawn call; child process state is copied by the host as part of spawning.

env.Name and env.Value construction can throw env.EnvironmentError or AllocError; replacement collection construction additionally reports DuplicateName. Spawning and native environment materialization can throw ProcessError.Invalid, io.IOError, or AllocError. Waiting can throw ProcessError or io.IOError. Exit status representation is intentionally inspected through success, code, and signal.

PATH search, shell parsing, detached process management, user/group IDs, callback-style spawn hooks, and platform-specific process flags are outside the current API. Scheduling of child exit observation is runtime behavior.

Use std.env for process arguments and environment variables, std.fs for AbsolutePath and explicit directory capabilities, and std.io for pipe reader/writer semantics. Raw process-resource interop is deferred to target-specific APIs.

API

SymbolSignatureNotes
exitexit(code: i32) -> neverTerminates the current process.
exit_successexit_success() -> neverExits with status 0.
exit_failureexit_failure() -> neverExits with status 1.
spawnspawn(path: str) throws(ProcessError | io.IOError | AllocError) -> ^SpawnResultStarts a child with immutable process-start defaults.
spawnspawn(path: str, options: Options) throws(ProcessError | io.IOError | AllocError) -> ^SpawnResultStarts a child with explicit options.
runrun(path: str, options: Options) throws(ProcessError | io.IOError | AllocError) -> ExitStatusBlocking spawn-and-wait without pipes.
successsuccess(status: ExitStatus) -> boolTrue for exit code 0.
codecode(status: ExitStatus) -> i32?Exit code when exited normally.
signalsignal(status: ExitStatus) -> i32?Signal number when terminated by signal.
ProcessErrorerror ProcessError { Invalid }Invalid paths, names, handles, or options.
ExitStatusstruct: CopyChild exit state. Prefer helper functions.
EnvironmentVariabletarget struct { let name: ^env.Name, let value: ^env.Value }One owned replacement entry.
EnvironmentVariablestarget opaque immutable collectionValidated complete replacement; rejects target-equivalent duplicate names.
Environmenttarget enum Environment { Start, Replace(^EnvironmentVariables) }Immutable child-environment policy with no live inheritance.
Stdioenum Stdio { Inherit, Ignore, Pipe }Portable child stdio policy with no raw-resource payload.
Optionsstruct OptionsCurrent source options; RFC 0023 replaces cwd with owned directory, and RFC 0024 replaces env with environment: Environment = .Start.
WorkingDirectorytarget enum WorkingDirectory { Start, Path(^fs.AbsolutePath) }Accepted child-only directory policy; not yet present in the transitional source inventory.
PipeReaderstruct: io.ReaderOwned read side of a child pipe.
PipeWriterstruct: io.WriterOwned write side of a child pipe.
Childstruct ChildOwned process wait resource; wait(^self) consumes it.
SpawnResultstruct { child: ^Child, stdin: ^PipeWriter?, stdout: ^PipeReader?, stderr: ^PipeReader? }Child plus optional parent-side pipe owners.

Status

This page documents accepted target contracts alongside the still implemented process primitives. The current source retains transitional Var, Env, inherit_env, replace_env, and Options.env; they are removed rather than kept as compatibility aliases by the RFC 0024 migration. RFC 0065 additionally removes Stdio.Fd, fd_stdio, integer pipe access, and closed placeholder pipes. For the exact currently exported surface, see Standard Library Inventory.

Released under the MIT License.