Skip to content

std.cli

std.cli is the standard command-line parser for Janus programs.

It provides stable E42xx error codes, token helpers, and a zero-allocation parser for long flags, short flags, short groups, values, positionals, and the -- terminator.

The v1 parser uses fixed-capacity builders:

use std.cli
var specs = cli.initSpecSet()
_ = cli.addSpec(&specs, cli.boolFlag(str_slice("verbose", 0, 7), str_slice("v", 0, 1)))
_ = cli.addSpec(&specs, cli.defaultValue(
cli.valueName(cli.valueFlag(str_slice("output", 0, 6), str_slice("o", 0, 1)), str_slice("PATH", 0, 4)),
str_slice("stdout", 0, 6)
))
_ = cli.addSpec(&specs, cli.valueName(
cli.repeatableValueFlag(str_slice("include", 0, 7), str_slice("I", 0, 1)),
str_slice("PATH", 0, 4)
))
_ = cli.addSpec(&specs, cli.defaultValue(
cli.valueName(cli.valueFlag(str_slice("jobs", 0, 4), str_slice("j", 0, 1)), str_slice("N", 0, 1)),
str_slice("4", 0, 1)
))
_ = cli.addSpec(&specs, cli.defaultValue(
cli.valueName(cli.valueFlag(str_slice("ratio", 0, 5), str_slice("r", 0, 1)), str_slice("N", 0, 1)),
str_slice("0.5", 0, 3)
))
_ = cli.addSpec(&specs, cli.defaultValue(
cli.valueName(cli.valueFlag(str_slice("format", 0, 6), str_slice("f", 0, 1)), str_slice("KIND", 0, 4)),
str_slice("text", 0, 4)
))
_ = cli.addSpec(&specs, cli.requireFlag(cli.valueFlag(str_slice("config", 0, 6), str_slice("c", 0, 1))))
var argv = cli.initArgList()
_ = cli.addArg(&argv, str_slice("--verbose", 0, 9))
_ = cli.addArg(&argv, str_slice("--output=file.txt", 0, 17))
let parsed = cli.parse(&argv, &specs, 0, 0)
let help = cli.buildHelp(
str_slice("demo", 0, 4),
str_slice("demo command", 0, 12),
str_slice("[input]", 0, 7),
&specs
)

This avoids heap allocation and avoids the compiler’s current nested slice-call gap. A future adapter can accept raw [][]const u8 argv once that ABI is fully closed.

  • --verbose
  • --color=false
  • --no-color
  • --output=file.txt
  • --output file.txt
  • --jobs -2
  • --ratio=0.75
  • -v
  • -abc
  • -ofile.txt
  • -- followed by positional arguments

ParseResult returns:

  • ok
  • args
  • err_code
  • err_message
  • err_flag

Use hasFlag(&parsed.args, name) and valueOf(&parsed.args, name) to query parsed flags.

Use valueOr(&parsed.args, name, fallback) when a caller should see a default even if the flag was not provided. Parser-level defaults are injected into the parsed flag list before success is returned.

Use positionalAt(&parsed.args, index) and positionalOr(&parsed.args, index, fallback) for operands:

let input = cli.positionalAt(&parsed.args, 0)
let output = cli.positionalOr(&parsed.args, 1, str_slice("stdout", 0, 6))

This keeps positional reads on the same lookup surface as flags and avoids direct indexing at call sites.

Use repeatableValueFlag(long, short) for options that may appear more than once, such as include paths or definitions. Read them with countOf and valueAt:

_ = cli.addSpec(&specs, cli.repeatableValueFlag(str_slice("include", 0, 7), str_slice("I", 0, 1)))
let include_count = cli.countOf(&parsed.args, str_slice("include", 0, 7))
let first_include = cli.valueAt(&parsed.args, str_slice("include", 0, 7), 0)
let last_include = cli.lastValueOf(&parsed.args, str_slice("include", 0, 7))

This supports -Istd, --include=lib, and --include vendor without heap allocation. valueOf returns the first match; lastValueOf returns the last match for last-wins command semantics.

Use intValue(&parsed.args, name) when a value flag should be read as an integer. It returns IntLookup:

  • found tells you whether the flag was present after defaults were applied
  • ok tells you whether parsing succeeded
  • value contains the parsed i64 when ok is true
  • err_code is E_INVALID_VALUE when the string is not a decimal integer

