Skip to content

Process Control Module

← Back to Standard Library

System process control and information functions.

Import

use <sys/process>

Overview

The process module provides functions for controlling and querying process state. It uses POSIX functions for Unix portability across macOS, Linux, and other Unix-like systems.

Functions

getcwd

Get the current working directory.

fn getcwd() -> Result<string>

Returns: - Result.Ok(path) containing the absolute path to the current working directory - Result.Err() if the directory cannot be determined (e.g., directory was deleted)

Example:

use <sys/process>

fn main() i32:
    match getcwd():
        Result.Ok(path) ->
            println("Current directory: {path}")
        Result.Err(_) ->
            println("Failed to get current directory")

    return Result.Ok(0)

Error propagation:

use <sys/process>

fn show_cwd() i32 | ProcessError:
    let string cwd = getcwd()??
    println("Working in: {cwd}")
    return Result.Ok(0)

fn main() i32:
    return Result.Ok(show_cwd().realise(1))

chdir

Change the current working directory.

fn chdir(string path) -> Result<i32>

Parameters: - path - Absolute or relative path to the new directory

Returns: - Result.Ok(0) on success - Result.Ok(-1) on failure (e.g., directory does not exist, permission denied)

Example:

use <sys/process>

fn main() i32:
    match chdir("/tmp"):
        Result.Ok(code) ->
            if (code == 0):
                println("Changed to /tmp")
            else:
                println("Failed to change directory")
        Result.Err(_) ->
            println("chdir returned error")

    return Result.Ok(0)

Navigating directories:

use <sys/process>

fn navigate() i32 | ProcessError:
    # Save current directory
    let string original = getcwd()??

    # Change to a different directory
    let i32 result = chdir("/var/log")??

    if (result == 0):
        println("Now in /var/log")
        let string current = getcwd()??
        println("Current: {current}")

        # Restore original directory
        chdir(original)??
        println("Restored to {original}")

    return Result.Ok(0)

fn main() i32:
    return Result.Ok(navigate().realise(1))

exit

Terminate the current process with an exit code.

fn exit(i32 code) -> ~

Parameters: - code - Exit code to return to the parent process (0 for success, non-zero for error)

Returns: - Never returns (terminates the process immediately)

Example:

use <sys/process>

fn main() i32:
    let bool critical_error = true

    if (critical_error):
        println("Critical error occurred")
        exit(1)

    println("This line is never reached")
    return Result.Ok(0)

Exit codes convention:

use <sys/process>

const i32 EXIT_SUCCESS = 0
const i32 EXIT_FAILURE = 1
const i32 EXIT_INVALID_ARGS = 2
const i32 EXIT_CONFIG_ERROR = 3

fn main() i32:
    let bool config_loaded = false

    if (not config_loaded):
        println("ERROR: Configuration file not found")
        exit(EXIT_CONFIG_ERROR)

    # Normal execution continues...
    return Result.Ok(EXIT_SUCCESS)

getpid

Get the process ID of the current process.

fn getpid() -> i32

Returns: - The process ID (always succeeds, never negative)

Example:

use <sys/process>

fn main() i32:
    let i32 pid = getpid()
    println("Process ID: {pid}")
    return Result.Ok(0)

Logging with PID:

use <sys/process>

fn log_message(string message) ~:
    let i32 pid = getpid()
    println("[PID {pid}] {message}")
    return Result.Ok(~)

fn main() i32:
    log_message("Application starting")
    log_message("Processing data")
    log_message("Application finished")
    return Result.Ok(0)

getuid

Get the user ID of the current process.

fn getuid() -> i32

Returns: - The real user ID of the calling process (always succeeds, never negative)

Example:

use <sys/process>

fn main() i32:
    let i32 uid = getuid()
    println("User ID: {uid}")

    if (uid == 0):
        println("Running as root")
    else:
        println("Running as unprivileged user")

    return Result.Ok(0)

Permission checking:

use <sys/process>

fn require_root() Result<i32, StdError>:
    let i32 uid = getuid()

    if (uid != 0):
        println("ERROR: This program must be run as root")
        exit(1)

    return Result.Ok(0)

fn main() i32:
    require_root().realise(1)

    println("Running with root privileges")
    # Perform privileged operations...

    return Result.Ok(0)

run

Spawn an external program by argv vector (PATH-searched, no shell), capturing its standard output and standard error and returning its exit code.

