cozycliparser

Search:
Group by:
Source   Edit  

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"""
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 the 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 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
Note: "bare" subcommands with only a run handler and no arg, opt, or flag registrations do not receive a help entry. Accessing their path via Cli.help("path") returns the help text of their parent level.

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.

Warning: Adding new lines to the interpolated strings will break formatting!

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:

  1. Walks the body AST recursively, following cmd closure nesting, collecting opt/flag/arg/run/cmd metadata into a Scope tree.
  2. Emits one const <sym>: HelpText per parser level (compile-time constant).
  3. Populates the per-token storage's helpMap at runtime so tok.help(path) can dispatch to the right HelpText by path string.
  4. Resets the active registration context, executes the body (registering handlers), then commits the context into permanent per-token storage.
  5. Installs the default error handler.
  6. Emits <subcmd>Cmd procs (innermost first), each with its own initOptParser + getopt loop and run handler.
  7. 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  
HelpSpan = object
  text*: string
  tag*: HelpTag
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  
HelpText = seq[HelpSpan]
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  

Vars

bpActive {.threadvar.}: BpContext
Scratch context written by registration procs during buildParser body execution. Committed into bpStorage by the macro after the body runs. Source   Edit  

Consts

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

Procs

proc `$`(doc: HelpText): string {....raises: [], tags: [], forbids: [].}
Converts doc to a plain string by concatenating all span text. The registered interpolator is NOT applied; use $ on a HelpView (returned by help) to get interpolated output. Source   Edit  
proc `$`[Tag: static string; Id](v: HelpView[Tag, Id]): string
Converts v to a plain string, applying the registered interpolator. Source   Edit  
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 display[Tag: static string; Id](v: HelpView[Tag, Id]; f: File = stdout;
                                     palette = DefaultPalette)
Displays v styled (or plain), applying the registered interpolator. 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  
proc write(f: File; doc: HelpText; interp: proc (s: string): string = nil) {.
    ...raises: [IOError, Exception], tags: [WriteIOEffect, RootEffect], forbids: [].}
Source   Edit  

Macros

macro buildParser(cfg: static ParserConfig; progName: static string;
                  token: CliHelp; mode: static CliMode; body: typed): untyped
Generates a complete CLI parser from a declarative body. token is the token injected by setParser. Only opt, flag, arg, run and cmd calls are allowed in body. Source   Edit  
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 argreg(name, help: string; body: untyped)
Convenience wrapper for arg. Injects key for the parsed argument. Source   Edit  
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 command(name, help: string; body: untyped)
Convenience wrapper for cmd. Source   Edit  
template flagreg(short: char; name, help: string; body: untyped)
Convenience wrapper for flag. 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 optreg(short: char; name, help, metavar: string; body: untyped)
Convenience wrapper for opt. Injects val for the parsed value. Source   Edit  
template runreg(body: untyped)
Convenience wrapper for run. 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