Use intValueInRange(&parsed.args, name, min, max) for numeric options with a valid operating range. Out-of-range values return E_VALUE_OUT_OF_RANGE. Negative integers may be passed in attached or spaced form, for example --jobs=-2 or --jobs -2; the parser still treats nonnumeric flag-looking tokens such as -v as a missing value boundary.

let jobs = cli.intValueInRange(&parsed.args, str_slice("jobs", 0, 4), 1, 16)
if jobs.found and jobs.ok do
run_with_jobs(jobs.value)
end

Use floatValue(&parsed.args, name) and floatValueInRange(&parsed.args, name, min, max) for decimal f64 options:

let ratio = cli.floatValueInRange(&parsed.args, str_slice("ratio", 0, 5), 0.0, 1.0)
if ratio.found and ratio.ok do
tune_ratio(ratio.value)
end

The current float reader accepts plain decimal text such as 0.75, 1, 1.0, .5, -2.5, and --ratio -2.5. It does not yet accept exponent notation. Invalid decimal text returns E_INVALID_VALUE; out-of-range values return E_VALUE_OUT_OF_RANGE.

Use boolValue(&parsed.args, name) when a boolean flag may be explicitly enabled or disabled. Bare flags read as true; missing flags read as found: false, ok: true, and value: false.

let color = cli.boolValue(&parsed.args, str_slice("color", 0, 5))

Accepted long boolean forms include --color, --color=true, --color=false, and --no-color. The parser accepts true, false, 1, 0, yes, no, on, and off for attached boolean values. Invalid boolean text returns E_INVALID_VALUE.

Use ChoiceSet and choiceValue(&parsed.args, name, &choices) for enum-like string values:

var formats = cli.initChoiceSet()
_ = cli.addChoice(&formats, str_slice("text", 0, 4))
_ = cli.addChoice(&formats, str_slice("json", 0, 4))
_ = cli.addChoice(&formats, str_slice("table", 0, 5))
let format = cli.choiceValue(&parsed.args, str_slice("format", 0, 6), &formats)
if format.found and format.ok do
render(format.value)
end

Invalid choices return E_INVALID_CHOICE.

Use ConflictSet and validateConflicts(&parsed.args, &conflicts) for mutually exclusive flags:

var conflicts = cli.initConflictSet()
_ = cli.addConflict(&conflicts, str_slice("json", 0, 4), str_slice("table", 0, 5))
let valid = cli.validateConflicts(&parsed.args, &conflicts)
if !valid.ok do
show_error(valid.err_code, valid.err_flag, valid.err_other)
end

Conflicts return E_CONFLICTING_FLAGS and include both flag names in err_flag and err_other.

Use FlagGroup when a command requires at least one flag from a set, or exactly one mode from a set:

var input = cli.initFlagGroup(str_slice("input", 0, 5))
_ = cli.addGroupFlag(&input, str_slice("stdin", 0, 5))
_ = cli.addGroupFlag(&input, str_slice("file", 0, 4))
_ = cli.addGroupFlag(&input, str_slice("url", 0, 3))
let has_input = cli.validateAnyFlag(&parsed.args, &input)
let one_input = cli.validateExactlyOneFlag(&parsed.args, &input)

Missing groups return E_MISSING_GROUP. validateExactlyOneFlag returns E_CONFLICTING_FLAGS when two or more group members are present.

FlagSpec carries metadata for the help layer:

  • required
  • has_default
  • default_value
  • value_name
  • help

The helper functions requireFlag, defaultValue, valueName, and helpText return updated specs, so they can be composed around boolFlag, valueFlag, or repeatableFlag.

buildHelp(app_name, summary, usage_tail, &specs) returns a zero-copy HelpDoc view over the existing SpecSet. It does not render terminal text yet. It gives the future renderer a stable structural surface:

  • row_count
  • helpLong(&doc, i)
  • helpShort(&doc, i)
  • helpTakesValue(&doc, i)
  • helpValueName(&doc, i)
  • helpTextAt(&doc, i)
  • helpRequired(&doc, i)
  • helpRepeatable(&doc, i)
  • helpHasDefault(&doc, i)
  • helpDefaultValue(&doc, i)

The document is a view, so keep the SpecSet alive while using it.

The stdlib proof target is:

Terminal window
./scripts/zb test-cli