System, Environment, And Processes

Import sys for process information, environment variables, script arguments, sleeping, exit status, and subprocess execution.

sys is about the running process and the host operating system. It does not own filesystem mutation; use io for files/directories and path for path string manipulation.

import sys;
import io;

io.println(sys.platform());
io.println(sys.cwd());

Module Boundaries

Need Module
Read/write files, make directories, chmod, temp files io
Join, clean, inspect, glob path strings path / pathlib
Current user, environment variables, cwd, process info sys
Run or manage subprocesses sys or process

Process Information

Function Returns Description
platform() string Host OS name, such as linux, darwin, or windows
osVersion() string OS kernel/release string (Linux: uname release)
arch() string CPU architecture, such as amd64 or arm64
hostname() string Hostname
pid() int Current process ID
goroutineId() int Id of the current goroutine (see below)
username() string Current OS username
homedir() string Current user's home directory
tmpdir() string Host temporary directory
cwd() string Current working directory
io.println(sys.hostname());
io.println(sys.pid() as string);
io.println(sys.homedir());
io.println(sys.tmpdir());

platform() names the OS; osVersion() complements it with the kernel/release string (on Linux, the uname release such as 6.6.0-generic). osVersion() is implemented on Linux; on platforms where it is not yet available it throws a catchable error naming the platform rather than returning an empty string.

Use sys.tmpdir() with path.join when you need a location, and io.tempFile or io.tempDir when you want Geblang to create a unique path for you.

sys.goroutineId() returns the id of the goroutine that calls it. It is stable for the life of that goroutine and unique among goroutines running at the same time (an id may be reused only after the goroutine that held it has exited). It is an advanced primitive for building goroutine-local or request-scoped state: key a store.Store by the id, and clear that key when the goroutine's work finishes so a later goroutine reusing the id starts clean. Most code never needs it; prefer passing state explicitly or sharing through a store.Store.

Bundled Resources

Function Returns Description
bundleDir() string Extract directory of a built binary's embedded resources, or "" when not running from a bundle

geblang build can embed non-code files (templates, static assets, data) listed under resources: in geblang.yaml. A running program locates them through sys.bundleDir(): resolve resource paths against it, falling back to the project directory when it is empty, so the same code works in development and in a built binary.

let base = sys.bundleDir();
if (base == "") { base = "."; }
let html = io.readText(base + "/templates/page.html");

See Bundling And Standalone Executables for the resources: manifest field and how embedding works.

Environment Variables

Function Returns Description
getenv(name) `string null`
setenv(name, value) void Set an environment variable for the current process and child processes
environ() dict<string, string> Snapshot of the current environment
let env = sys.getenv("APP_ENV");
if (env == null) {
    sys.setenv("APP_ENV", "development");
}

let all = sys.environ();
io.println(all["PATH"]);

setenv affects the current Geblang process and subprocesses launched after the call. It does not change the parent shell's environment.

Use the dotenv module when loading .env files:

import dotenv;

if (io.exists(".env")) {
    dotenv.loadAndApply(".env");
}

Script Arguments

Function Returns Description
args() list<string> Arguments passed to the script
let argv = sys.args();
if (argv.length() > 0) {
    io.println("first arg: " + argv[0]);
}

For user-facing command-line applications, prefer the cli module for option parsing, help text, prompts, and terminal formatting.

Sleep And Exit

Function Returns Description
sleep(ms) void Block for the given milliseconds
exit(code) never Exit the script with the given process status
sys.sleep(500);
sys.exit(0);

sys.sleep blocks the current execution thread. In async code, prefer async.sleep(ms) so the scheduler can continue other work while waiting.

Running Subprocesses

Use sys.run when you want to run a command, wait for it to finish, and inspect captured stdout/stderr.

Function Returns Description
run(command, args) dict Run a command with a list of arguments
shell(command) dict Run a shell command through /bin/sh -c
runWithOptions(options) dict Run with cwd/env/timeout options

