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 ; throwsIndexOutOfBoundsExceptionon out-of-range.programName()— same asargAt(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.
| Call | Effect |
|---|---|
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
std/sysreferencestd/sys/cli— flag parsingstd/sys/env— standard dirsstd/sys/process— pid / argv / chdirstd/system/log— logging- Error handling —
IOExceptionpatterns - Files and I/O — file ops to combine with paths
- Concept index — every CLI / system concept cross-linked