fn run(string cmd, ...string args) -> Result<ProcessOutput, ProcessError>

args is variadic: pass individual arguments directly (run("echo", "hello")), or forward an already-built string[] with a bloom (run("sh", argv...)) when the argument list is computed at runtime. A bloom moves the source array into the call, so don't reuse it afterward.

Parameters: - cmd - The program to run (searched on PATH, e.g. "clang", "echo") - args - The argument vector passed to the program (not including cmd itself)

Returns: - Result.Ok(ProcessOutput) if the program was spawned and ran to completion. A non-zero exit code is still Ok - it is a normal outcome the caller inspects, not an error. The ProcessOutput struct carries: - exit_code (i32) - the program's exit status - stdout_text (string) - everything the program wrote to standard output - stderr_text (string) - everything the program wrote to standard error - Result.Err(ProcessError.SpawnFailed) if the program could not be started (e.g. command not found) - Result.Err(ProcessError.SignalReceived) if the program was killed by a signal

Because arguments are passed as a real argv vector rather than through a shell, there is no shell-quoting or injection surface. Field names are stdout_text / stderr_text (not stdout / stderr, which are reserved stream keywords).

Example:

use <sys/process>
use <io/stdio>

fn main() i32:
    match run("echo", "Mostly Harmless"):
        Result.Ok(out) ->
            println("exit={out.exit_code}")
            println(out.stdout_text)
        Result.Err(ProcessError.SpawnFailed) ->
            println("command not found")
        Result.Err(_) ->
            println("spawn error")

    return Result.Ok(0)

