Process API
Run other programs, capture their output, talk to them over standard I/O, send signals, and manage the current process's environment and exit.
Overview
The Process global exposes two complementary shapes. The one-shot helper Process.exec(...) runs a command to completion and hands back everything in a single map. The handle-based API Process.spawn(...) returns a live Process instance that can be read from, written to, polled, signaled, and waited on. A small set of process statics covers the current process itself: working directory, environment variables, PID, and exit.
Conventions
Command + args. All spawn/exec calls take an explicit program path and an optional list of arguments. There is no shell interpretation: Process.exec("rm", ["-rf", path]) is not the same as sh -c "rm -rf …".
Bytes are Buffer. Reads return a Buffer; writeBuffer accepts one. write accepts a string and is a convenience for sending text. See the Buffer API.
Stdio modes. Each of stdin, stdout, and stderr can independently be set to one of four modes:
| Mode | Behavior |
|---|---|
"pipe" | The parent reads/writes through a pipe. This is the default. |
"inherit" | The child shares the parent's terminal or pipe. |
"null" | Connected to /dev/null (or the Windows equivalent). |
"pty" | The child gets a real TTY. On Linux/macOS/BSD this uses openpty/forkpty; on Windows it uses ConPTY (Windows 10 1809+). When any stream is "pty", all three are unified through the pseudo-terminal so isatty() is true in the child. |
Options map. spawn and exec accept an optional trailing options map: {stdin, stdout, stderr, cwd}. Unknown keys are ignored.
Numbers and booleans. PIDs, exit codes, and signal numbers are plain numbers. Predicates such as isRunning() return booleans.
Open-failure vs raise. Process.spawn returns null if the spawn fails so scripts can branch with if (p == null). Process.exec raises a runtime error on spawn failure; it always returns the result map otherwise. Argument-validation problems and OS errors raise.
Signaled exit. A child terminated by a signal reports exitCode = 128 + signum, matching the shell convention.
Spawning
Starts a long-running child under the caller's control. Returns a live Process handle, or null if the spawn fails.
command(string) — program path (no shell interpretation)args(list, optional) — list of argument stringsoptions(map, optional) —{stdin, stdout, stderr, cwd}; unknown keys are ignored
Returns: A Process handle, or null on failure.
Runs a command and blocks until the child exits. Closes the child's stdin immediately, so commands that read stdin still terminate. Raises if the spawn itself fails.
command(string) — program path (no shell interpretation)args(list, optional) — list of argument stringsoptions(map, optional) —{stdin, stdout, stderr, cwd}; unknown keys are ignored
Returns: A map {stdout: Buffer, stderr: Buffer, exitCode}.
var r = Process.exec("/bin/echo", ["hello", "world"]) print("rc=%n", r.exitCode) print("out=%s", r.stdout.toUtf8()) // "hello world\n"
var r = Process.exec("/bin/sh", ["-c", "echo out; echo err 1>&2; exit 7"]) print("rc=%n", r.exitCode) // 7 print("stdout=%s", r.stdout.toUtf8()) // "out\n" print("stderr=%s", r.stderr.toUtf8()) // "err\n"
Process Statics
These statics act on the current process, the running script itself, not a child.
Returns the current working directory as a string.
Changes the current working directory. Returns true; raises on failure.
path(string) — the new working directory
Reads an environment variable. Returns its value as a string, or null if it is not set.
key(string) — the variable name
Sets an environment variable. Returns true; raises on failure.
key(string) — the variable namevalue(string) — the value to set
Removes an environment variable. Returns true on success.
key(string) — the variable name
Returns a snapshot of the current environment as a map of string → string.
Returns the current process's PID.
Returns the parent process's PID, or null on Windows.
Immediately exits the current Zym process, the running script itself and not a child, with the given integer code. Does not return. Skips all cleanup; see Notes & Gotchas.
code(number, optional) — the exit code (default0)
print("pid=%n parent=%v cwd=%s", Process.getPid(), Process.getParentPid(), Process.getCwd()) print("home=%v", Process.getEnv("HOME")) Process.setEnv("MY_FLAG", "yes") Process.unsetEnv("MY_FLAG")
The Process Handle
Returned by Process.spawn(...). Once a handle exists, the child is running (or has already exited).
Writing to the Child
Sends the UTF-8 bytes of a string to the child's stdin. Errors if stdin was not piped or has been closed.
s(string) — the text to send
Returns: The number of bytes written.
Sends raw bytes from a Buffer to the child's stdin.
buf(Buffer) — the bytes to send
Returns: The number of bytes written.
Closes the child's stdin and returns true. Many tools (e.g. wc, cat, sort) only finish once stdin is closed.
var p = Process.spawn("/usr/bin/wc", ["-c"]) p.write("hello, zym") p.closeStdin() // wc waits for EOF before printing var rc = p.wait() print("rc=%n", rc) // 0 print("count=%s", p.read().toUtf8()) // "10\n"
Reading from the Child
Blocking read of the currently available stdout bytes. Returns a Buffer; an empty buffer signals EOF.
Blocking read of the currently available stderr bytes. Returns a Buffer; an empty buffer signals EOF.
Drains everything immediately available on stdout without blocking. May return an empty Buffer.
Drains everything immediately available on stderr without blocking. May return an empty Buffer.
Lifecycle
Sends signal to the child and returns true on success. The signal may be a name (e.g. "SIGTERM", "SIGKILL", "SIGINT", "SIGHUP", "SIGQUIT", "SIGUSR1", "SIGUSR2", "SIGSTOP", "SIGCONT", "SIGPIPE") or a number. On Windows the signal argument is ignored and the process is forcibly terminated.
signal(string or number, optional) — the signal to send (default"SIGTERM")
var p = Process.spawn("/bin/sleep", ["10"]) print("pid=%n running=%v", p.getPid(), p.isRunning()) p.kill("SIGTERM") print("rc=%n", p.wait()) // 143 (= 128 + 15)
Blocks until the child exits, then returns its exit code (128 + signum if signaled). Idempotent once the child has exited.
Non-blocking check. Returns the exit code, or null if the child is still running. Reaps the child if it has finished.
Returns true until the child has been waited on or polled to completion.
Returns the child's PID.
Returns the last known exit code, or null if the child is still running.
Notes & Gotchas
Always reap. A handle whose child has exited still has to be wait()ed or poll()ed for the OS to release the slot. The handle's finalizer kills and reaps any still-running child when the handle is collected, but reaping by hand is far more deterministic.
exec closes child stdin for you. It is meant for “run a command, get output.” If a command needs to read from its stdin, use spawn and call write/closeStdin explicitly.
No shell. There is no sh -c step. Pass arguments as a list. To use shell features, run Process.exec("/bin/sh", ["-c", "..."]) (or cmd.exe /C on Windows) explicitly.
read() blocks; readNonBlock() does not. A blocking read() after the child has closed its stdout returns an empty buffer (EOF). Use readNonBlock() inside loops that must not stall.
kill defaults to SIGTERM. Pass "SIGKILL" (or 9) for a forced kill on Unix. On Windows the signal name is accepted but the kernel-level effect is always a forced termination.
Signaled exit codes. A child killed by signal N reports exitCode = 128 + N from wait()/poll()/getExitCode(). This matches the convention POSIX shells use.
PTY caveats. PTY mode is supported on Linux/macOS/BSD (via openpty/forkpty) and on Windows (via ConPTY, requires Windows 10 1809 or newer). When PTY is requested, the three stdio modes are unified onto one TTY. Reading stderr separately is not meaningful and will read from the same stream as stdout. On older Windows versions where ConPTY is unavailable, requesting "pty" will fail to spawn.
Windows differences. The Windows backend uses CreateProcess for plain pipes and CreatePseudoConsole (ConPTY) for PTY mode. getParentPid() returns null. kill(signal) always force-terminates regardless of signal, because Windows has no signal model equivalent to POSIX. Path separators in cwd and arguments use backslashes; pass them as literal strings. Argument quoting follows CreateProcess rules. There is no shell, so to invoke shell features use Process.exec("cmd.exe", ["/C", "..."]) (or powershell.exe) explicitly.
exec's stderr is captured into a Buffer, not interleaved with stdout. For ordered, interleaved output, use spawn with stderr: "inherit" (or stdout: "inherit") and let the OS preserve order on the terminal.
Process.exit terminates the current Zym process (this script), not a child. It is the equivalent of the OS-level _exit / ExitProcess. The running interpreter dies immediately. Finalizers do not run, which means open File handles are not flushed or closed, child processes spawned via Process.spawn are not killed or reaped (they become orphans), and if Console was put into raw mode, alt-screen, or hidden-cursor mode the terminal is left in that state. The top of the script file is the implicit entry point, so exiting gracefully simply means letting the script reach its natural end (or returning from the top level), unless a main(argv) has been included, in which case the end of main. The VM's normal teardown path is what runs every finalizer. In-memory data is reclaimed by the OS regardless, so the risk is side-effect leakage (un-flushed writes, orphaned children, broken terminal state), not memory. Prefer Process.exit(code) only as the last statement of a script after explicit cleanup, or for hard-abort situations where stopping immediately is more important than tidying up.
Examples
Streaming Output Without Blocking
var p = Process.spawn("/bin/sh", ["-c", "for i in 1 2 3; do echo $i; sleep 0.05; done"]) while (p.isRunning()) { var chunk = p.readNonBlock() if (chunk.size() > 0) { Console.write(chunk.toUtf8()) } Time.sleep(10) } // Drain anything that arrived between the last read and exit. var tail = p.read() if (tail.size() > 0) { Console.write(tail.toUtf8()) } print("rc=%n", p.wait())
Customising Stdio, Working Directory, and Environment
// Discard stderr, capture stdout, run in /tmp. Process.setEnv("ZYM_DEMO", "1") var r = Process.exec("/bin/sh", ["-c", "echo cwd=$(pwd) demo=$ZYM_DEMO; echo nope 1>&2"], { stderr: "null", cwd: "/tmp" }) print("rc=%n", r.exitCode) print("out=%s", r.stdout.toUtf8()) // "cwd=/tmp demo=1\n" print("err.size=%n", r.stderr.size()) // 0 (stderr was /dev/null)
Driving an Interactive Child Through a PTY
// PTY mode unifies stdin/stdout/stderr on a single TTY (Linux/macOS/BSD). var p = Process.spawn("/usr/bin/python3", ["-q"], { stdin: "pty", stdout: "pty", stderr: "pty" }) p.write("print(2 + 2)\n") p.write("exit()\n") print("rc=%n", p.wait()) print("out=%s", p.read().toUtf8())