Chapter 27 14 min read

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 stringstring 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:

output
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:

shell
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.