tools.cli
tools.cli provides a small, Ward-native command-line argument parser for Lua scripts.
It is designed for:
- Declarative option/argument definitions (data-first spec).
- Common GNU-style flag forms:
--long,--long=value,--long value,-s,-svalue, and short bundles like-abc. - Automatic help generation (
-h/--help). - Optional version output (
--version/-V). - Optional subcommands (including nesting).
- Subcommand aliases (via
subcommand.aliases). - Grouped option sections in help (via
option.group). - Examples and an epilog section in help (via
spec.examplesandspec.epilog). - Subcommand help inherits parent
exampleswhen the subcommand does not define its own examples. - No side effects: it does not use
ioand it does not exit the process; it returns structured results/errors.
Quick example
local cli = require("wardlib.tools.cli")
local parser = cli.new({
name = "mytool",
version = "1.2.3",
summary = "Example tool",
options = {
{ id = "verbose", short = "v", long = "verbose", kind = "count", help = "Increase verbosity" },
{ id = "config", short = "c", long = "config", kind = "value", metavar = "FILE", help = "Config file" },
{ id = "dry_run", long = "dry-run", kind = "flag", help = "Do not apply changes" },
},
positionals = {
{ id = "input", metavar = "INPUT", kind = "value", required = true, help = "Input file" },
{ id = "rest", metavar = "ARGS", kind = "values", variadic = true, help = "Extra args" },
},
}, { auto_version = true })
local ok, out = parser:parse() -- defaults to global `arg`
if not ok then
-- out.code may be "help" or "version" (non-error exits), or an error code.
print(out.text)
return
end
print(out.values.verbose)
print(out.values.config)
print(out.positionals.input)
Subcommands example
local cli = require("wardlib.tools.cli")
local parser = cli.new({
name = "mytool",
summary = "Tool with subcommands",
subcommands = {
{
name = "run",
summary = "Run the tool",
options = {
{ id = "jobs", short = "j", long = "jobs", kind = "value", type = "int", default = 1, metavar = "N" },
},
positionals = {
{ id = "target", metavar = "TARGET", kind = "value", required = true },
},
},
},
})
local ok, out = parser:parse({ [0] = "mytool", "run", "--jobs", "4", "all" })
assert(ok)
-- Root values/positionals are in out.values / out.positionals.
-- Selected command details are in out.cmd.
print(out.cmd.name) -- "run"
print(out.cmd.values.jobs) -- 4
print(out.cmd.positionals.target) -- "all"
API
cli.new(spec, opts?) -> parser
Creates a parser.
opts:
auto_help(boolean, defaulttrue): injects-h/--helpif not defined.auto_version(boolean, defaultfalse): injects--version/-Vif not defined.
parser:parse(argv?, parse_opts?) -> (ok, result_or_err)
Parses arguments.
argv:nil(default): reads_G.arg.{"--foo", "bar"}: array form.{ [0] = "script.lua", "--foo", "bar" }: Luaarg-like form.
parse_opts:
start_index(number, default1): where to begin readingargv.allow_unknown(boolean, defaultfalse): if true, unknown options are appended toresult.rest.stop_at_first_positional(boolean, defaultfalse): if true, once a positional is seen, the remaining tokens are treated as positional/rest.on_event(event, state)(function|nil): event callback.
Return values:
-
On success:
ok=true, andresult:result.values(table): parsed option values byid.result.positionals(table): parsed positional values byid.result.rest(array): remaining/unconsumed tokens (when allowed).result.cmd(table|nil): selected subcommand parse (when subcommands are defined).result.argv0(string): program/script name when available.
-
On failure/help/version:
ok=false, anderr:err.code(string): e.g."help","version","unknown_option","missing_value","invalid_value","missing_required","unknown_command","option_repeated","too_many_occurrences","mutually_exclusive","missing_one_of".err.message(string)err.token(string|nil)err.text(string): formatted message plus usage/help.
parser:usage() -> string
Returns a single-line usage string.
parser:help(help_opts?) -> string
Returns a multi-section help string.
help_opts:
width(number, default100)include_description(boolean, defaulttrue)include_defaults(boolean, defaulttrue)
Help formatting
Help output is deterministic and organized into sections. Options may be grouped by setting group on each option.
When subcommands are used, auto-injected --help/-h and --version/-V are displayed under Common options if you define at least one explicit option group.
If spec.examples is provided, an Examples section is appended. For subcommands, if the subcommand does not define examples, the parent command’s examples are used (inheritance). If the subcommand defines examples, they override the parent examples.
If spec.epilog is provided, it is appended after all other sections.
examples entries may be:
- a string command line (printed verbatim), or
- a table
{ cmd = "...", desc = "..." }(or{ "...", "..." })
Spec schema
Top-level fields
-
name(string, required): program name used in usage/help. -
summary(string|nil): one-line description. -
description(string|nil): longer description. -
version(string|nil): version string (used by--versionwhenauto_version=true). -
options(array|nil): root options. -
positionals(array|nil): root positionals. -
subcommands(array|nil): list of subcommands. -
examples(array|nil): help examples (strings or{cmd, desc}tables). -
epilog(string|nil): additional text appended to help. -
constraints(table|nil): extra validation rules applied after parsing. See Constraints below.
Options
Each element of spec.options is a table:
-
id(string, required): key inresult.values. -
long(string|nil):--longname (without--). -
short(string|nil): single-letter-s. -
kind(string, default"flag"):"flag": boolean"count": increments per occurrence"value": consumes one value"values": consumes one value per occurrence into an array
-
type(string, default"string"):"string" | "number" | "int" | "enum" -
choices(array, enum only): allowed values -
default(any|nil) -
required(boolean|nil) -
metavar(string|nil): label shown in help for value options -
group(string|nil): option help section heading (default:"Options"). -
help(string|nil) -
negatable(boolean|nil): forkind="flag"only. If true,--no-<long>is accepted and sets the flag tofalse. -
repeatable(boolean, defaulttrue): if set tofalse, repeating the option produces anoption_repeatederror (applies toflag,value, andcount). -
max_count(number|nil): forkind="count"only. If set, exceeding the limit produces atoo_many_occurrenceserror. -
validate(value)(function|nil): custom validator; returntrueon success, orfalse, reasonto reject. A thrown error is surfaced as aninvalid_valueerror. -
on(value, event, state)(function|nil): called after parsing/coercion
Positionals
Each element of spec.positionals is a table:
-
id(string, required) -
metavar(string, required) -
kind(string, default"value"):"value" | "values" -
type(string, default"string") -
required(boolean|nil) -
variadic(boolean|nil): only valid for the last positional; consumes remaining tokens -
help(string|nil) -
validate(value)(function|nil): custom validator; returntrueon success, orfalse, reasonto reject. -
on(value, event, state)(function|nil)
Constraints
spec.constraints is optional and supports:
mutex(array of groups): each group is an array of optionids that are mutually exclusive. If more than one is present, parsing fails withcode="mutually_exclusive".one_of(array of groups): each group is an array of optionids where at least one must be present. If none are present, parsing fails withcode="missing_one_of".
Example:
constraints = {
mutex = { { "json", "yaml" } },
one_of = { { "input", "stdin" } },
}
Subcommands
Each element of spec.subcommands is a table:
-
name(string, required) -
summary(string|nil) -
description(string|nil) -
options(array|nil) -
positionals(array|nil) -
subcommands(array|nil): nested subcommands -
aliases(string|array|nil): alternative tokens that invoke this command (e.g.{ "r", "execute" }).
When a subcommand is selected, parsing continues with that subcommand spec. The nested parse result is returned in result.cmd.