cli
Declarative argv parser for the <action> <options> shape.
Built around one boundary rule: the tool's options come before the first
positional argument; everything after it belongs to whatever the action hands
it to. The option zone closes on the first of two events: a positional
argument, or a bare -- (consumed, not passed on). Past the close nothing is
interpreted, so an action that forwards its tail (a program runner) hands it
over verbatim, flags and all, with no escaping needed.
That single rule replaces the earlier layered scheme, where a permissive pass pulled out the flags it knew and let the rest fall through. The old shape made the boundary depend on flag recognition rather than position, which meant a forwarding action could never be strict: an unrecognised token was indistinguishable from one meant for the program.
Flag forms recognised, all equivalent per kind:
--name/-n: a bool flag, setsname → "true".--name=v/-n=v: a string flag, its value inline.--name v/-n v: a string flag, its value spaced.--: closes the option zone.- anything else: closes the option zone, and is the first positional.
Combined short flags (-abc for -a -b -c) are deliberately unsupported:
they only apply to bool flags, of which a CLI this size has a handful, and
they make the scan materially harder to read.
Types
FlagKind :: enum(u8) Kind of a flag: controls whether a value is required and how it is checked.
Str is the default because it is what most flags are (11 of the compiler's
19). Bool needs no value: presence alone sets "true". Int is a Str
whose value must parse as an integer, so a typo'd number is caught at the
boundary instead of being silently clamped by the caller.
Variants
Bool Str Int ValueSpec :: struct One accepted value of an enumerated flag, with the line documenting it.
A flag whose values is non-empty accepts nothing else: the parser rejects
an unlisted value on its own and names the accepted set, so the validation,
the error message and the help all read from this one array.
Fields
name: string help: string One-line description: the help's right-hand column under the flag.
FlagSpec :: struct Specification of a single recognised flag. Pass an array of these to parse.
Everything a help renderer or an error message needs lives here, so the spec table is the only place a flag is described. A field left empty is a spec defect, not a feature: there are no undocumented flags.
Fields
name: string Canonical identifier, without the leading --.
help: string One-line description, imperative mood. Required: it is what the help prints, and a flag nobody described is a spec defect, not a hidden flag.
alias: string = "" Short alias without the dash, e.g. "o". Empty when the flag has none.
An alias is a name and nothing else: the kind above is what decides
whether a value is required, identically for both spellings.
value_name: string = "" Placeholder for a Str flag's value: "path", "N", "a,b". Renders
as --out=<path>, and appears in the missing-value error. Ignored for
Bool.
default_text: string = "" Shown as (default: cc) when non-empty. Documentation only: the caller
still supplies the fallback to string_flag.
kind: FlagKind = .Str Defaults to Str, the common case. A Bool must say so: declaring one
by accident makes it demand a value and swallow the next token, which the
value_name check in vader/tools/cli/spec.vader's tests is there to catch.
values: ValueSpec[] = [] Closed value set. Empty means any string is accepted. Meaningless on
Bool, and on Int the range check is the caller's business.
TailKind :: enum(u8) What an action does with positionals past the first: drives its usage line and the error it raises on an unexpected one.
Variants
None Takes one positional; a second is a mistake.
Paths Every positional is a path to operate on.
Forward The tail is another program's argv, handed over verbatim.
CommandSpec :: struct One action of the CLI: vader <name> <options>.
ParsedArgs :: struct Parser output. flags carries every recognised flag's literal string value
(bool flags resolve to "true"). positionals holds the tokens from the
zone close onward, in order and uninspected. errors lists unknown flags,
missing values and rejected enumerated values: non-fatal here; the caller
decides whether to bail out.
Fields
flags: MutableMap<string, string> positionals: string[] errors: string[] Key :: enum(u8) A key the prompts understand. Anything else decodes to .Unknown and is
ignored rather than guessed at.
Variants
Up Down Left Right Home End Enter Space Backspace Tab Escape Interrupt Ctrl-C. Raw mode turns OFF signal generation, so it arrives as a byte
(0x03) instead of killing the process, which is what lets a prompt
restore the terminal before giving up.
Char A printable character; the text is in KeyPress.text.
Unknown KeyPress :: struct One decoded keypress.
Fields
key: Key text: string The character when key == .Char, else "". A whole codepoint, so a
multi-byte character arrives as one press rather than as its bytes.
DecodedKey :: struct A keypress plus how many bytes it took, so the caller can decode the rest of the burst.
Fields
press: KeyPress consumed: i64 Always >= 1, so a decode loop cannot spin.
PromptFailure :: enum(u8) Why a prompt produced no answer.
Variants
NoTerminal stdin is not an interactive terminal. The caller must fall back to a flag or a default: retrying will not help.
Cancelled The user pressed Escape or Ctrl-C.
InputClosed Input ended before an answer (the terminal closed, or stdin ran out).
PromptError :: struct A prompt that produced no answer, and why.
Fields
reason: PromptFailure message: string One line, ready to print. It does NOT name a flag to use instead: the library does not know the command's options; the caller adds that.
Choice :: struct One option of a select or a multiselect.
Fields
label: string What the option is called, and what the folded step shows once chosen.
hint: string = "" A few words beside the label, in grey; none when empty.
Functions
parse :: fn(args: string[], specs: FlagSpec[]) -> ParsedArgs Parse args against specs, stopping at the first positional or --.
One pass, no state kept between calls. Unknown flags inside the option zone are errors, not positionals: that is the whole point of having a zone, and it is what lets a mistyped flag be caught instead of silently becoming a filename.
Parameters
| args | string[] | tokens to parse: for a CLI, everything after the action name. |
| specs | FlagSpec[] | the flags this action accepts. |
Returns
ParsedArgs flags read from the zone, plus every token past its close as positionals (the first being the action's own argument, the rest being whatever its TailKind says).Errors
- one entry in
errorsper unknown flag, perStrflag with no value, perBoolflag given something other thantrue/false, and per value outside a flag'svaluesset.
bool_flag :: fn(self: ParsedArgs, name: string) -> bool True iff name was seen as a bool flag (or set explicitly to "true").
Missing flag → false, no error.
Parameters
| self | ParsedArgs | parser output. |
| name | string | canonical flag name (not an alias: the parser normalises). |
Returns
bool string_flag :: fn(self: ParsedArgs, name: string, default: string) -> string Read a string flag, falling back to default when absent.
Parameters
| self | ParsedArgs | parser output. |
| name | string | canonical flag name (not an alias). |
| default | string | value returned when name wasn't on the command line. |
Returns
string int_flag :: fn(self: ParsedArgs, name: string, default: i32) -> i32 Read an Int flag, falling back to default when absent.
The value is already known to parse (parse rejected it otherwise) so this
cannot fail. It still falls back on an unparseable value rather than trapping,
for the case a caller reads an Int flag from a ParsedArgs it never checked
errors on. Range limits stay the caller's: --parallel wants ≥ 1, which is
its business, not the parser's.
Parameters
| self | ParsedArgs | parser output. |
| name | string | canonical flag name (not an alias). |
| default | i32 | value returned when name is absent or somehow unparseable. |
Returns
i32 render_help :: fn(program: string, version: string, tagline: string, commands: CommandSpec[], common: FlagSpec[]) -> string Render the overview: usage line, the action list, the common options.
Deliberately does NOT list every action's own flags: that is what
render_command_help is for. A single flat help is how the hand-written one
reached sixty lines.
Parameters
| program | string | the binary's name, e.g. "vader". |
| version | string | version string, shown on the title line. |
| tagline | string | one-line description of the program. |
| commands | CommandSpec[] | every action, in the order to list them. |
| common | FlagSpec[] | flags accepted by every action. |
Returns
string the full help text, newline-terminated.render_command_help :: fn(program: string, cmd: CommandSpec, common: FlagSpec[]) -> string Render one action in full: its usage line, then every flag it accepts.
This is the view a hand-written help can't give: vader build --help had no
answer at all before, which is most of why the overview grew to carry every
flag of every action.
Parameters
| program | string | the binary's name. |
| cmd | CommandSpec | the action to describe. |
| common | FlagSpec[] | flags accepted by every action, listed after the action's own. |
Returns
string the action's help text, newline-terminated.render_program_help :: fn(program: string, usage: string, summary: string, flags: FlagSpec[]) -> string Render the help of a program that takes no action: its usage line, then every flag it accepts.
Parameters
| program | string | what the usage line opens with: the binary's name, or the binary's and an action's ("vader build"). |
| usage | string | what follows on the usage line, e.g. "[options] <file>" ; empty when nothing does. |
| summary | string | one-line description of the program. |
| flags | FlagSpec[] | every flag the program accepts. |
Returns
string the help text, newline-terminated.decode_key :: fn(burst: string, from: i64) -> DecodedKey Decode the first keypress in burst, starting at byte from.
Parameters
| burst | string | the bytes as read from the terminal. |
| from | i64 | byte offset to decode at; must be < burst.byte_len(). |
Returns
DecodedKey the press and the byte count it took, never 0, so a caller looping over a burst always advances.intro :: fn(title: string) -> void Open a session of prompts under title: the top of the rail every later step
hangs from. Close it with outro or cancel.
Parameters
| title | string | what the session is about, shown in bold at its top. |
Example
intro("vader new")
name :: text("Project name?", "demo")
if name is PromptError {
cancel("no project was made")
} else {
outro("created ${name}")
}note :: fn(title: string, lines: string[]) -> void Put an answered step of title on the rail, with lines under it: what a
command did once the prompts were answered.
Parameters
| title | string | the step, as a folded question would read. |
| lines | string[] | shown under it, one per rail line. |
outro :: fn(message: string) -> void Close the session on message: what to do next, say.
Parameters
| message | string | the last line, beside the rail's end. |
cancel :: fn(message: string) -> void Close the session on why nothing was done, on the error output.
Parameters
| message | string | the reason, in red beside the rail's end. |
fit :: fn(s: string, width: i32, cut: string = "...") -> string Cut s to width columns, marking the cut so a truncated option does not
read as a complete one.
Parameters
| s | string | the line to fit. |
| width | i32 | columns available; when the mark alone does not fit, s comes back unchanged, since there is no room to mark anything. |
| cut | string = "..." | the mark, which takes the place of the last columns kept. |
Returns
string s, or as much of it as fits before cut.select :: fn(question: string, choices: Choice[]) -> i64 | PromptError Ask the user to pick one of choices; the first is chosen until the user
moves.
Parameters
| question | string | shown above the list. |
| choices | Choice[] | at least one; an empty list is a caller mistake and returns .NoTerminal rather than drawing an empty frame. |
Returns
i64 | PromptError the chosen INDEX into choices.Errors
.NoTerminalwhen stdin is not interactive: name the flag to pass instead;.Cancelledon Escape or Ctrl-C;.InputClosedwhen input ends first.
multiselect :: fn(question: string, choices: Choice[]) -> i64[] | PromptError Ask the user to check any number of choices.
Parameters
| question | string | No description. |
| choices | Choice[] | No description. |
Returns
i64[] | PromptError the indices checked, in order; empty when none were.Errors
- as
select.
checked_indices :: fn(checked: bool[]) -> i64[] The indices set in checked, in order.
Parameters
| checked | bool[] | No description. |
Returns
i64[] confirm :: fn(question: string, default_yes: bool) -> bool | PromptError Ask a yes/no question. Enter takes default_yes until the user switches it.
Parameters
| question | string | No description. |
| default_yes | bool | No description. |
Returns
bool | PromptError Errors
.NoTerminalwhen stdin is not interactive: the caller should use its flag's own default;.Cancelledon Escape or Ctrl-C.
confirm_answer :: fn(press: KeyPress, current: bool) -> bool | null How one press answers a yes/no, or null when it decides nothing. Pure, so the accepted spellings are pinned by a test rather than by trying them.
Parameters
| press | KeyPress | No description. |
| current | bool | the answer Enter takes. |
Returns
bool | null text :: fn(question: string, default_value: string, check: fn(string) -> string | null = any_answer) -> string | PromptError Ask for one line of text.
Supports typing, Backspace, and Enter. There is no cursor movement inside the line: a prompt is not an editor, and the keys that would be needed for it (Left/Right plus a render that positions the cursor) are what would turn this into one.
Parameters
| question | string | No description. |
| default_value | string | returned when the user presses Enter on an empty line; shown in grey while the line is empty. |
| check | fn(string) -> string | null = any_answer | why an answer is refused, null when it is taken. A refused answer stays to be corrected, the reason under it until the line changes. |
Returns
string | PromptError Errors
- as
confirm.
drop_last :: fn(s: string) -> string Drop the last CHARACTER of s, not its last byte, so backspacing over a
multi-byte character removes the whole thing.
Parameters
| s | string | No description. |
Returns
string