Result dictionary:

Field Type Description
code int Exit code, where 0 normally means success
stdout string Captured standard output
stderr string Captured standard error
timedOut bool Whether the process was killed by timeout
let result = sys.run("git", ["log", "--oneline", "-5"]);
if ((result["code"] as int) == 0) {
    io.println(result["stdout"]);
} else {
    io.stderrWrite(result["stderr"]);
}

sys.shell(command) is convenient for shell features such as pipes and redirection, but it executes through the host shell. Do not pass untrusted input into a shell command string.

let result = sys.shell("ls -la | head -5");

runWithOptions

sys.runWithOptions(options) accepts:

Option Type Description
command string Required executable
args list<string> Arguments, default []
cwd string Working directory
env dict<string, string> Extra environment values merged into the current environment
timeoutMs int Kill the process after this many milliseconds
let result = sys.runWithOptions({
    "command": "npm",
    "args": ["run", "build"],
    "cwd": "/app",
    "env": {"NODE_ENV": "production"},
    "timeoutMs": 30000
});

if (result["timedOut"] as bool) {
    io.stderrWrite("build timed out\n");
}

The env dictionary is merged with the current process environment. To modify or pass through a large environment, start with sys.environ():

let env = sys.environ();
env["PATH"] = "/usr/local/bin:" + env["PATH"];

let result = sys.runWithOptions({
    "command": "node",
    "args": ["server.js"],
    "env": env
});

Streaming Subprocess Handles

Use sys.start when you need to interact with a process while it runs.

Function Returns Description
start(command, args) process handle Start a process with stdin/stdout/stderr pipes
startWithOptions(options) process handle Start with cwd/env options
processWrite(proc, text) int Write text to stdin
processCloseStdin(proc) void Close stdin
processReadStdout(proc) string Read all remaining stdout
processReadStderr(proc) string Read all remaining stderr
processReadStdoutN(proc, n) string Read up to n bytes from stdout
processReadStderrN(proc, n) string Read up to n bytes from stderr
processWait(proc) int Wait for exit and return the code
processKill(proc) void Send SIGKILL
processSignal(proc, signal) void Send KILL, TERM, INT, or HUP
processPid(proc) int Return the OS process ID
let proc = sys.start("cat", []);

sys.processWrite(proc, "hello\n");
sys.processCloseStdin(proc);

io.println(sys.processReadStdout(proc));
let code = sys.processWait(proc);
io.println(code);

startWithOptions accepts command, args, cwd, and env. It does not accept timeoutMs; manage long-running process lifecycle yourself with processKill, processSignal, or the object-oriented process module.

Async Patterns

sys.run and sys.runWithOptions are synchronous. Use async.run to start blocking work concurrently:

import async;
import sys;

let lintTask = async.run(func(): dict {
    return sys.run("eslint", ["src/"]);
});

let testTask = async.run(func(): dict {
    return sys.run("pytest", ["tests/"]);
});

let lintResult = await lintTask;
let testResult = await testTask;

Inside async functions, prefer async.sleep over sys.sleep.

process Module

Import process for a class-based alternative to sys.run and sys.start. The process module wraps the same underlying functionality in Result and Process objects.

import process;

let result = process.run("git", ["status", "--short"]);
if (result.isOk()) {
    io.println(result.stdout());
}

Module Functions

Function Returns Description
process.run(cmd, args...) Result Run command and wait
process.runWithOptions(opts) Result Run with options dict
process.shell(cmd) Result Run through the host shell
process.start(cmd, args...) Process Start a live process
process.startWithOptions(opts) Process Start with options dict

Both run and start accept either variadic string arguments or a command followed by a list.

process.Result

Method Returns Description
isOk() bool Exit code is 0
code() int Exit code
stdout() string Captured stdout
stderr() string Captured stderr
timedOut() bool Process was killed by timeout

process.Process

