cozycliparser: Command-Line Parser Builder
| Version: | 0.3.1 |
|---|
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.
- Multiple buildParser calls in one module are supported.
- 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 token 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(parserConfig(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.help("greet") == """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 typed token. Access help as Cli.help (root level) or Cli.help("sub cmd") (subcommand). Call $Cli.help for a plain string, or Cli.help.display(<fd>) for styled output.
Accessing help
After buildParser, help is available through the injected constant named by the helpName argument ("Cli" in the examples). Call help on it to get the root-level HelpView, or pass a subcommand path string to reach a nested level:
Example: cmd: -r:off
import cozycliparser buildParser("tool", "Cli", GnuMode): flag('v', "verbose", "Enable verbose output") do (): discard discard Cli.help # root-level HelpView discard Cli.help("sub") # subcommand HelpView (returns root's if unknown) discard Cli.help("remote add") # nested subcommand HelpView discard $Cli.help # plain string Cli.help.display() # styled output to stdout Cli.help.display(stderr) # styled output to stderr
If you need to reference the token before buildParser runs -- for example, to define a helper proc that displays help and is called from inside a handler closure -- declare the token first with setParser:
Example: cmd: -r:off
import cozycliparser setParser("Cli") # Cli is now in scope. Its help storage is empty until buildParser runs. proc showHelp() = Cli.help.display(stderr) buildParser("tool", Cli, GnuMode): arg("FILE", "Input file") do (val: string): if val.len == 0: showHelp(); quit(1)When using setParser explicitly, pass the token directly to buildParser -- do not pass the string name, as that would call setParser a second time and redeclare the token.
Help string interpolation
Because help text is built at compile time, injecting dynamic runtime values (like the current directory or an environment variable) into help descriptions is done via a lazy interpolation (string replacement) hook.
Register an interpolator inside the macro buildParser body using setHelpInterpolator. The closure is invoked exactly when the help text is converted to a string ($) or displayed.
The interpolator must be registered inside the body (not after buildParser returns) so that it is in place before the parser loop runs and can fire if -h is passed.
Only plain-text help spans (the optional helpPrefix and description paragraphs) are passed through the interpolator. Left-column syntax (keys, arg names, metavars, section names) is not interpolated.
Example:
import cozycliparser import std/strutils let currentDir = "/tmp" # simulates os.getCurrentDir(); captured by closure below buildParser(parserConfig(helpPrefix = "MyProg $ver"), "myprog", "Cli", GnuMode): opt('d', "dir", "Target directory (default: $dir)", "PATH") do (_: string): discard Cli.setHelpInterpolator: s.multiReplace( ("$ver", "v1.2.3"), ("$dir", currentDir) ) doAssert $Cli.help == """ MyProg v1.2.3 Usage: myprog [options] Options: -d, --dir=PATH Target directory (default: /tmp) -h, --help Show this help and exit"""The registered interpolator fires once per plain-text span, so calls inside it are evaluated repeatedly. Precompute and cache any expensive values in variables before the closure captures them, as shown above.
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 closure:
Example: cmd: -r:off
import cozycliparser type Options = object addQueue: seq[string] force: bool var options: Options 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.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 with run. It is called once right after that subcommand's parser loop finishes.
Example: cmd: -r:off
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:
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 of cmdNone: discard # Cli.help.display(); quit(1) # goes here
Default values
No built-in support for default option values is provided. Nim's default values for object fields enable this convenient pattern:
Example:
import cozycliparser from std/strutils import parseInt const DefWidth = 2 DefHeight = 21 type Options = object width: int = DefWidth height: int = DefHeight proc validateNum(s: string): Natural = try: let i = parseInt(s) if i notin 0..100: raise newException(ValueError, "Value not in 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` shadows the short key for auto-injected help; a hint will be shown. options.height = validateNum(n) doAssert options.width * options.height == 42
Error handling
Unknown options are routed to the installed error handler. The default handler writes the offending option and the relevant help text to stderr, then exits with code 1.
Override it with proc onError:
Example: cmd: -r:off
import cozycliparser buildParser("myprog", "Cli", GnuMode): flag('v', "verbose", "Enable verbose output") do (): discard Cli.onError do (e: ParseError): stderr.writeLine "Unknown option: ", e.key e.help.display(stderr) 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 per parser; e.path and e.help distinguish which level triggered the error.
Principle of operation
buildParser does the following:
- Walks the body AST recursively, following cmd closure nesting, collecting opt/flag/arg/run/cmd metadata into a Scope tree.
- Emits one const <sym>: HelpText per parser level (compile-time constant).
- Populates the per-token storage's helpMap at runtime so tok.help(path) can dispatch to the right HelpText by path string.
- Resets the active registration context, executes the body (registering handlers), then commits the context into permanent per-token storage.
- Installs the default error handler.
- Emits <subcmd>Cmd procs (innermost first), each with its own initOptParser + getopt loop and run handler.
- Emits the root-level loop.
Per-token storage is keyed on a unique phantom type gensym'd by setParser. Two modules both using "Cli" produce tokens with distinct phantom types and therefore distinct storage instantiations, so they never collide.
Types
ArgHandler = proc (key: string) {.closure.}
- Source Edit
CliHelp[Tag; Id] = object
- Phantom token minted once per setParser call. Tag is the user-visible name; Id is a unique gensym'd type that isolates per-call storage so same-named tokens from different modules never collide. Source Edit
CmdRunHandler = proc () {.closure.}
- Source Edit
ErrorHandler = proc (e: ParseError) {.closure.}
- Source Edit
FlagHandler = proc () {.closure.}
- Source Edit
Handler = object case kind*: OptKind of okOpt: onOpt*: OptHandler of okFlag: onFlag*: FlagHandler of okArg: onArg*: ArgHandler of okRun: onRun*: CmdRunHandler
- Source Edit
HelpPalette = array[HelpTag, tuple[fg: ForegroundColor, style: set[Style]]]
- Source Edit
HelpTag = enum htPlain, ## whitespace, punctuation, "[options]" - never styled htProgName, ## program name in the usage line htSection, ## "Usage:", "Arguments:", "Commands:", "Options:" htArg, ## positional name/metavar in listings and usage 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
- Semantic tags applied to help text spans for styling purposes. htPlain spans are passed through the interpolator; all others are not. Source Edit
HelpView[Tag; Id] = object doc*: HelpText
- A HelpText bound to a specific parser token. $ and display on a HelpView automatically apply the interpolator registered on the same token, unlike calling them on a bare HelpText. Source Edit
OptHandler = proc (val: string) {.closure.}
- Source Edit
OutStream = enum osStdout = "stdout", osStderr = "stderr"
- Output stream selector for use in ParserConfig fields. Source Edit
ParseError = object key*: string ## option name as seen on the command line (no leading dashes) val*: string ## associated value, or `""` for flags and bare unknowns path*: string ## active subcommand chain, e.g. `""` at root or `"remote add"` help*: HelpText ## help page for the active parser level
- Describes an unknown option or argument encountered during parsing. Source Edit
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 parserConfig proc to selectively override the defaults. Source Edit
Consts
DefaultPalette: HelpPalette = [(fgDefault, {}), (fgDefault, {}), (fgYellow, {styleDim}), (fgCyan, {styleBright}), (fgCyan, {}), (fgGreen, {styleBright}), (fgBlue, {styleBright}), (fgMagenta, {styleBright})]
- Source Edit
Procs
proc arg(name, help: string; handler: ArgHandler) {....raises: [], tags: [], forbids: [].}
- Registers a positional argument handler. Multiple arg calls are allowed per parser level. Tokens are dispatched in registration order; the last handler absorbs overflow. Source Edit
proc cmd(name, help: string; cmdRegistrations: proc ()) {....raises: [Exception], tags: [RootEffect], forbids: [].}
-
Declares a subcommand. cmdRegistrations is called immediately to register the subcommand's own options, flags, args and nested commands, not when the command is met during parsing.
This, conceptually, declares a parsing level, not a command handler.
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.
Source Edit proc display(doc: HelpText; f: File = stdout; palette = DefaultPalette; interp: proc (s: string): string = nil) {. ...raises: [IOError, Exception], tags: [ReadEnvEffect, WriteIOEffect, RootEffect], forbids: [].}
- Writes doc styled if the terminal supports it, plain otherwise. Respects NO_COLOR and CLICOLOR_FORCE. Source Edit
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.
Source Edit proc helpInterpolator[Tag: static string; Id](token: CliHelp[Tag, Id]; handler: proc (s: string): string)
- Installs a custom interpolator for help text descriptions and prefixes. Evaluated lazily when help text is converted to string or displayed. Source Edit
proc onError[Tag: static string; Id](token: CliHelp[Tag, Id]; handler: ErrorHandler)
-
Installs a custom unknown-option handler. One handler serves all parser levels. 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.
Source Edit 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. Source Edit
proc parserConfig(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: [].}
- Source Edit
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.
Source Edit
Macros
macro setParser(helpName: static string): untyped
-
Mints a unique phantom type for this parser call and injects const <helpName>: CliHelp[<helpName>, <UniqueId>] into the outer scope.
The common case does not require calling setParser directly; the string-name buildParser overloads call it automatically. Use setParser explicitly only when you need to reference the token before buildParser runs - for example, to use inside functions that require help display and are called from inside the handler closures.
After setParser, helpName becomes a valid symbol, but its help storage is empty until macro buildParser populates it.
Pass the helpName symbol directly to the token-taking buildParser overload, don't pass the string name again, as that would redeclare the symbol.
Source Edit
Templates
template buildParser(cfg: static ParserConfig; progName, helpName: static string; mode: static CliMode; body: untyped): untyped
- Unified single-call overload. Equivalent to setParser(helpName) + buildParser(cfg, progName, <token>, mode, body). Injects const <helpName> into the outer scope. Source Edit
template buildParser(progName, helpName: static string; mode: static CliMode; body: untyped): untyped
- Convenience overload using the default ParserConfig. Source Edit
template buildParser(progName: static string; token: CliHelp; mode: static CliMode; body: typed): untyped
- Convenience overload using the default ParserConfig. Source Edit
template help[Tag: static string; Id](token: CliHelp[Tag, Id]; path: string = ""): HelpView[ Tag, Id]
- Returns the HelpView for path ("" = root level). If path is not found, returns the root help page. The view carries the token identity so $ and display apply the registered interpolator automatically. Source Edit
template setHelpInterpolator[Tag: static string; Id](token: CliHelp[Tag, Id]; body: untyped)
- Convenience wrapper for helpInterpolator. Injects s for the closure's string parameter. Source Edit