Axle v0.14.1

CLI tools and system access

Build a command-line tool — read args and env, find user config paths, run child processes, log output. The bricks live in std/sys (a multi-module package).

Command-line arguments

use std::sys::process;

fn main() : i32 ! IndexOutOfBoundsException {
    let n : i32 = process::argc();
    let prog : string = process::programName();

    let i : i32 = 0;
    while (i < n) {
        let a : string = process::argAt(i);
        // … inspect a …
        i = i + 1;
    }
    return 0;
}
  • argc() — count of arguments including the program name at index 0.
  • argAt(i) — the i-th arg ; throws IndexOutOfBoundsException on out-of-range.
  • programName() — same as argAt(0), exposed separately for clarity.

Flag and option parsing

std/sys/cli ships small helpers for ad-hoc CLI parsing :

use std::sys::cli;

fn main() : i32 {
    if (cli::argParseFlag("verbose")) {
        // -v / --verbose mode
    }
    if (cli::argHasOption("input")) {
        let path : string = cli::argParseOption("input");
        // …
    }
    let firstPos : i32 = cli::argFirstPositional();
    // firstPos is the index of the first non-flag argument, or -1
    return 0;
}

Pass the bare flag name — "verbose", not "--verbose". The helper adds the dashes itself and matches both the --name long form and the -name short form, plus --name=value and --name value for options. For real CLIs with subcommands, use a proper argument parser (or build one on top — process::argc + argAt give you the raw vector).

Environment variables

use std::sys::process;

fn main() : i32 {
    let term : string = process::getEnv("TERM");        // "" if absent
    if (process::hasEnv("DEBUG")) {
        // explicit presence check — useful for "set but empty"
    }

    process::setEnv("MY_APP_KEY", "value");
    let fallback : string = process::envOrDefault("PORT", "8080");

    process::unsetEnv("OLD_VAR");
    return 0;
}

getEnv(key) returns "" for missing vars — combine with hasEnv to distinguish “missing” from “present but empty”.

Standard paths

use std::sys::env;

fn main() : i32 ! IOException {
    let home   : string = env::homeDir();         // /home/user, C:\Users\user, /Users/user
    let temp   : string = env::tempDir();         // /tmp, %TEMP%, /var/folders/...
    let config : string = env::configDir();       // $XDG_CONFIG_HOME / %APPDATA% / ~/Library/...
    let cache  : string = env::cacheDir();
    let data   : string = env::dataDir();
    let exe    : string = env::currentExe();      // absolute path of the running binary
    return 0;
}

Use these instead of hardcoded paths — they Do The Right Thing per-OS without if (sys::osName() == "Linux") ladders.

Each returns "" when the host names no such convention, and none of them carries a trailing separator — join with Path or with the platform’s sys::fileSeparator() rather than assuming one is there. A variable set to the empty string counts as unset: it names no directory, so the next fallback is used instead.

Process introspection

use std::sys::process;

fn main() : i32 ! IOException {
    let pid    : i64 = process::pid();
    let parent : i64 = process::parentPid();
    let cwd    : string = process::cwd();

    process::chdir("/tmp");                     // throws IOException
    println(pid.toString() + parent.toString() + cwd);
    return 0;
}

OS info

use std::sys;

fn main() : i32 {
    let name : string = sys::osName();            // "Linux" / "Windows" / "macOS"
    let arch : string = sys::osArch();            // "x86_64" / "aarch64"
    let nl   : string = sys::lineSeparator();     // "\n" or "\r\n"
    let sep  : string = sys::fileSeparator();     // "/" or "\\"
    let psep : string = sys::pathSeparator();     // ":" or ";"
    return 0;
}

lineSeparator() is the platform-native newline — use it when writing files for human consumption ; for protocols / config parsers, hardcode "\n" to avoid surprising consumers.

Exit codes

fn sanityCheckFailed() : bool {
    return false;
}

fn main() : i32 {
    if (sanityCheckFailed()) {
        exit(2);                                  // explicit exit code
    }
    return 0;
}

fn main returning a non-zero i32 and exit(code) both work ; the explicit form is needed when you want to bail from deep inside the call stack.

process::abort() (free function in std::sys::process) skips destructors and defer blocks — use only for unrecoverable corruption ; otherwise prefer exit.

Sleeping / timing

fn work() : void {
}

fn main() : i32 {
    sleep(500);                                   // milliseconds — blocks the thread

    let start : i64 = nanoTime();                 // monotonic
    work();
    let elapsedNs : i64 = nanoTime() - start;
    println(elapsedNs.toString());
    return 0;
}

See numeric & time for LocalDateTime and wall-clock formatting.

Running a child process

use std::sys::process::ProcessBuilder;
use std::sys::process::Process;

fn main() : i32 ! IOException {
    let pb : ProcessBuilder = new ProcessBuilder("ls")
        .addArg("-la")
        .addArg("/tmp")
        .inheritIO();                            // forwards child stdio to our stdout/stderr

    let p : Process = pb.start();
    defer p.close();                             // signals + reaps the child

    let exitCode : i32 = p.waitFor();
    return exitCode;
}

ProcessBuilder is fluent — each setter returns the same builder. start() launches and yields a Process handle.

Capturing stdout / stderr

Drop inheritIO() if you want to capture instead :

