4x8matrix/cli_builder
A CLI Builder for Luau
cli-parser
A command-line parser for Luau: global options, commands, subcommands, positional arguments, generated help you
can restyle or replace, usage errors that read like a real tool's, and Tab completion in bash, zsh, fish and
PowerShell that comes for free. Runtime-agnostic; the examples run under Lune, create_app and
studio-themer run it under Zune.
pesde add 4x8matrix/cli_builder
Example
local process = require("@lune/process")
local cliBuilder = require("./luau_packages/cli_builder")
local cli = cliBuilder.cli
local command = cliBuilder.command
local option = cliBuilder.option
local argument = cliBuilder.argument
local app = cli
.new()
:setName("pixelcove")
:setVersion("2.1.0")
:setDescription("Manage game assets.")
:addOption(option.new():setName("verbose"):addAlias("V"):setDescription("Log everything."):build())
:addCommand(command
.new()
:setName("import")
:addAlias("i")
:setDescription("Import an asset.")
:addArgument(argument.new():setName("source"):setRequired(true):build())
:addOption(option.new():setName("type"):setType("string"):setChoices({ "sprite", "sound" }):setDefaultValue("sprite"):build())
:setCallback(function(context)
print(context.commandArguments.source, context.commandOptions.type, context.globalOptions.verbose)
end)
:build())
process.exit(app:run(process.args, process.env))
$ pixelcove import player.png --type sound -V
player.png sound true
$ pixelcove import
error: missing required argument <source>
The path to the source asset file.
Usage:
pixelcove import [options] <source>
Run 'pixelcove import --help' for more information.
Usage errors say what was missing or wrong, with the description and the possible values of the argument or option involved, and a did-you-mean for unknown commands and options.
run(args, environment?) returns an exit code: 0 after a callback, help or the version; 2 after a usage
error, which is printed with a usage hint. Pass the process environment so options with setEnv can read it.
What the parser accepts
--name value,--name=value,-a value,-avalue; boolean aliases cluster (-Vc).- Options anywhere after the command they belong to, before or after positionals. Global options anywhere.
--ends option parsing; everything after it is positional and also available ascontext.rest.- Booleans:
--flag,--flag=false(true/false/yes/no/1/0), and--no-flagwhen the option is negatable. - Repeatable options collect an array; a non-repeatable option given twice is a usage error.
- Choices, number conversion, defaults, environment fallbacks (
[env: NAME]in help), required options. - Variadic last argument (
<files...>), number arguments, argument choices and defaults. - Command aliases, hidden commands and options, a default command that takes the first token as its argument.
- Unknown commands and options fail with a did-you-mean suggestion.
--help/-hanywhere,app help <command...>,app <command> help.--version/-vwhen a version is set and the app has not defined those itself.
Builders
Every builder takes a resource table (option.new({ name = "type", type = "string" })) or a chain of setters,
and ends with :build().
option: setName, setDescription, setType("string" | "boolean" | "number") (boolean by default),
setDefaultValue, setValueName (the <PATH> placeholder), addAlias (one character), setChoices,
setRepeatable, setEnv, setNegatable, setRequired, setHidden.
argument: setName, setDescription, setType("string" | "number"), setRequired, setChoices,
setVariadic, setDefaultValue. Required arguments come before optional ones; a variadic argument is last.
command: setName, setDescription, setCallback, addOption, addArgument, addSubCommand,
addAlias, setHidden.
cli: setName, setDescription, setVersion, setLicense, setGitRepository, setBugReportUrl,
setHomepage, setCopyright, setEpilog, addAuthor, addOption, addCommand, setCallback (runs when no
command is given), setDefaultCommand, setBeforeRunHook, configure({ ... }), helpText(commandTree?),
helpModel(commandTree?), completionScript(shell), complete(words, environment?), run(args, environment?).
Options and arguments also take setCompleter(fn) for live completion values (see Completions).
Definitions that could not be told apart are refused as the app is built: duplicate option names or aliases,
duplicate command names or aliases, and the reserved --help, -h, help, completions and __complete.
Configuration
app:configure({
ansiColorEnabled = not process.env.NO_COLOR, -- default true; also pass false when stdout is not a TTY
output = print, -- receives help, the version, usage errors, completion data
theme = { title = { "Cyan", "Bold" }, note = { "Dim" }, indent = 4, columnGap = 2 },
help = { row = function(row, width, style) ... end },
completions = true, -- false removes `completions` and the `__complete` hook
})
Customising the output
Three depths, from a colour tweak to a rewrite.
Theme. Roles paint parts of the text; each role is a list of formats (Bold, Dim, Italic, Underline,
Strikethrough, Grey, Red, Green, Yellow, Blue, Magenta, Cyan, White). indent and columnGap
set the row layout.
| role | paints | default |
|---|---|---|
title | Usage: and section headings | Yellow, Underline |
name | command, option and argument names | Green, Bold |
placeholder | <PATH> | |
description | row descriptions | |
note | [default: x], [env: X], (required) | Grey |
link | URLs in the footer | Bold |
credit | authors, license, copyright | Grey, Italic |
error | the error: prefix | Red, Bold |
Parts. Help is rendered by eight functions; replace any of them with configure({ help = { <part> = fn } })
and the others stay default. Every part receives a style and paints only through style.paint(role, text), so
ansiColorEnabled = false strips custom parts too. Returning "" drops a part.
| part | signature | renders |
|---|---|---|
banner | (model, style) | name version and the description |
usage | (model, style) | the Usage: block |
row | (row, width, style) | one aligned row; width is the label column |
section | (section, rows, style) | a heading over the rendered rows |
footer | (model, style) | links, epilog, credits, copyright |
help | (model, parts, style) | joins parts.banner, parts.usage, parts.sections, parts.footer |
error | (errorModel, style) | a usage error with its usage hint |
version | (model, style) | the version line |
app:configure({
help = {
row = function(row, width, style)
return ` {style.paint("name", row.label)}{string.rep(" ", width - #row.label + 2)}{row.description}`
end,
},
})
Model. app:helpModel(commandTree?) returns the data every part is rendered from, for apps that render
help themselves:
{
kind = "global" | "command",
name, version, description,
path = { "import", "batch" },
usage = { "pixelcove import [options] <source> [destination]", "pixelcove import <command>" },
sections = { { id = "options", heading = "Options:", rows = { { kind, names, placeholder, label, description, notes } } } },
labelWidth = 24,
links = { { label, url } }, epilog, credits = { "Authors: ..." }, copyright,
}
The callback context
{
commandTree = { Command }, -- the resolved path, innermost last
commandOptions = { [name] = value }, -- options of every command on the path
commandArguments = { [name] = value },
globalOptions = { [name] = value },
rest = { string }, -- tokens after `--`
}
Absent booleans are false; absent strings and numbers are nil unless a default or environment variable
fills them; repeatable options and variadic arguments are arrays.
Completions
Every app gets a completions <shell> command and Tab completion for bash, zsh, fish and PowerShell without
writing anything. Install once:
pixelcove completions fish > ~/.config/fish/completions/pixelcove.fish
pixelcove completions bash > ~/.local/share/bash-completion/completions/pixelcove
pixelcove completions zsh > ~/.zsh/completions/_pixelcove # a directory on $fpath
pixelcove completions powershell >> $PROFILE
The scripts are thin: on every Tab the shell runs pixelcove __complete -- <words so far> and shows what the
app answers, the way kubectl and gh do. So completion knows commands, aliases, options, --no- forms and
choices as they are today, shows descriptions in fish and zsh, and can offer live values:
argument.new():setName("theme"):setCompleter(function(request)
-- request.partial, request.words, request.commandPath, request.environment
return { "mocha", { value = "nord", description = "cool blues" } }
end)
When neither a completer nor choices exist, the shell falls back to file names. app:complete(words) returns
the same candidates as data, which is how the engine is tested. configure({ completions = false }) removes
both commands.
Development
pesde install frktest into luau_packages/
lune run .lune/test.luau the suites under tests/
License
MIT