CLI
Use cli for interactive console applications and args for argument parsing.
The source module cli.command adds reusable command option builders.
Argument parsing
cli.parseArgs(argv, spec) parses a list of command-line strings according to
a spec dict and returns the parsed values:
import cli;
import sys;
import io;
let opts = cli.parseArgs(sys.args(), {
"name": {"type": "string", "short": "n", "required": true},
"verbose": {"type": "bool", "short": "v"},
"port": {"type": "int", "default": 8080},
"tag": {"type": "string"}
});
io.println(opts["name"]);
if (opts["verbose"] as bool) {
io.println("verbose mode");
}
cli.help(name, spec) returns usage text for the program name, derived from
the spec. The low-level args.parse(argv, spec) and args.help(name, spec)
functions provide the same interface without the interactive features.
Interactive prompts
let name = cli.prompt("Your name: ");
let pass = cli.password("Password: "); # input hidden
let pass2 = cli.secret("Confirm password: "); # same as password
let ok = cli.confirm("Continue? [y/N] "); # returns bool
let lang = cli.choose("Language: ", ["gb", "py", "js"]); # returns chosen string
let langs = cli.multiChoose("Languages:", ["gb", "py", "js"]); # returns list<string>
confirm accepts y/yes and n/no, ignoring case and surrounding spaces.
Invalid answers re-prompt. Blank input uses its second argument, defaulting
to false. choose displays numbered options and retries invalid choices;
its optional third argument is a zero-based default index for blank input.
Both raise IOError at EOF, even if a default is supplied.
Nullable input and typed prompts
cli.readLine(label = "") -> ?string preserves blank input as "" and
returns null at EOF. A final line without a newline is returned once before
EOF. cli.prompt retains its string result, returning "" at EOF.
cli.promptInt(label, options = {}) -> ?int parses and validates a whole
number, re-prompting after invalid text or a rejected value:
let count = cli.promptInt("Tosses (q to stop): ", {
"min": 1, "max": 1000000, "cancel": ["q", "quit", "exit"]
});
if (count != null) { io.println("Simulating ${count} tosses"); }
Entering no, 0, then 10 prints a parsing error, then a minimum error,
and returns 10. EOF or a cancel token returns null. Cancel matching ignores
case and outer spaces. Blank input retries unless a default is supplied;
a default takes precedence over a blank cancel token. Add "" to cancel
to let blank input cancel when there is no default. Integer parsing matches
string.toInt(), including literal prefixes and digit separators.
cli.promptDecimal(label, options = {}) -> ?decimal shares the same retry,
blank/default, cancellation, EOF and validation rules. It parses exact decimal
text; defaults accept decimals or integers (returned as decimals). Floats and
numeric strings are not accepted as defaults.
let amount = cli.promptDecimal("Amount (q to stop): ", {
"min": 0, "max": 1000.00, "cancel": ["q"]
});
if (amount != null) { io.println(amount.format(2)); }
Shared rules work in both typed prompts and each cli.parseArgs option:
| Rule | Meaning |
|---|---|
min, max |
Inclusive finite numeric bounds. |
choices |
List of permitted values, compared by value. |
validate |
Callable receiving the parsed value; null accepts it, a string rejects it with that message. |
Bounds and choices run before the custom validator. Invalid prompt
configuration or an invalid default raises an error. Argument validation
returns a message in the parser's existing error entry:
let parsed = cli.parseArgs(["--count", "0"], {
"count": {"type": "int", "min": 1, "max": 1000000}
});
io.println(parsed["error"]); /* option --count: must be at least 1 */
args.parse supports bounds and choices; custom Geblang callbacks are
supported by cli.parseArgs.
Scoped CLI contexts
cli.withContext(options, body) returns the callback result and restores
the previous context after a return or an exception. Options are stdin
(a string or readable IO stream/file), stdout and stderr (writable
streams/files), quiet (suppress prompts and retry messages), and terminal
(override terminal detection). Omitted options inherit the current context.
let captured = io.memory();
let value = cli.withContext({"stdin": "bad\n12\n", "stdout": captured}, func(): ?int {
return cli.promptInt("Count: ", {"min": 1});
});
io.println(value); /* 12 */
io.println(io.toString(captured)); /* prompts and the parsing error */
CLI and IO readers share buffered input, including io.stdinReadLine,
io.stdinReadAll and io.readLine(io.stdin()). Nested contexts preserve
unread outer input. io.withStdin also shares this input and suppresses
prompts for existing tests. Scope contexts around synchronous work; finish
or await any child tasks before the callback returns. Concurrent code must
not change the same active context.
cli.isTerminal() reports whether stderr is a terminal, respecting the
context's terminal override. Progress goes to stderr so stdout can carry
machine-readable results.
multiChoose selects several options at once. On an interactive terminal it
shows an arrow-key checkbox list (up/down or j/k move, space toggles, a
toggles all, enter confirms, q or ctrl-c cancels). When stdin is not a
terminal (piped input, CI) it falls back to a numbered list and reads
comma-separated choices, so scripts and tests keep working. An optional third
argument pre-checks options by index:
let picked = cli.multiChoose("Pick services:",
["web", "db", "cache", "queue"],
[0, 1]); # web and db checked to start
Text styling
cli.style(text, options) returns an ANSI-styled string. cli.stripAnsi(text)
removes all escape sequences.
io.println(cli.style("Success", {"fg": "green", "bold": true}));
io.println(cli.style("Warning", {"fg": "yellow"}));
io.println(cli.style("Error", {"fg": "red", "bold": true}));
io.println(cli.style("Info", {"fg": "cyan", "italic": true}));
Style options:
| Key | Values |
|---|---|
fg |
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "default" |
bg |
same colour names as fg |
bold |
true |
italic |
true |
underline |
true |
dim |
true |
stripAnsi is useful when writing styled output to a log file or when
detecting that stdout is not a terminal:
let message = cli.style("done", {"fg": "green"});
io.println(cli.stripAnsi(message)); # "done"
Tables
cli.table(rows, options) renders a list of dicts as an aligned text table
and returns the formatted string:
let rows = [
{"name": "Alice", "role": "admin", "active": "yes"},
{"name": "Bob", "role": "viewer", "active": "no"},
{"name": "Carol", "role": "editor", "active": "yes"}
];
io.println(cli.table(rows, {
"columns": ["name", "role", "active"],
"headers": ["Name", "Role", "Active"]
}));
Output:
Name Role Active
---- ------ ------
Alice admin yes
Bob viewer no
Carol editor yes
Options:
| Key | Description |
|---|---|
columns |
list of dict keys to include, in order |
headers |
list of header labels (defaults to the column key names) |
separator |
column separator string (default: two spaces) |
strict |
Raise ValueError for missing keys instead of leaving blank cells (default: false). |
A column can also be a descriptor. key selects a dict field; header
defaults to that key; align is left (default), right or center;
format uses the same format specification as string interpolation:
io.println(cli.table([{"outcome": "Heads", "share": 0.5}], {"columns": [
{"key": "outcome", "header": "Outcome"},
{"key": "share", "header": "Share", "align": "right", "format": ".2%"}
]}));
/* Outcome Share
------- ------
Heads 50.00% */
For class instances, use value instead of key to supply a callable
receiving the row, for example {"header": "Name", "value": func(User row): string { return row.name; }}. Accessors may return any formattable value.
Exceptions from accessors propagate to the caller. String columns and the
legacy bare header list keep their existing behavior.
Columns are sized by displayed width, not byte length: accented and other
multi-byte characters count as one column, East Asian wide characters as
two, and ANSI styling from cli.style or cli.color as none. A styled or
non-ASCII cell therefore lines up with plain ones:
io.println(cli.table([
{"check": "build", "result": cli.style("ok", {"fg": "green"})},
{"check": "café", "result": cli.style("failed", {"fg": "red"})}
], {"columns": ["check", "result"]}));
cli.command - command builders
Import the source module cli.command to build a reusable option schema,
parse arguments and generate help text:
import cli.command as cmd;
import io;
import sys;
let deploy = cmd.newCommand("deploy", "Deploy the application")
.option(cmd.newOption("env", "string").required().help("Target environment"))
.option(cmd.newOption("dry", "bool").help("Dry run, no changes"))
.option(cmd.newOption("tag", "string").help("Image tag to deploy"));
let parsed = deploy.parse(sys.args());
if (parsed["error"] != null) {
io.println(parsed["error"]);
io.println(deploy.help());
sys.exit(1);
}
io.println("deploying to " + parsed["env"]);
Use newOption(name, kind) with:
.required()- fail if the option is absent.help(text)- add help text.default(value)- set a default value.short(char)- single-character alias.min(value),.max(value),.choices(values),.validate(callback)- apply the shared validation rules after parsing
Progress bars and spinners
Managed progress
Prefer widgets.withProgress(total, label, body, options = {}) for scoped
work. It returns the body's result and always clears any rendered bar,
including when the body throws. Off-terminal it emits no progress output.
import cli.widgets as widgets;
widgets.withProgress(100, "Processing", func(widgets.ManagedProgress progress): void {
for (i in 0..<100) {
/* Process one item. */
progress.advance();
}
});
Options: width (default 30, range 1..1000), intervalMs (default 80,
minimum 0), and enabled (default true; false suppresses rendering).
Total must be positive. Updates are throttled using monotonic time;
completion renders immediately. advance(n = 1) and set(value) check
cancellation, clamp the count to 0..total, and update the display.
updateLabel(text) changes the label.
progress.cancel() requests cooperative cancellation. checkCancelled()
or the next advance/set raises widgets.CancelledError; withProgress
also checks before returning a successful result. Catch that class to
handle cancellation. The helper does not register signal handlers: the
application owns its signal handlers and decides when to call cancel().
Cancellation requests use shared storage; rendering and count updates
belong to the task running the progress body.
Import the source module cli.widgets for two terminal progress
indicators. Both draw to stderr, so they don't interfere with piped
stdout.
Spinner
widgets.Spinner(message) shows a rotating frame next to a message
while a task runs. The caller owns the cadence: call .tick() to
advance one frame, .update(message) to change the label, and
.stop(finalMessage?) to clear the line.
import cli.widgets as widgets;
let sp = widgets.Spinner("connecting");
for (let int i = 0; i < 20; i++) {
sp.tick();
# ... do a slice of work ...
}
sp.update("fetching data");
sp.tick();
sp.stop("connected");
| Method | Description |
|---|---|
Spinner(message) |
Create a spinner with an initial message. |
tick() |
Draw the next frame. |
update(message) |
Change the label shown after the spinner. |
stop(finalMessage = "") |
Clear the line; optionally print a final message. |
ProgressBar
widgets.ProgressBar(total, width = 30, label = "") renders a bar
like [#####-----] 50% (5/10) label. Use .advance(n = 1) for the
common increment, .set(value) for an explicit position,
.updateLabel(label) to change the trailing text, and .finish()
when done.
import cli.widgets as widgets;
let bar = widgets.ProgressBar(10, 20, "downloading");
for (let int i = 0; i < 10; i++) {
bar.advance();
# ... download one chunk ...
}
bar.finish("done");
| Method | Description |
|---|---|
ProgressBar(total, width = 30, label = "") |
Create a bar of width columns over total units. |
advance(n = 1) |
Add n to the current count and redraw. |
set(value) |
Set the current count to value and redraw. |
updateLabel(label) |
Change the trailing label and redraw. |
finish(finalMessage = "") |
Clear the line; optionally print a final message. |
cli.color - ANSI terminal styling
import cli.color as color;
import io;
io.println(color.bold(color.red("error: ") + "build failed"));
io.println(color.dim("hint: run with --verbose for details"));
Each helper wraps its argument with the matching ANSI escape sequence
and a reset. The helpers honour the NO_COLOR
convention: when NO_COLOR is set in the environment to any non-empty
value, every wrapper returns its input unchanged. This makes the same
code work on plain terminals, in CI logs, and in interactive shells
without an isTTY check.
Styles: bold, dim, italic, underline.
Foreground colors: black, red, green, yellow, blue, magenta,
cyan, white.
Background colors: bgBlack, bgRed, bgGreen, bgYellow, bgBlue,
bgMagenta, bgCyan, bgWhite.
| Function | Returns | Description |
|---|---|---|
color.isEnabled() |
bool |
true when NO_COLOR is unset or empty - i.e., when wrappers actually emit escape codes. |
To toggle color off at runtime, set the env var explicitly:
import sys;
sys.setenv("NO_COLOR", "1");