CLI: argparse & spinner
Two small modules for building command-line tools. stdlib::argparse is a heap-allocated argument parser supporting long/short flags of three value types (string, int, bool), positional capture, a -- end-of-flags marker, and auto-generated --help text. stdlib::spinner is a minimalist braille progress spinner that animates on stderr and degrades gracefully on non-TTYs.
Import
import stdlib::argparse::*; // ArgParser, StrFlag, IntFlag, BoolFlag
import stdlib::spinner::*; // spinner_start / spinner_set / spinner_finish
Public surface at a glance
| Module | Public item | Kind |
|---|---|---|
argparse |
StrFlag, IntFlag, BoolFlag |
structs (boxed flag values) |
argparse |
ArgParser |
struct |
argparse |
new, free |
ArgParser lifecycle |
argparse |
add_string, add_int, add_bool |
declare flags |
argparse |
parse_argv, parse_tokens |
parse (-> !i32) |
argparse |
get_string, get_int, get_bool, positional |
read results |
argparse |
help_requested, format_help, print_help |
help |
spinner |
spinner_start, spinner_set, spinner_finish |
free functions |
argparse — overview
Build a parser with ArgParser::new, declare each flag with add_string / add_int / add_bool, then call parse_argv() (or parse_tokens() for a supplied list). After a successful parse, read values with get_string / get_int / get_bool and positional arguments with positional().
import stdlib::argparse::*;
import stdlib::io::*;
fn main() -> !i32 {
let p: *ArgParser = ArgParser::new("greet", "Say hello to someone");
defer p.free();
p.add_string("name", "n", "world", "who to greet");
p.add_int("count", "c", 1, "number of greetings");
p.add_bool("verbose", "v", false, "extra output");
let r: !i32 = p.parse_argv();
if !r.ok { eprintln("error: ".concat(r.err)); return ok(2); }
if p.help_requested() { p.print_help(); return ok(0); }
let who: string = p.get_string("name");
let n: i32 = p.get_int("count");
for let i: i32 = 0; i < n; i++ { println!("hello,", who); }
return ok(0);
}
Recognised token forms
| Token | Meaning |
|---|---|
--name=value |
long flag with inline value |
--name value |
long flag, value is the next token |
-n value |
short flag, value is the next token |
--name / -n |
bare bool flag (flips it from its default) |
--help / -h |
sets help_requested(); not printed automatically |
-- |
end-of-flags marker; everything after is positional |
| anything else | positional argument (incl. -3 style negatives) |
Flag value structs
The three boxed value holders are public. You rarely construct these directly — add_* allocates and stores them — but they are visible for inspection.
| Struct | Fields | Used by |
|---|---|---|
StrFlag |
pub default_value: string, pub value: string |
add_string / get_string |
IntFlag |
pub default_value: i32, pub value: i32 |
add_int / get_int |
BoolFlag |
pub default_value: bool, pub value: bool |
add_bool / get_bool |
pub struct StrFlag { pub default_value: string, pub value: string }
pub struct IntFlag { pub default_value: i32, pub value: i32 }
pub struct BoolFlag { pub default_value: bool, pub value: bool }
// The flag value structs are public and can be built directly.
let f: IntFlag = IntFlag{ default_value: 5, value: 5 };
let sf: StrFlag = StrFlag{ default_value: "x", value: "x" };
let bf: BoolFlag = BoolFlag{ default_value: false, value: true };
ArgParser
pub struct ArgParser { /* prog, desc, flag maps, order, positional ... (private) */ }
All fields are private; interact through the methods below. Internally it holds three HashMaps of boxed flag structs (one per type), several string→string maps for short/long aliasing, type tags and help text, a Vector<string> of flag names in declaration order, and the positional Vector<string>.
Construction
ArgParser::new
pub fn new(prog: string, desc: string) -> *ArgParser
Allocates an empty parser on the heap. prog and desc appear in --help output. The returned pointer owns several internal hash maps and vectors; free it with free() (pair with defer).
let p: *ArgParser = ArgParser::new("greet", "Say hello");
defer p.free();
free
pub fn free(self: *ArgParser)
Frees every owned flag struct, hash map, and vector, then the parser itself. The pointer dangles afterward — only call once, ideally via defer.
Declaring flags
| Method | Signature |
|---|---|
add_string |
pub fn add_string(self: *ArgParser, long: string, short: string, default_value: string, help: string) |
add_int |
pub fn add_int(self: *ArgParser, long: string, short: string, default_value: i32, help: string) |
add_bool |
pub fn add_bool(self: *ArgParser, long: string, short: string, default_value: bool, help: string) |
Each declares one flag. long is the --long name; short is the single-char -s alias, or "" for none. default_value seeds the value before parsing, and help is the per-flag description in --help.
p.add_string("name", "n", "world", "who to greet"); // --name=alice --name alice -n alice
p.add_int("count", "c", 1, "number of greetings"); // --count=5 --count 5 -c 5
p.add_bool("verbose", "v", false, "extra output"); // --verbose / -v sets it true
Parsing
parse_argv
pub fn parse_argv(self: *ArgParser) -> !i32
Parses the process's argv starting at index 1 (index 0, the program path, is skipped). Returns ok(0) on success or err(message) on the first malformed token (unknown flag, missing value, bad int/bool). Read the message via .err.
let r: !i32 = p.parse_argv();
if !r.ok { eprintln("error: ".concat(r.err)); return 2; }
parse_tokens
pub fn parse_tokens(self: *ArgParser, tokens: *Vector<string>) -> !i32
Same parsing logic on a caller-supplied token list — handy for tests and for building subcommands by hand. parse_argv is implemented in terms of this.
let toks: *Vector<string> = Vector::new();
toks.push_all!("--name=alice", "--count", "3", "-v", "file.txt");
p.parse_tokens(toks)?;
Error messages you can match against (all surface via .err):
| Condition | Message |
|---|---|
unknown --long flag |
unknown flag: --<name> |
--int-flag with no following value |
flag --<name> requires a value |
-i short with no following value |
flag -<short> requires a value |
| non-integer for an int flag | flag --<name> expects an integer, got: <val> |
non-bool for a bool flag (via =) |
flag --<name> expects a bool, got: <val> |
Reading values
| Method | Signature | Returns |
|---|---|---|
get_string |
pub fn get_string(self: *ArgParser, long: string) -> string |
parsed string flag |
get_int |
pub fn get_int(self: *ArgParser, long: string) -> i32 |
parsed int flag |
get_bool |
pub fn get_bool(self: *ArgParser, long: string) -> bool |
parsed bool flag |
positional |
pub fn positional(self: *ArgParser) -> *Vector<string> |
positional args, in order |
let who: string = p.get_string("name");
let n: i32 = p.get_int("count");
if p.get_bool("verbose") { eprintln("verbose mode on"); }
let files: *Vector<string> = p.positional();
for let i: i32 = 0; i < files.len(); i++ { println!("file:", files.get(i)); }
Positionals, --, and negative numbers
positional() collects, in order: any non-flag token, any unknown -x short token (so -3 survives), and everything after a -- separator (even if it looks like a flag).
import stdlib::argparse::*;
import stdlib::io::*;
fn main() -> i32 {
let p: *ArgParser = ArgParser::new("calc", "");
defer p.free();
p.add_bool("verbose", "v", false, "noisy");
p.add_int("scale", "s", 1, "scale factor");
let toks: *Vector<string> = Vector::new();
// -3 is not a known short flag, so it survives as a positional.
// Everything after `--` is positional even if it looks like a flag.
toks.push_all!("-v", "--scale=2", "-3", "--", "--scale", "literal");
let r: !i32 = p.parse_tokens(toks);
if !r.ok { eprintln(r.err); return 2; }
println!("verbose:", format!("{}", p.get_bool("verbose"))); // true
println!("scale:", format!("{}", p.get_int("scale"))); // 2
let pos: *Vector<string> = p.positional();
for let i: i32 = 0; i < pos.len(); i++ {
println!("positional:", pos.get(i)); // -3, --scale, literal
}
return 0;
}
Help output
| Method | Signature |
|---|---|
help_requested |
pub fn help_requested(self: *ArgParser) -> bool |
format_help |
pub fn format_help(self: *ArgParser) -> string |
print_help |
pub fn print_help(self: *ArgParser) |
help_requested() reports whether --help / -h was seen during parsing — the parser never prints help on its own, so the caller decides what to do. format_help() builds the usage string (program name, description, one line per flag in declaration order, with type + default + help); print_help() prints it to stdout via print!.
if p.help_requested() { p.print_help(); return ok(0); }
let txt: string = p.format_help(); // capture instead of printing
format_help produces output shaped like:
usage: greet [flags] [positional...]
Say hello to someone
flags:
--name (-n) [string, default="world"]
who to greet
--count (-c) [int, default=1]
number of greetings
--verbose (-v) [bool, default=false]
extra output
--help (-h) show this message
An empty desc (passed as "") omits the description block; a flag declared with an empty help omits its second (indented) line.
Worked example: a real CLI with a spinner
A complete build-style tool: several flags, --help handling, error surfacing, then real work wrapped in a spinner.
import stdlib::argparse::*;
import stdlib::spinner::*;
import stdlib::io::*;
fn main() -> !i32 {
let p: *ArgParser = ArgParser::new("build", "Compile and link a target");
defer p.free();
p.add_string("target", "t", "app", "target name to build");
p.add_int("jobs", "j", 1, "number of parallel jobs");
p.add_bool("release", "r", false, "optimized release build");
p.add_bool("color", "", true, "colorized output (--color turns it off)");
let r: !i32 = p.parse_argv();
if !r.ok { eprintln("error: ".concat(r.err)); return ok(2); }
if p.help_requested() { p.print_help(); return ok(0); }
let target: string = p.get_string("target");
let jobs: i32 = p.get_int("jobs");
let release: bool = p.get_bool("release");
let mode: string = "debug";
if release { mode = "release"; }
eprintln(format!("building {} ({}) with {} jobs", target, mode, jobs));
// Spinner around the work. On a TTY this animates; piped, it degrades
// to plain status lines.
spinner_start("compiling ".concat(target));
// ... long-running work would happen here ...
spinner_set("linking ".concat(target));
// ... more work ...
spinner_finish(true, "built ".concat(target));
// Trailing positionals (after flags, or after a `--`).
let extra: *Vector<string> = p.positional();
for let i: i32 = 0; i < extra.len(); i++ {
println!("extra:", extra.get(i));
}
return ok(0);
}
Invoke it like:
build -t mylib -j 4 --release -- a.o b.o
Pattern: subcommands on top of parse_tokens
There is no built-in subcommand type. Build one by peeling the first positional token and dispatching to a per-command parser fed via parse_tokens.
import stdlib::argparse::*;
import stdlib::io::*;
fn run_add(rest: *Vector<string>) -> !i32 {
let p: *ArgParser = ArgParser::new("git add", "stage files");
defer p.free();
p.add_bool("all", "a", false, "stage everything");
p.parse_tokens(rest)?;
println!("add all =", format!("{}", p.get_bool("all")));
let files: *Vector<string> = p.positional();
for let i: i32 = 0; i < files.len(); i++ { println!("stage:", files.get(i)); }
return ok(0);
}
fn main() -> !i32 {
let argv: *Vector<string> = Vector::new();
argv.push_all!("add", "-a", "main.glide", "lib.glide"); // would be real argv
if argv.len() == 0 { eprintln("missing subcommand"); return ok(2); }
let cmd: string = argv.get(0);
let rest: *Vector<string> = Vector::new();
for let i: i32 = 1; i < argv.len(); i++ { rest.push(argv.get(i)); }
if cmd.eq("add") { run_add(rest)?; return ok(0); }
eprintln("unknown subcommand: ".concat(cmd));
return ok(2);
}
spinner
A single global spinner. A background pthread redraws a braille frame every 80 ms on stderr. Starting a new spinner while one is live silently replaces it (it joins the old one first). When stderr is not a TTY (piped output, CI without a PTY) the animation is suppressed: spinner_start / spinner_set print their message as a plain line, and spinner_finish still emits its final line, keeping logs clean.
| Function | Signature | Description |
|---|---|---|
spinner_start |
pub fn spinner_start(msg: string) |
Start (or replace) the spinner with msg as the initial status. |
spinner_set |
pub fn spinner_set(msg: string) |
Update the live spinner's status text. No-op if none is active (emits a fresh line on non-TTY). |
spinner_finish |
pub fn spinner_finish(ok: bool, msg: string) |
Stop and join the animator, clear the line, then print ✓ msg (when ok) or ✗ msg. Empty msg suppresses the final line. |
import stdlib::spinner::*;
fn main() -> i32 {
spinner_start("compiling...");
// ... long-running work ...
spinner_set("linking...");
// ... more work ...
spinner_finish(true, "built target/my_app");
spinner_start("deploying...");
spinner_finish(false, "deploy failed"); // prints ✗ deploy failed
spinner_start("warming up");
spinner_finish(true, ""); // no final line
return 0;
}
Frames cycle through the ten braille glyphs ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏. On finish the line is erased (\r\x1b[K) before the ✓/✗ summary is printed.