Driving a toolchain (reading a child's diagnostics):

use <sys/process>
use <io/stdio>

fn compile_ir(string path) i32 | ProcessError:
    let string[] argv = from([path, "-o", "out"])
    let ProcessOutput out = run("clang", argv...)??   # bloom: argv is moved into the call
    if (out.exit_code != 0):
        println("{out.stderr_text}")
    return Result.Ok(out.exit_code)

Error Handling

Functions integrate with Sushi's error handling system:

Result-returning functions

getcwd() and chdir() return Result types:

use <sys/process>

# 1. Error propagation (??): the enclosing function carries the error type
fn enter_tmp() string | ProcessError:
    let string dir = getcwd()??
    chdir("/tmp")??
    return Result.Ok(dir)

fn main() i32:
    # 1. Propagation, handled at the boundary with .realise()
    let string dir = enter_tmp().realise("/unknown")
    println("Was in: {dir}")

    # 2. Pattern matching
    match getcwd():
        Result.Ok(path) ->
            println("CWD: {path}")
        Result.Err(_) ->
            println("Failed to get CWD")

    # 3. .realise() for a default value
    let string safe_cwd = getcwd().realise("/unknown")
    println("Safe CWD: {safe_cwd}")

    return Result.Ok(0)

Never-failing functions

getpid() and getuid() always succeed:

use <sys/process>

fn main() i32:
    # No error handling needed
    let i32 pid = getpid()
    let i32 uid = getuid()

    println("PID: {pid}, UID: {uid}")
    return Result.Ok(0)

Never-returning functions

exit() never returns, so code after it is unreachable:

use <sys/process>

fn main() i32:
    if (false):
        exit(1)
        # Compiler knows this is unreachable

    return Result.Ok(0)

Platform-Specific Behavior

macOS (darwin)

Platform-specific implementation in stdlib/src/_platform/darwin/process.py: - Uses standard POSIX getcwd(), chdir(), getpid(), getuid() - PATH_MAX is typically 1024 bytes - Follows BSD semantics

Linux

Platform-specific implementation in stdlib/src/_platform/linux/process.py: - Uses standard POSIX getcwd(), chdir(), getpid(), getuid() - PATH_MAX is typically 4096 bytes - Follows GNU/Linux semantics

Windows (not supported)

Windows support is not yet implemented. The module requires POSIX compatibility.

Common Patterns

Directory Navigation

Run an operation in another directory and always restore the original. with_directory takes the operation as a function value (fn() -> i32) and calls through it — see First-Class Functions.

use <sys/process>

fn with_directory(string path, fn() -> i32 operation) i32 | ProcessError:
    # Save the current directory, then switch to the target
    let string original = getcwd()??
    chdir(path)??

    # Call through the operation function value, unwrapping its result
    let i32 result = operation().realise(-1)

    # Restore the original directory
    chdir(original)??

    return Result.Ok(result)

fn process_files() i32:
    println("Processing files in current directory")
    return Result.Ok(0)

fn main() i32:
    match with_directory("/tmp", process_files):
        Result.Ok(code) ->
            println("Operation completed: {code}")
        Result.Err(_) ->
            println("Operation failed")

    return Result.Ok(0)

Process Information

use <sys/process>

fn print_process_info() ~:
    let i32 pid = getpid()
    let i32 uid = getuid()
    let string cwd = getcwd().realise("/unknown")

    println("Process Information:")
    println("  PID: {pid}")
    println("  UID: {uid}")
    println("  CWD: {cwd}")

    return Result.Ok(~)

fn main() i32:
    print_process_info()
    return Result.Ok(0)

Graceful Exit

use <sys/process>

fn cleanup() ~:
    println("Cleaning up resources...")
    return Result.Ok(~)

fn graceful_exit(i32 code) ~:
    cleanup()??
    println("Exiting with code {code}")
    exit(code)
    return Result.Ok(~)  # Never reached

fn main() i32:
    let bool error = false

    if (error):
        graceful_exit(1)

    println("Normal execution")
    return Result.Ok(0)

Security Considerations

Directory Traversal

Always validate paths to prevent directory traversal attacks:

use <sys/process>

fn safe_chdir(string path) i32 | ProcessError:
    # Validate path doesn't contain ../ components
    # Add your validation logic here

    let i32 code = chdir(path)??
    return Result.Ok(code)

fn main() i32:
    # Bad: User-controlled path without validation
    # chdir(user_input)

    # Good: Validated path
    match safe_chdir("/tmp"):
        Result.Ok(code) ->
            if (code == 0):
                println("Changed directory safely")
        Result.Err(_) ->
            println("Invalid directory")

    return Result.Ok(0)

Privilege Checks

Always check privileges before performing sensitive operations:

use <sys/process>

fn require_non_root() Result<i32, StdError>:
    let i32 uid = getuid()

    if (uid == 0):
        println("ERROR: Do not run this as root")
        exit(1)

    return Result.Ok(0)

fn main() i32:
    require_non_root().realise(1)
    println("Running as unprivileged user")
    return Result.Ok(0)

Exit Code Convention

Use standard exit codes for better shell integration:

use <sys/process>

const i32 EXIT_SUCCESS = 0
const i32 EXIT_FAILURE = 1
const i32 EXIT_USAGE = 2       # Command line usage error
const i32 EXIT_DATAERR = 65    # Data format error
const i32 EXIT_NOINPUT = 66    # Cannot open input
const i32 EXIT_UNAVAILABLE = 69 # Service unavailable
const i32 EXIT_SOFTWARE = 70    # Internal software error
const i32 EXIT_IOERR = 74       # I/O error
const i32 EXIT_CONFIG = 78      # Configuration error

fn main() i32:
    # Use appropriate exit codes
    exit(EXIT_CONFIG)
    return Result.Ok(0)  # Never reached

Example: Simple File Processor

use <sys/process>
use <io/stdio>

fn process_directory(string dir_path) i32 | ProcessError:
    println("Processing directory: {dir_path}")

    # Save current location
    let string original_dir = getcwd()??

    # Change to target directory
    let i32 change_result = chdir(dir_path)??
    if (change_result != 0):
        println("ERROR: Cannot access directory: {dir_path}")
        return Result.Ok(1)

    # Verify we're in the right place
    let string current = getcwd()??
    println("Working in: {current}")

    # Process files here...
    println("Processing files...")

    # Return to original directory
    let i32 restore = chdir(original_dir)??
    if (restore != 0):
        println("WARNING: Could not restore directory")

    return Result.Ok(0)

fn main() i32:
    let i32 pid = getpid()
    let i32 uid = getuid()

    println("File Processor [PID: {pid}, UID: {uid}]")

    # Don't run as root
    if (uid == 0):
        println("ERROR: Do not run as root")
        exit(1)

    # Process each directory (.realise maps a ProcessError to the failure code 1)
    let i32 result1 = process_directory("/tmp").realise(1)
    let i32 result2 = process_directory("/var/log").realise(1)

    if (result1 != 0 or result2 != 0):
        println("Some directories failed to process")
        exit(1)

    println("All directories processed successfully")
    return Result.Ok(0)

See Also