use std::sys::process::Process;
use std::sys::process::ProcessBuilder;

fn main() : i32 ! IOException {
    let p : Process = new ProcessBuilder("git")
        .addArg("rev-parse")
        .addArg("HEAD")
        .start();
    defer p.close();

    let exitCode : i32 = p.waitFor();
    let sha : string = p.getStdout();
    let err : string = p.getStderr();
    println(sha + err);
    return exitCode;
}

The output is read after waitFor() ; long-running children that print continuously fill an internal buffer — capture is intended for small outputs (a few KB).

Setting env / cwd

use std::sys::process::Process;
use std::sys::process::ProcessBuilder;

fn main() : i32 ! IOException {
    let p : Process = new ProcessBuilder("./run.sh")
        .setDirectory("/var/app")
        .setEnv("CONFIG", "/etc/app.conf")
        .setEnv("DEBUG", "1")
        .start();
    defer p.close();
    return p.waitFor();
}

setDirectory is the child’s cwd ; the parent’s cwd is unaffected. setEnv adds to the inherited environment — to start with a blank env, there is no API (write the wrapper around your specific use case).

Terminating a child

use std::sys::process::Process;
use std::sys::process::ProcessBuilder;

fn main() : i32 ! IOException {
    let p : Process = new ProcessBuilder("longRunning").start();
    defer p.close();

    if (p.isAlive()) {
        p.destroy();                             // forcible kill — SIGKILL on Unix
    }
    return 0;
}

destroy() terminates the child forcibly (SIGKILL on Unix — it gets no chance to clean up). p.close() also kills a still-running child before releasing the handle, so a defer p.close() is enough for the “kill on scope exit” pattern.

Logging

use std::system::log;              // bind the module — call everything through log::
use std::system::log::LogLevel;    // the enum type, needed to name a level

fn main() : i32 {
    log::setLevel(LogLevel::INFO);               // TRACE / DEBUG / INFO / WARN / ERROR

    log::trace("very chatty");                   // suppressed below INFO
    log::debug("variable details");              // suppressed
    log::info("starting up");
    log::warn("config file missing — using defaults");
    log::error("connection failed");

    if (log::isEnabled(LogLevel::DEBUG)) {
        // build an expensive debug message only if debug is on
    }
    return 0;
}

Bind the module once with use std::system::log; and reach every function through log:: — log::info(…), log::setLevel(…), log::isEnabled(…). The one extra use line imports the LogLevel enum type, which you need to spell a level (LogLevel::INFO); the functions themselves are never imported one-by-one. Output goes to stderr (one line per call, with a millisecond timestamp + level prefix). Levels are the LogLevel enum (TRACE=0, DEBUG=10, INFO=20, WARN=30, ERROR=40). The default minimum is INFO ; log::setLevel(level) changes it process-wide and log::getLevel() reads it back. Calls below the threshold are O(1) — the message is not formatted.

CallEffect
log::setLevel(LogLevel::INFO)raise/lower the process-wide threshold
log::getLevel()read the current LogLevel back
log::trace/debug/info/warn/error(msg)emit at that level (dropped below the threshold)
log::isEnabled(LogLevel::DEBUG)true iff a call at that level would emit

Conditional expensive logging

use std::system::log;
use std::system::log::LogLevel;
use std::text::StringBuilder;

fn main() : i32 {
    if (log::isEnabled(LogLevel::DEBUG)) {
        let sb : StringBuilder = new StringBuilder();
        sb.append("dump: ");
        // … expensive serialization …
        log::debug(sb.toString());
    }
    return 0;
}

log::debug(msg) evaluates msg even when debug is disabled — the level gate is inside the helper, so the argument is built before the call decides to drop it. Guard expensive message-building with an explicit log::isEnabled check.

System properties

A small key/value store for runtime config :

use std::sys;

fn main() : i32 {
    if (sys::hasProperty("axle.profile")) {
        let p : string = sys::getProperty("axle.profile");
        // …
    }

    sys::setProperty("my.key", "value");
    return 0;
}

These are process-local and not persisted — closer to env vars in spirit, but separate so user code can store typed defaults without colliding with the OS environment.

Putting it together — small CLI skeleton

use std::sys::process;
use std::sys::cli;
use std::sys::env;
use std::system::log;
use std::system::log::LogLevel;

fn main() : i32 ! IOException, IndexOutOfBoundsException {
    if (cli::argParseFlag("help")) {
        printHelp();
        return 0;
    }

    let level : LogLevel = LogLevel::INFO;
    if (cli::argParseFlag("verbose")) { level = LogLevel::DEBUG; }
    if (cli::argParseFlag("quiet"))   { level = LogLevel::WARN; }
    log::setLevel(level);

    let cfg : string = cli::argHasOption("config")
        ? cli::argParseOption("config")
        : env::configDir() + "/myapp/config.toml";

    log::info("loading config from " + cfg);

    let firstPos : i32 = cli::argFirstPositional();
    if (firstPos < 0) {
        log::error("no positional argument");
        return 1;
    }

    let target : string = process::argAt(firstPos);
    return runCommand(target);
}

fn printHelp() : void {
    // emit a usage string to stdout
}

fn runCommand(name : string) : i32 {
    return 0;
}

See also

clisystemprocessenvironment