Method Returns Description
write(text) int Write text to stdin
closeStdin() void Close stdin
readStdout() string Read all remaining stdout
readStderr() string Read all remaining stderr
readStdoutN(n) string Read up to n bytes from stdout
readStderrN(n) string Read up to n bytes from stderr
wait() int Wait for exit
kill() void Send SIGKILL
signal(name) void Send KILL, TERM, INT, or HUP
pid() int OS process ID
let proc = process.start("cat", []);
proc.write("hello\n");
proc.closeStdin();
io.println(proc.readStdout());
io.println(proc.wait());

Current-process identity and credentials

The running script is itself a process, so its identity and credentials are process functions. These are read-only and need no launch capability.

Function Returns Description
process.pid() int Current process id (same value as sys.pid())
process.ppid() int Parent process id
process.uid() int Real user id (unix)
process.gid() int Real group id (unix)
process.euid() int Effective user id (unix)
process.egid() int Effective group id (unix)
process.groups() list<int> Supplementary group ids (unix)

The credential functions (uid, gid, euid, egid, groups) are unix concepts. On platforms where they do not apply they throw a catchable error naming the platform rather than returning a misleading zero.

Inspecting other processes

Query processes by pid. These are read-only and need no launch capability.

Function Returns Description
process.list() list<dict<string, any>> Running processes, each {pid, ppid, name, cmdline, state}
process.info(pid) dict<string, any> One process, or null when the pid is absent
process.exists(pid) bool Whether a process with that pid is running
let self = process.info(process.pid());
io.println(self["name"]);
for (entry in process.list()) {
    io.println("${entry["pid"]} ${entry["name"]}");
}

list and info are implemented on Linux (read from /proc). On platforms where they are not yet available they throw a catchable error naming the platform. exists is available on all platforms.

Privileged operations

Changing credentials or signalling an arbitrary process is dangerous, so these operations are gated behind the --allow-process-control launch flag. Without it, each throws a catchable PermissionError whose message names the flag.

Function Returns Description
process.setuid(uid) void Set the real user id (unix, root-only)
process.setgid(gid) void Set the real group id (unix, root-only)
process.kill(pid) void Send SIGKILL to an arbitrary pid
process.signal(pid, name) void Send a named signal (TERM, KILL, INT, HUP, QUIT, USR1, USR2, STOP, CONT) to an arbitrary pid
try {
    process.signal(targetPid, "TERM");
} catch (PermissionError e) {
    io.println(e.message);
}

Enable the privileged subset by launching with the flag:

geblang --allow-process-control app.gb

The Process handle methods kill() and signal() (above) act on a child the script launched and stay ungated; only process.kill(pid) and process.signal(pid, name) by arbitrary pid are gated. On Windows the signal set is limited to KILL; other names throw a catchable error.

Intercepting Signals

sys.onSignal(name, handler) traps a named signal for the current process. The handler receives the canonical signal name and runs in an isolated execution context (the same model as HTTP handlers), so share state through store and terminate explicitly with sys.exit:

import io;
import sys;

sys.onSignal("SIGINT", func(string name): void {
    io.println("shutting down (${name})");
    sys.exit(0);
});

Supported names: SIGINT, SIGTERM, SIGHUP, SIGQUIT, SIGUSR1, SIGUSR2 (the SIG prefix is optional; SIGUSR1/SIGUSR2 are not available on Windows). SIGKILL cannot be trapped and is rejected. Registering a handler replaces any previous handler for that signal; sys.clearSignal(name) restores default delivery.

sys.raise(name) sends a signal to the current process - useful in tests and for re-raising after cleanup:

import sys;
import store;

let state = store.Store();
state.set("reloads", 0);

sys.onSignal("SIGHUP", func(string name): void {
    state.update("reloads", func(any old): any { return (old as int) + 1; });
});

A handler that returns normally resumes the program; one that calls sys.exit(code) runs the runtime's cleanup (open files, database connections, destructors) and terminates with that code.