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:

ModeBehavior
"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

Process.spawn(command, args?, options?)

Starts a long-running child under the caller's control. Returns a live Process handle, or null if the spawn fails.

Returns: A Process handle, or null on failure.

Process.exec(command, args?, options?)

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.

Returns: A map {stdout: Buffer, stderr: Buffer, exitCode}.

one-shot capture
var r = Process.exec("/bin/echo", ["hello", "world"])
print("rc=%n", r.exitCode)
print("out=%s", r.stdout.toUtf8())   // "hello world\n"
separate stdout and stderr
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.

Process.getCwd()

Returns the current working directory as a string.

Process.setCwd(path)

Changes the current working directory. Returns true; raises on failure.

Process.getEnv(key)

Reads an environment variable. Returns its value as a string, or null if it is not set.

Process.setEnv(key, value)

Sets an environment variable. Returns true; raises on failure.

Process.unsetEnv(key)

Removes an environment variable. Returns true on success.

Process.getEnvAll()

Returns a snapshot of the current environment as a map of string → string.

Process.getPid()

Returns the current process's PID.

Process.getParentPid()

Returns the parent process's PID, or null on Windows.

Process.exit(code?)

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.

querying and editing the environment
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

p.write(s)

Sends the UTF-8 bytes of a string to the child's stdin. Errors if stdin was not piped or has been closed.

Returns: The number of bytes written.

p.writeBuffer(buf)

Sends raw bytes from a Buffer to the child's stdin.

Returns: The number of bytes written.

p.closeStdin()

Closes the child's stdin and returns true. Many tools (e.g. wc, cat, sort) only finish once stdin is closed.

feeding stdin to a child
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

p.read()

Blocking read of the currently available stdout bytes. Returns a Buffer; an empty buffer signals EOF.

p.readErr()

Blocking read of the currently available stderr bytes. Returns a Buffer; an empty buffer signals EOF.

p.readNonBlock()

Drains everything immediately available on stdout without blocking. May return an empty Buffer.

p.readErrNonBlock()

Drains everything immediately available on stderr without blocking. May return an empty Buffer.

Lifecycle

p.kill(signal?)

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.

killing a long-running child
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)
p.wait()

Blocks until the child exits, then returns its exit code (128 + signum if signaled). Idempotent once the child has exited.

p.poll()

Non-blocking check. Returns the exit code, or null if the child is still running. Reaps the child if it has finished.

p.isRunning()

Returns true until the child has been waited on or polled to completion.

p.getPid()

Returns the child's PID.

p.getExitCode()

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())