cozycliparser

Search:
Group by:

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"""
Important: By default (ParserConfig.helpAuto = true), a -h/--help flag is auto-injected at every parser level, writing help to stdout and calling quit(0). Registering a flag that conflicts with the configured short or long key at a given level suppresses that key or whole auto-injection (if both were shadowed) with a hint or a warning.

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
Note: "bare" subcommands with only a run handler and no arg, opt, or flag registrations do not receive a namespace entry. Accessing <helpName>.<subcmd>.help for such a subcommand will not compile. The default auto-generated help shows the help text of such command's parent level.

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:

  1. Walks the typed body AST recursively, following the nesting structure of command handling closures, and collects metadata.
  2. Injects const <helpName>: a zero-cost typedesc namespace with help templates for each level, each returning a HelpText.
  3. 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.
  4. Emits the passed body verbatim. cmd calls its closure immediately, populating that subcommand's handlers before parsing begins.
  5. Emits <subcmd>Cmd procs (innermost first), each with its own initOptParser + getopt loop and a run handler, if registered.
  6. 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.

Vars

bpCtx {.global.}: BpContext
A global storage for option and error handlers.

Consts

DefaultPalette: HelpPalette = [(fgDefault, {}), (fgDefault, {}),
                               (fgYellow, {styleDim}), (fgCyan, {styleBright}),
                               (fgCyan, {}), (fgGreen, {styleBright}),
                               (fgBlue, {styleBright}),
                               (fgMagenta, {styleBright})]

Procs

proc `$`(doc: HelpText): string {....raises: [], tags: [], forbids: [].}
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.

proc write(f: File; doc: HelpText) {....raises: [IOError], tags: [WriteIOEffect],
                                     forbids: [].}
Writes doc unstyled.

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 argreg(name, help: string; body: untyped)
Convenience wrapper for arg. Injects key for the parsed argument.
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)
template flagreg(short: char; name, help, metavar: string; body: untyped)
Convenience wrapper for flag.
template optreg(short: char; name, help, metavar: string; body: untyped)
Convenience wrapper for opt. Injects val for the parsed value.
template runreg(body: untyped)
Convenience wrapper for run.