Skip to content

Tiri Options API

Paul Manias edited this page May 10, 2026 · 5 revisions

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'

Table of Contents

Quick Start

Creating a Parser

    Parameter Definitions | Built-In Types | Validation | Repeated Values | Subcommands

Help Generation

Parser Methods

parser.process() | parser.tryProcess() | parser.getUsage() | parser.getHelp() | parser.printHelp() | parser.addParam()

parser.getParam() | parser.addCommand() | parser.getCommand() | parser.setDefaults() | parser.validate()

Technical Details

Input Sources

Source Precedence

Error Handling

Usage Examples

Usage Examples

Quick Start

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

Creating a Parser

options()

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' }
})

Parser Configuration

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.

Parameter Definitions

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' }
}

Parameter Definition Fields

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.

Parameter Kinds

option

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

flag

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.

positional

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' }.

Aliases

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.

Built-In Types

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.

Type Examples

{ 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' } }

Custom Converters

When type is a function, the parser calls it for each raw value:

function converter(RawValue:any, Definition:table, Parser:table):any

For 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 }

Validation

Required Parameters

Parameters with required = true must be supplied through input, environment, or defaults. Missing required parameters raise an error.

Choices

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.

Pattern

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.

Path Existence

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 }

Relationships

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.

exec Callback

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.

Repeated Values

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.

Subcommands

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' }
         }
      }
   }
})

Command Definition Fields

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.

Command Parsing

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.

Help Generation

Automatic Help

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.

Parser Methods

parser.process()

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.

parser.tryProcess()

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)
end

parser.getUsage()

text = 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>

parser.getHelp()

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()

parser.printHelp(Format, Topic)

Prints the text generated by parser.getHelp().

parser.addParam()

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.' })

parser.getParam()

param = parser.getParam(Name)

Returns a registered parameter definition by canonical name or alias, or nil when no parameter matches.

parser.addCommand()

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.

parser.getCommand()

command = parser.getCommand(Name)

Returns a registered command definition by canonical name or alias, or nil when no command matches.

parser.setDefaults()

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 })

parser.validate()

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' })

Input Sources

Default Input

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

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 })

Task Parameters Option

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' } })

Source Precedence

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.

JSON User Config

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.

Environment Variables

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

Error Handling

onError Callback

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'.

onHelp Callback

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> { ... }
})

Strict Mode

When strict is true (the default), unknown parameters in the input raise an error. Setting strict = false silently ignores unknown tokens.

Usage Examples

Simple File Processor

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)

Subcommand Tool

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')
end

Environment Configuration

import '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')

Testing with tryProcess()

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'))

Clone this wiki locally