Library

cli

Declarative argv parser for the <action> <options> shape.

4 files lib/cli/cli.vader, lib/cli/help.vader, lib/cli/keys.vader, lib/cli/prompt.vader 34 exports

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, sets name → "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
No description.
Str
No description.
Int
No description.
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
No description.
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>.

Fields

name: string
No description.
summary: string

One-line summary for the action list.

usage: string

Usage tail after the action name, e.g. "<file>" / "[path]".

flags: FlagSpec[]
No description.
tail: TailKind
No description.
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>
No description.
positionals: string[]
No description.
errors: string[]
No description.
Key :: enum(u8)

A key the prompts understand. Anything else decodes to .Unknown and is ignored rather than guessed at.

Variants

Up
No description.
Down
No description.
Left
No description.
Right
No description.
Home
No description.
End
No description.
Enter
No description.
Space
No description.
Backspace
No description.
Tab
No description.
Escape
No description.
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
No description.
KeyPress :: struct

One decoded keypress.

Fields

key: Key
No description.
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
No description.
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
No description.
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

argsstring[] tokens to parse: for a CLI, everything after the action name.
specsFlagSpec[] 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 errors per unknown flag, per Str flag with no value, per Bool flag given something other than true / false, and per value outside a flag's values set.
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

selfParsedArgs parser output.
namestring 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

selfParsedArgs parser output.
namestring canonical flag name (not an alias).
defaultstring 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

selfParsedArgs parser output.
namestring canonical flag name (not an alias).
defaulti32 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

programstring the binary's name, e.g. "vader".
versionstring version string, shown on the title line.
taglinestring one-line description of the program.
commandsCommandSpec[] every action, in the order to list them.
commonFlagSpec[] 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

programstring the binary's name.
cmdCommandSpec the action to describe.
commonFlagSpec[] 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

programstring what the usage line opens with: the binary's name, or the binary's and an action's ("vader build").
usagestring what follows on the usage line, e.g. "[options] <file>" ; empty when nothing does.
summarystring one-line description of the program.
flagsFlagSpec[] 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

burststring the bytes as read from the terminal.
fromi64 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.
decode_burst :: fn(burst: string) -> KeyPress[]

Decode a whole burst into the presses it holds.

Parameters

burststring bytes as returned by std/tty::read_keys.

Returns

KeyPress[] every press in order; empty when burst is empty.
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

titlestring 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

titlestring the step, as a folded question would read.
linesstring[] shown under it, one per rail line.
outro :: fn(message: string) -> void

Close the session on message: what to do next, say.

Parameters

messagestring 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

messagestring 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

sstring the line to fit.
widthi32 columns available; when the mark alone does not fit, s comes back unchanged, since there is no room to mark anything.
cutstring = "..."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

questionstring shown above the list.
choicesChoice[] 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

  • .NoTerminal when stdin is not interactive: name the flag to pass instead; .Cancelled on Escape or Ctrl-C; .InputClosed when input ends first.
moved :: fn(cursor: i64, count: i64, key: Key) -> i64

No vaderdoc yet. The parameters and the return come from the signature.

cursori64
counti64
keyKey
Returns i64
multiselect :: fn(question: string, choices: Choice[]) -> i64[] | PromptError

Ask the user to check any number of choices.

Parameters

questionstring No description.
choicesChoice[] 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

checkedbool[] 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

questionstring No description.
default_yesbool No description.

Returns

bool | PromptError

Errors

  • .NoTerminal when stdin is not interactive: the caller should use its flag's own default; .Cancelled on 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

pressKeyPress No description.
currentbool 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

questionstring No description.
default_valuestring returned when the user presses Enter on an empty line; shown in grey while the line is empty.
checkfn(string) -> string | null = any_answerwhy 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

sstring No description.

Returns

string
Written by vader doc. Icons: Codicons, CC BY 4.0. Fonts: Atkinson Hyperlegible, JetBrains Mono, OFL.