-
-
Notifications
You must be signed in to change notification settings - Fork 2
Tiri Options API
The Options library provides a standardised means of command-line option parsing for Tiri scripts. It replaces ad hoc arg() lookups with a declarative parser that handles named options, flags, positional arguments, subcommands, typed value conversion, validation, environment variable lookup, and help generation.
It can be loaded with the line:
import 'options'Parameter Definitions | Built-In Types | Validation | Repeated Values | Subcommands
parser.process() | parser.tryProcess() | parser.getUsage() | parser.getHelp() | parser.printHelp() | parser.addParam()
parser.getParam() | parser.addCommand() | parser.getCommand() | parser.setDefaults() | parser.validate()
import 'options'
parser = options({
name = 'vuepoint',
description = 'A file viewer for SVG, RIPL, and image formats.',
args = array<table> {
{ name = 'input', kind = 'positional', type = 'file', required = true,
comment = 'File to open.' },
{ name = 'output', kind = 'option', type = 'file',
comment = 'Render output path.' },
{ names = array<string> { 'verbose', 'v' }, kind = 'flag',
comment = 'Enable verbose logging.' }
}
})
args = parser.process()
print(args.input, args.output, args.verbose)Running this script from the command line:
origo viewer.tiri scene.svg output=preview.png verbose
produces:
scene.svg preview.png true
parser = options(Config)
Constructs a parser from a configuration table. The returned parser object exposes methods for adding parameters, parsing input, generating help, and validating arguments.
parser = options({
name = 'mytool',
cmdName = 'mytool',
description = 'A short description of the tool.',
epilog = 'See the wiki for more information.',
copyright = 'Copyright 2026 Example Corp',
license = 'MIT',
strict = true,
envPrefix = 'MYTOOL',
args = array<table> { ... },
commands = array<table> { ... },
defaults = { output = 'result.txt' }
})| Option | Type | Default | Description |
|---|---|---|---|
name |
str |
'script' |
Program name shown in usage and help. |
cmdName |
str |
lower-case name
|
Script name shown in generated usage lines. Derived from name if not specified. |
description |
str |
nil |
Long description shown before options in help. |
epilog |
str |
nil |
Text shown after options in help. |
copyright |
str |
nil |
Copyright text appended to help. |
license |
str |
nil |
Licence text appended to help. |
args |
array<table> |
{} |
Parameter definitions. |
commands |
array<table> |
{} |
Subcommand definitions. |
strict |
bool |
true |
Reject unknown parameters. |
defaults |
table |
nil |
Programmatic defaults keyed by parameter name. |
userConfig |
str |
nil |
JSON config file path to load as user defaults when the file exists. |
envPrefix |
str |
nil |
Prefix for environment variable lookup. |
autoHelp |
bool |
true |
Reserve automatic help and print help on empty input. When false, explicit help still works unless the parser declares its own help parameter. |
onError |
function |
nil |
Error callback for rejected values. |
onHelp |
function |
nil |
Help callback, receives generated help text. |
Parameters are defined as tables within the args array. Each definition describes one named option, flag, or positional argument.
args = array<table> {
{ name = 'file', kind = 'option', type = 'file', required = true },
{ names = array<string> { 'verbose', 'v' }, kind = 'flag', comment = 'Enable verbose output.' },
{ name = 'count', kind = 'option', type = 'int', default = 10 },
{ name = 'input', kind = 'positional', type = 'file' }
}| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required* | Canonical parameter name. |
names |
array<string> |
nil |
Accepted names; the first is canonical, the rest are aliases. |
comment |
str |
nil |
One-line help description. |
description |
str |
nil |
Detailed help shown via help=paramname. |
kind |
str |
'option' |
Parameter role: option, flag, or positional. |
type |
str or function
|
'str' |
Built-in type name or custom converter function. |
default |
any | nil |
Value used when the parameter is absent. |
required |
bool |
false |
Parameter must be supplied. |
multiple |
bool |
false |
Accept repeated values and return an array. |
hidden |
bool |
false |
Exclude from generated help output. |
group |
str |
automatic | Help grouping label. |
metavar |
str |
upper-case name | Value label used in usage and help text. |
choices |
array |
nil |
Allowed values after type conversion. |
pattern |
regex or str
|
nil |
Regex validation applied to the converted value. |
exists |
bool |
false |
For path, file, and folder types, require the path to exist. |
requires |
str or array
|
nil |
Other parameters required when this one is present. |
requiresAny |
array |
nil |
At least one listed parameter must also be present. |
excludes |
str or array
|
nil |
Parameters that cannot be used with this one. |
exec |
function |
nil |
Callback executed after conversion and validation. |
*Either name or names must be supplied. When names is supplied, the first entry is the canonical name.
Named parameters that require name=value syntax. If a default is declared, a bare name token uses the default value.
{ name = 'output', kind = 'option', type = 'file' }output=preview.png
Boolean switches. A bare name token resolves to true, a no-name token resolves to false, and name=value uses boolean conversion.
{ name = 'verbose', kind = 'flag' }verbose -- true
no-verbose -- false
verbose=off -- false
The no- prefix is only recognised when the suffix matches a declared flag name or alias. Using no- on a non-flag parameter raises an error.
Unnamed values assigned in declaration order. The final positional parameter may use multiple=true to consume all remaining values.
{ name = 'input', kind = 'positional', type = 'file', required = true },
{ name = 'extras', kind = 'positional', type = 'str', multiple = true }scene.svg one two three
Here input receives scene.svg and extras receives { 'one', 'two', 'three' }.
Provide multiple accepted names for a parameter using the names field. The first name is canonical and is used as the key in the result table.
{ names = array<string> { 'verbose', 'v' }, kind = 'flag' }Both verbose and v are accepted on the command line, but the result is always stored under args.verbose.
The type field selects a built-in converter or accepts a custom converter function.
| Type | Output | Accepted Input | Description |
|---|---|---|---|
str / string
|
str |
any string | Default string value. |
num / number
|
num |
numeric string | Converts with tonumber(). |
int |
num |
integer string | Converts and rejects fractional values. |
bool |
bool |
true/false, 1/0, yes/no, on/off
|
Converts explicit boolean values. |
enum |
str |
value listed in choices
|
String value restricted to a choices list. |
path |
str |
path string | Optionally validates existence with exists=true. |
file |
str |
path string | Requires a file when exists=true. |
folder |
str |
path string | Requires a folder when exists=true. |
csv |
array<string> |
comma-separated string | Splits and trims values. |
array |
array<string> |
{ ... } parameter or repeated values |
Normalises multi-value input. |
duration |
num |
number with ms, s, m, h, or d suffix |
Converts to seconds. |
size |
num |
number with b, kb, mb, gb, or tb suffix |
Converts to bytes. |
percent |
num |
number or number with % suffix |
Normalises to a fraction (50% becomes 0.5). |
regex |
regex object | PCRE-compatible pattern | Compiles via the Tiri regex API. |
json |
any | JSON value | Parses JSON into the corresponding Tiri value. |
date |
str |
YYYY-MM-DD |
Validates ISO calendar dates including leap years. |
time |
str |
HH:MM, HH:MM:SS, optional fractional seconds |
Validates ISO time values. |
datetime |
str |
ISO 8601 date-time | Validates date-time with optional timezone offset. |
colour |
str |
CSS functional colour syntax | Validates rgb(), hsl(), hsv(), and oklch() values. |
{ name = 'timeout', type = 'duration', default = '30s' } -- 30.0
{ name = 'maxSize', type = 'size', default = '10mb' } -- 10485760
{ name = 'opacity', type = 'percent', default = '80%' } -- 0.8
{ name = 'tags', type = 'csv' } -- { 'a', 'b', 'c' }
{ name = 'config', type = 'json' } -- parsed table/array
{ name = 'filter', type = 'regex' } -- compiled regex
{ name = 'start', type = 'date' } -- '2026-01-15'
{ name = 'format', type = 'enum', choices = array<string> { 'svg', 'png', 'pdf' } }When type is a function, the parser calls it for each raw value:
function converter(RawValue:any, Definition:table, Parser:table):anyFor command-line and environment values, RawValue is normally a string. For defaults and programmatic input, it may already be a typed value. The converter returns the converted value or throws an error.
{ name = 'label', type = function(RawValue:any, Definition:table, Parser:table):any
return 'prefix:' .. tostring(RawValue)
end }Parameters with required = true must be supplied through input, environment, or defaults. Missing required parameters raise an error.
Restrict a parameter to a set of allowed values after type conversion:
{ name = 'format', type = 'enum', choices = array<string> { 'svg', 'png', 'pdf' } }The enum type is a convenience that combines str conversion with a mandatory choices list. Other types can also use choices to restrict their output values.
Validate the converted value against a regex pattern:
{ name = 'id', type = 'str', pattern = '^[A-Z]{3}-[0-9]+$' }The pattern field accepts a regex string or a compiled regex object.
For path, file, and folder types, setting exists = true requires the path to exist. The file type additionally requires the path to be a file, and folder requires it to be a directory.
{ name = 'input', type = 'file', exists = true }Parameters can declare dependencies and conflicts with other parameters:
{ name = 'output', kind = 'option', type = 'file',
requires = 'input' } -- 'input' must also be present
{ name = 'format', kind = 'option', type = 'enum',
choices = array<string> { 'svg', 'png' },
requiresAny = array<string> { 'output', 'preview' } } -- at least one must be present
{ name = 'quiet', kind = 'flag',
excludes = 'verbose' } -- cannot be used with 'verbose'Relationships are checked after all sources have been merged and values have been converted.
The exec field runs a callback after conversion and validation for additional custom checks:
{ name = 'port', type = 'int', exec = function(Parser:table, ArgName:str, Value:any, Param:table)
if Value < 1 or Value > 65535 then
error('Port must be between 1 and 65535.')
end
end }The callback receives the parser, the argument name as supplied, the converted value, and the parameter definition. Throwing an error from exec triggers the same error handling path as a failed type conversion.
Tip
You can use raise ERR_Terminate from within an exec function to stop the program silently. This capability is used for printing help.
Parameters with multiple = true collect repeated occurrences into an array. This applies to named options, flags, and positional parameters.
parser = options({
args = array<table> {
{ name = 'tag', type = 'str', multiple = true },
{ name = 'files', kind = 'positional', type = 'file', multiple = true }
}
})
args = parser.process(array<string> { 'tag=red', 'tag=blue', 'one.svg', 'two.svg' })
-- args.tag = { 'red', 'blue' }
-- args.files = { 'one.svg', 'two.svg' }A scalar default for a multiple parameter is converted and wrapped as a single-element array. An array default is converted item by item.
When using table input, an array value is treated as repeated values:
args = parser.process({ tag = array<string> { 'red', 'blue' } })A variadic positional parameter (using multiple = true) must be the final positional definition.
Larger tools can expose command verbs with global options and command-specific arguments.
parser = options({
name = 'tool',
args = array<table> {
{ names = array<string> { 'verbose', 'v' }, kind = 'flag' }
},
commands = array<table> {
{
name = 'build',
comment = 'Build the project.',
args = array<table> {
{ name = 'target', kind = 'option', type = 'str', default = 'all' }
}
},
{
names = array<string> { 'test', 't' },
comment = 'Run tests.',
args = array<table> {
{ name = 'filter', kind = 'option', type = 'str' }
}
}
}
})| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required* | Canonical command name. |
names |
array<string> |
nil |
Accepted names; the first is canonical, the rest are aliases. |
comment |
str |
nil |
One-line help description. |
description |
str |
nil |
Detailed help shown via help=commandname. |
group |
str |
'Commands' |
Help grouping label. |
args |
array<table> |
{} |
Command-specific parameter definitions. |
hidden |
bool |
false |
Exclude from generated help output. |
*Either name or names must be supplied.
Global options parse before the command token. The selected command's arguments parse from the remaining input. The result contains three fields:
| Field | Type | Description |
|---|---|---|
command |
str |
Canonical command name. |
globals |
table |
Parsed global parameters. |
args |
table |
Parsed parameters for the selected command. |
result = parser.process(array<string> { 'verbose', 'build', 'target=release' })
-- result.command = 'build'
-- result.globals.verbose = true
-- result.args.target = 'release'When using table input, the command is specified with a command key:
result = parser.process({
command = 'build',
globals = { verbose = true },
args = { target = 'release' }
})Command aliases dispatch to the canonical command name. Missing commands raise an error when commands are defined.
When autoHelp is true (the default), the parser reserves the help token and prints help if no parameters are supplied. Explicit help and help=topic requests are also handled. Set autoHelp=false when an empty command line should be parsed normally or validated. In that mode, explicit help still works unless the parser declares its own help parameter, in which case that parameter owns the token and can decide how to handle it. Normal help shows the program name, description, usage line, grouped parameters, commands, and metadata. Detailed help accepts a parameter name, alias, or command name as the topic.
mytool help -- prints normal help
mytool help=output -- prints detailed help for 'output'
mytool help=build -- prints detailed help for 'build' command
When help is handled through process(), the help text is printed (or dispatched through onHelp) and nil is returned. Through tryProcess(), help requests return a structured result table without printing.
result = parser.process(Input, Options)
Parses input and returns a typed argument table, command result table, or nil when help is handled. Parse failures are raised as errors.
Input may be an array of command-line tokens, a table of keyed values, or nil to use the default input source. Options is an optional table; currently Options.taskParameters overrides the task parameter source.
result, err = parser.tryProcess(Input, Options)
The non-terminating parsing API. Returns result, nil on success, or nil, err on failure. Help requests return a structured table with help, topic, and text fields instead of printing. The err table includes the original ERR value in code and error string in message.
result, err = parser.tryProcess(array<string> { 'file=scene.svg' })
if err then
print('Parse error: ' .. err.message)
end
-- Check for help result
if result and result.help then
print(result.text)
endtext = parser.getUsage()
Returns a compact plain-text usage line. Hidden parameters are excluded. Required parameters are shown without brackets; optional parameters are bracketed.
print(parser.getUsage())
-- Usage: mytool [output=OUTPUT] [verbose] <INPUT>text = parser.getHelp(Format, Topic)
Returns generated help text. Format may be nil or 'text' for plain text; other formats are reserved for future use. Topic may be a parameter name, alias, or command name for detailed help, or nil for normal help.
parser.printHelp(Format, Topic)
Prints the text generated by parser.getHelp().
param = parser.addParam(Definition)
Adds a parameter definition after construction. Returns the normalised parameter definition.
parser.addParam({ name = 'extra', type = 'str', comment = 'An extra option.' })param = parser.getParam(Name)
Returns a registered parameter definition by canonical name or alias, or nil when no parameter matches.
command = parser.addCommand(Definition)
Adds a command definition after construction. Returns the normalised command definition. Commands added this way support command.addParam() for adding command-specific parameters.
command = parser.getCommand(Name)
Returns a registered command definition by canonical name or alias, or nil when no command matches.
parser = parser.setDefaults(Defaults)
Replaces the runtime defaults layer. Runtime defaults take precedence over parser configuration defaults and parameter defaults, but are overridden by environment variables and explicit input. Returns the parser for chaining.
parser.setDefaults({ output = 'default.txt', count = 5 })result = parser.validate(Args)
Validates an argument table against the parser's rules. Converts values, applies required checks, validation fields, relationship checks, and exec callbacks. Returns the validated argument table or raises an error.
validated = parser.validate({ output = 'result.txt', count = '10' })When process() or tryProcess() is called with nil, the parser reads ordered arguments from processing.task().parameters. Launcher options (such as --log-warning and --set-volume) and the script path are automatically skipped, so only script-relevant arguments are parsed.
args = parser.process()Explicit input bypasses task parameters entirely.
Array input parses ordered tokens using the same rules as command-line input:
args = parser.process(array<string> { 'scene.svg', 'output=preview.png', 'verbose' })Table input parses keyed values, accepting pre-typed values without string conversion:
args = parser.process({ input = 'scene.svg', count = 3, enabled = true })The Options table can override the task parameter source:
-- Use a specific argument array as the task parameter source
args = parser.process(nil, { taskParameters = array<string> { 'script.tiri', 'file.svg' } })When multiple sources provide a value for the same parameter, the highest-priority source wins. From lowest to highest:
| Priority | Source | Description |
|---|---|---|
| 1 | Parameter default
|
Fallback value declared on the parameter definition. |
| 2 | Parser defaults
|
Programmatic defaults from the parser configuration. |
| 3 |
userConfig JSON defaults |
Defaults loaded from the selected JSON config file. |
| 4 | setDefaults() |
Runtime defaults set after construction. |
| 5 | Environment variable | Values from envPrefix environment variables. |
| 6 | Input value | Values from explicit input or ordered task parameters. |
parser = options({
envPrefix = 'MYTOOL',
defaults = { output = 'parser-default.txt' },
args = array<table> {
{ name = 'output', type = 'file', default = 'param-default.txt' }
}
})
parser.setDefaults({ output = 'runtime-default.txt' })With MYTOOL_OUTPUT=env.txt set in the environment, the value env.txt is used unless the command line supplies output=cli.txt, which takes the highest priority.
Setting userConfig injects a normal config path option with the supplied path as its default. The selected path is resolved using normal precedence, so defaults.config, setDefaults({ config = ... }), an environment variable, or config=... input can choose a different file.
parser = options({
userConfig = 'user:config/http_server_cfg.json',
args = array<table> {
{ name = 'folder', type = 'folder', exists = true, required = true },
{ name = 'port', type = 'int', default = 8080 }
}
})If the selected file exists, it must be a JSON object. Its fields are parsed like setDefaults() values, so unknown parameter names fail in the same way as unknown runtime defaults. Missing config files are skipped without error. Automatic help requests do not load the config file.
If userConfig is set as true then no default path applies, and the user must provide one by setting the config parameter on the command line.
Setting envPrefix enables environment variable lookup for every named option and flag. The variable name is formed from the prefix, an underscore, and the canonical parameter name converted to upper snake-case. Aliases do not create additional environment variable names.
parser = options({
envPrefix = 'APP',
args = array<table> {
{ name = 'logLevel', kind = 'option', type = 'str' },
{ names = array<string> { 'verbose', 'v' }, kind = 'flag' }
}
})| Parameter | Environment Variable |
|---|---|
logLevel |
APP_LOG_LEVEL |
verbose |
APP_VERBOSE |
The onError callback is called when a value is rejected during conversion or validation. It receives the parser, the error, the parameter definition, the argument name as supplied, and the raw value.
parser = options({
onError = function(Parser:table, Error:table, Param:table, ArgName:str, RawValue:any):str
print('Warning: ' .. Error.message)
return 'ignore'
end,
args = array<table> { ... }
})The callback returns an instruction string:
| Instruction | Description |
|---|---|
'fail' |
Use the default failure path. process() raises the error; tryProcess() returns nil, err. |
'ignore' |
Treat the value as absent. If the parameter has a default, use the converted default. |
'skip' |
Discard the value and leave the parameter unset, even when a default exists. |
Returning nil, false, or any unrecognised value is equivalent to 'fail'.
The onHelp callback is called when process() handles a help request. It receives the parser, the generated help text, and the topic (or nil for normal help).
parser = options({
onHelp = function(Parser:table, Text:str, Topic:any)
io.writeAll('temp:help.txt', Text)
end,
args = array<table> { ... }
})When strict is true (the default), unknown parameters in the input raise an error. Setting strict = false silently ignores unknown tokens.
import 'options'
parser = options({
name = 'convert',
description = 'Convert files between formats.',
args = array<table> {
{ name = 'input', kind = 'positional', type = 'file', required = true, exists = true,
comment = 'Input file to convert.' },
{ name = 'output', kind = 'option', type = 'file', required = true,
comment = 'Output file path.' },
{ name = 'format', kind = 'option', type = 'enum',
choices = array<string> { 'svg', 'png', 'pdf' }, default = 'svg',
comment = 'Output format.' },
{ names = array<string> { 'verbose', 'v' }, kind = 'flag',
comment = 'Enable verbose logging.' }
}
})
args = parser.process()
if args is nil then return end
print('Converting', args.input, 'to', args.format, 'at', args.output)import 'options'
parser = options({
name = 'project',
description = 'Project management tool.',
args = array<table> {
{ names = array<string> { 'verbose', 'v' }, kind = 'flag' }
},
commands = array<table> {
{
name = 'build',
comment = 'Build the project.',
args = array<table> {
{ name = 'target', kind = 'option', type = 'str', default = 'all',
comment = 'Build target.' },
{ name = 'jobs', kind = 'option', type = 'int', default = '4',
comment = 'Parallel jobs.' }
}
},
{
name = 'test',
comment = 'Run tests.',
args = array<table> {
{ name = 'filter', kind = 'option', type = 'str',
comment = 'Test name filter.' },
{ name = 'timeout', kind = 'option', type = 'duration', default = '60s',
comment = 'Test timeout.' }
}
}
}
})
result = parser.process()
if result is nil then return end
if result.command is 'build' then
print('Building', result.args.target, 'with', result.args.jobs, 'jobs')
elseif result.command is 'test' then
print('Testing with', result.args.timeout, 'second timeout')
endimport 'options'
parser = options({
name = 'server',
envPrefix = 'MYSERVER',
args = array<table> {
{ name = 'host', type = 'str', default = '0.0.0.0', comment = 'Bind address.' },
{ name = 'port', type = 'int', default = '8080', comment = 'Listen port.' },
{ name = 'logLevel', type = 'enum',
choices = array<string> { 'debug', 'info', 'warning', 'error' },
default = 'info', comment = 'Log verbosity.' }
}
})
args = parser.process()
if args is nil then return end
-- Values can come from:
-- MYSERVER_HOST, MYSERVER_PORT, MYSERVER_LOG_LEVEL
-- or from the command line:
-- host=127.0.0.1 port=9090 logLevel=debug
print('Listening on', args.host .. ':' .. args.port, 'at', args.logLevel, 'level')import 'options'
parser = options({
args = array<table> {
{ name = 'count', type = 'int', required = true }
}
})
-- Successful parse
result, err = parser.tryProcess(array<string> { 'count=5' })
assert(result.count is 5)
assert(err is nil)
-- Failed parse returns nil and an error table
result, err = parser.tryProcess(array<string> { })
assert(result is nil)
assert(err.code is ERR_ParameterRequired)
assert(err.message:find('count'))