cozycliparser: Command-Line Parser Builder
A thin but useful wrapper over std/parseopt.
Features:
- A single place to declare options, flags and arguments - not three (parser, help text, shortNoVal/longNoVal set and seq).
- Low-magic implementation: no cryptic compiler errors.
- Same DIY stdlib approach: parsing only, handling is fully in your control.
- No idiosyncratic DSL: just regular Nim checked by the compiler.
- Declaration and handling code are co-located and cannot go out of sync.
- Slim code, no dependencies beyond std/[parseopt, strutils, terminal, envvars].
Provides macro buildParser, which generates the command-line parser from a set of regular, fully type-checked procedure calls and option-handling closures.
Quick start
Call macro buildParser with a program name, a help namespace name, a parser mode, and a declarative body. The opt, flag, arg and cmd routines register options, flags, positional arguments and subcommands along with their handlers — closures passed as the last argument. The handlers are invoked when the parser meets the corresponding input.
Example:
import cozycliparser type Options = object output: string input: string verbose: bool greetName: string = "world" var options: Options buildParser(parseConfig(helpPrefix = "Greeter v0.1\nThis program greets."), "greeter", "Cli", GnuMode): opt('\0', "output", "Output file", "FILE") do (val: string): options.output = val flag('v', "verbose", "Enable verbose output") do (): options.verbose = true arg("INPUT", "Input file") do (val: string): options.input = val cmd("greet", "Greets NAME") do (): arg("NAME", "Name to greet") do (val: string): if val != "": options.greetName = val echo "Hello ", options.greetName doAssert $Cli.help == """Greeter v0.1 This program greets. Usage: greeter [options] INPUT <greet> Arguments: INPUT Input file Commands: greet Greets NAME Options: --output=FILE Output file -v, --verbose Enable verbose output -h, --help Show this help and exit""" doAssert $Cli.greet.help == """Greeter v0.1 This program greets. Usage: greeter greet [options] NAME Arguments: NAME Name to greet Options: -h, --help Show this help and exit"""
buildParser injects a single name into the outer scope:
- const Cli: a zero-runtime-cost namespace of structured HelpText values. Access as Cli.help (root) or Cli.<subcmd>.help. Call $Cli.help for a plain string, or Cli.help.display(<fd>) for styled output.
The registration procs (opt, flag, arg, run, cmd) also use a public module-level variable bpCtx to store their handlers there.
Accessing help
Help is exposed through a simulated namespace backed by type-constrained templates. Each .help resolves to a HelpText object that facilitates styled output, built at compile time:
# with `buildParser`'s `helpName` argument set to "Cli": Cli.help.display() # root-level Cli.foo.help.display() # subcommand Cli.help.display(stderr) quit($Cli.foo.bar.help, 0) # pass plain string if required
The namespace is available inside handler closures too, since it is injected before their declarations are emitted.
Optional short / long forms
- Pass '\0' (or any char with ord < 32) as short to suppress the short form of an option or flag.
- Pass "" as name to suppress the long form.
Attempting to suppress both is a compile-time error.
Subcommands
Declare subcommands with cmd. Register the subcommand's own options directly inside the cmd's handler closure:
buildParser("git", "Cli", GnuMode): cmd("add", "Add files to the index") do (): arg("FILE", "File to add") do (val: string): if val != "": options.addQueue.add(val) flag('f', "force", "Add anything") do (): options.add.force = true
Nesting is supported.
When a subcommand fires, there are two approaches to acting on it:
1. `run` handler
Register a command-handling hook for its level with run. This handler is called once right after that subcommand's parser loop finishes, while still inside the generated subcommand's proc. This is often a simpler way to customize the control flow.
Example:
import cozycliparser type Options = object filterCol, filterRe: string var options: Options buildParser("csvtool", "Cli", GnuMode): cmd("filter", "Filter rows by column value") do (): arg("COLUMN", "Column name") do (val: string): options.filterCol = val opt('r', "regex", "Match pattern", "RE") do (val: string): options.filterRe = val run do (): discard # act on `options`, call other procs, etc. cmd("version", "Prints version and exits") do (): run do (): quit("csvtool v0.1", 0)
Only one run handler is allowed per parser level. A run handler registered at the root level is called after the main parser loop finishes.
2. Global state
Track which subcommand was selected in a state variable and act on it after buildParser returns.
Example: cmd: -r:off
import cozycliparser type Cmd = enum cmdNone, cmdFilter Options = object cmd: Cmd filterCol, filterRe: string var options = Options() buildParser("csvtool", "Cli", GnuMode): cmd("filter", "Filter rows by column value") do (): arg("COLUMN", "Column name") do (val: string): options.filterCol = val options.cmd = cmdFilter opt('r', "regex", "Match pattern", "RE") do (val: string): options.filterRe = val case options.cmd of cmdFilter: echo options # act on `options` of cmdNone: display(Cli.help); quit(1) # show help and signal error
Default values
No built-in support for default options values is provided. However, Nim's default values for object fields enable this convenient pattern:
Example: cmd: -r:off
import cozycliparser from std/strutils import parseInt # Define default constants to use in help text that must be statically known const DefWidth = 2 DefHeight = 21 # Set object field defaults type Options = object width: int = DefWidth height: int = DefHeight proc validateNum(s: string): Natural = try: let i = parseInt(s) # raises ValueError on wrong input if i notin 0..100: raise newException(ValueError, "Value not in a valid range [0..100]:" & $i) except ValueError as e: quit("Error: " & e.msg, 1) var options = Options() buildParser("multiplier", "Cli", NimMode): opt('w', "width", "Width value. Default=" & $DefWidth, "W") do (n: string): options.width = validateNum(n) opt('h', "height", "Height value. Default=" & $DefHeight, "H") do (n: string): # `h` suppresses the short key for auto-injected help and a hint will be shown options.height = validateNum(n) echo options.width * options.height
Error handling
Unknown options are routed to the installed error handler. The default handler is installed automatically. It writes the offending option and the relevant help text to stderr, then exits with code 1. You can disable automatic error handling setting helpAuto in ParserConfig.
Override it with onError after the buildParser body:
buildParser("myapp", "Cli", GnuMode): ... onError do (e: ParseError): stderr.writeLine "Unknown option: ", e.key e.help.display(stderr) # styled help for the active subcommand level quit(1)
ParseError fields:
- key: option/argument name as seen on the command line (no leading dashes).
- val: associated value, or "" for flags and bare unknowns.
- path: active subcommand chain, e.g. "" at root or "remote add".
- help: HelpText for the active parser level.
There is one onError handler for all parser levels; e.path and e.help distinguish which level triggered the error.
Principle of operation
The macro buildParser:
- Walks the typed body AST recursively, following the nesting structure of command handling closures, and collects metadata.
- Injects const <helpName>: a zero-cost typedesc namespace with help templates for each level, each returning a HelpText.
- Declares module-level var bpCtx: BpContext holding both the handler seq and the error handler. The option registration procs (opt, flag, arg, run, onError) store their handlers there.
- Emits the passed body verbatim. cmd calls its closure immediately, populating that subcommand's handlers before parsing begins.
- Emits <subcmd>Cmd procs (innermost first), each with its own initOptParser + getopt loop and a run handler, if registered.
- Emits the root-level loop.
Types
ArgHandler = proc (key: string) {.closure.}
CmdRunHandler = proc () {.closure.}
ErrorHandler = proc (e: ParseError) {.closure.}
FlagHandler = proc () {.closure.}
Handler = object case of okOpt: onOpt*: OptHandler of okFlag: onFlag*: FlagHandler of okArg: onArg*: ArgHandler of okRun: onRun*: CmdRunHandler
- Runtime storage for one registered handler closure.
HelpPalette = array[HelpTag, tuple[fg: ForegroundColor, style: set[Style]]]
HelpTag = enum htPlain, ## whitespace, punctuation, "[options]" — never styled htProgName, ## "csvtool", "csvtool filter" in the usage line htSection, ## "Usage:", "Arguments:", "Commands:", "Options:" htArg, ## positional name/metavar in listings and usage: "COLUMN" htMetavar, ## value placeholder after an opt key: "FILE", "CHAR" htShortKey, ## short option form: "-v", "-s" htLongKey, ## long option form: "--verbose", "--output" htSubCmd ## subcommand name in command listing: "filter", "version"
HelpText = seq[HelpSpan]
OptHandler = proc (val: string) {.closure.}
OutStream = enum osStdout = "stdout", osStderr = "stderr"
- Output stream selector for use in ParserConfig fields.
ParseError = object key*: string val*: string path*: string help*: HelpText
-
Describes an unknown option encountered during parsing.
- key: option name as seen on the command line (no leading dashes).
- val: associated value, or "" for flags and bare unknowns.
- path: active subcommand chain, e.g. "" at root or "remote add".
- help: HelpText for the active parser level. Call $e.help for a plain string or e.help.display(stderr) for styled output.
ParserConfig = object helpPrefix*: string = "" ## A header prepended to all help strings helpAuto*: bool = true ## inject -h/--help at every level unless overridden helpFlag*: (char, string) = ('h', "help") helpText*: string = "Show this help and exit" helpStream*: OutStream = osStdout helpExitCode*: int = 0 useColors*: bool = true errorExitCode*: int = 1 errorShowHelp*: bool = true ## display help on unknown input errorStream*: OutStream = osStderr fmtIndent*: int = 2 fmtColSep*: int = 2 ## minimal help text alignment shift palette*: HelpPalette = [(fgDefault, {}), (fgDefault, {}), (fgYellow, {styleDim}), (fgCyan, {styleBright}), (fgCyan, {}), (fgGreen, {styleBright}), (fgBlue, {styleBright}), (fgMagenta, {styleBright})] debug*: bool = false ## print the expanded macro AST at compile time
- Compile-time configuration for macro buildParser. Use parseConfig proc to selectively override the defaults.
Consts
DefaultPalette: HelpPalette = [(fgDefault, {}), (fgDefault, {}), (fgYellow, {styleDim}), (fgCyan, {styleBright}), (fgCyan, {}), (fgGreen, {styleBright}), (fgBlue, {styleBright}), (fgMagenta, {styleBright})]
Procs
proc arg(name, help: string; handler: ArgHandler) {....raises: [], tags: [], forbids: [].}
-
Registers a positional argument handler. Multiple arg calls are allowed per parser level.
Parsed tokens are dispatched to handlers consecutively, in registration order, with the last handler receiving all overflow tokens.
name is the placeholder shown in usage lines.
proc cmd(name, help: string; cmdRegistrations: proc ()) {....raises: [Exception], tags: [RootEffect], forbids: [].}
-
Declares a command and registers handlers for it and its options and arguments. This conceptually declares a parsing level, not a command handler. The cmdRegistrations closure is called immediately, not when the command is met during parsing.
To act on a command during parsing, use proc run. Use arg, flag, opt, run or cmd itself inside cmdRegistrations, as you do at the root parser level.
proc display(doc: HelpText; f: File = stdout) {....raises: [IOError], tags: [ReadEnvEffect, WriteIOEffect], forbids: [].}
- display(HelpText,HelpPalette,File) overload using DefaultPalette.
proc display(doc: HelpText; palette: HelpPalette; f: File = stdout) {. ...raises: [IOError], tags: [ReadEnvEffect, WriteIOEffect], forbids: [].}
-
Checks if f can and is allowed to emit colored output and, correspondingly, displays doc styled with palette or as a plain text.
Respects the NO_COLOR and CLICOLOR_FORCE environmental variables.
proc flag(short: char; name, help: string; handler: FlagHandler) {....raises: [], tags: [], forbids: [].}
-
Registers a boolean flag (--name / -s), fired with no value.
Pass '\0' for short to omit the short form; "" for name to omit the long form.
proc onError(handler: ErrorHandler) {....raises: [], tags: [], forbids: [].}
-
Installs a custom handler for unknown options across all parser levels. Call once after macro buildParser. The same handler receives errors from any depth; inspect e.path and e.help to distinguish the level.
The default handler writes the unknown option and the relevant help text to stderr, then exits with code 1.
proc opt(short: char; name, help, metavar: string; handler: OptHandler) {. ...raises: [], tags: [], forbids: [].}
-
Registers a key-value option (--name=val / -s val).
Pass '\0' for short to omit the short form; "" for name to omit the long form.
metavar is the value placeholder in usage lines.
proc parseConfig(helpPrefix = ""; helpAuto = true; helpFlag = ('h', "help"); helpText = "Show this help and exit"; helpStream = osStdout; helpExitCode = 0; useColors = true; errorExitCode = 1; errorShowHelp = true; errorStream = osStderr; fmtIndent = 2; fmtColSep = 2; palette = [(fgDefault, {}), (fgDefault, {}), (fgYellow, {styleDim}), (fgCyan, {styleBright}), (fgCyan, {}), (fgGreen, {styleBright}), (fgBlue, {styleBright}), (fgMagenta, {styleBright})]; debug = false): ParserConfig {.compiletime, ...raises: [], tags: [], forbids: [].}
proc run(handler: CmdRunHandler) {....raises: [], tags: [], forbids: [].}
-
Registers a closure called once after this parser level's loop finishes. Use it to perform actions in a command's own context immediately after its arguments have been parsed, without tracking state externally.
Only one run handler is allowed per parser level.
Macros
macro buildParser(cfg: static ParserConfig; progName, helpName: static string; mode: static CliMode; body: typed): untyped
-
Generates a complete CLI parser from a declarative body. Only arg, cmd, flag, opt and run calls are allowed inside.
Injects one symbol into outer scope:
- const <helpName>: a compile-time namespace where each .help property returns a HelpText. Use $ for a plain string or display for styled output.
Templates
template buildParser(progName, helpName: static string; mode: static CliMode; body: typed): untyped
- Convenience overload using the default ParserConfig.
template command(name, help: string; body: untyped)