src/therapist

Search:
Group by:

Therapist - for when commands and arguments are getting you down

A simple to use, declarative, type-safe command line parser, with beautiful help messages and clear errors, suitable for simple scripts and complex tools.

Therapist allows you to use a carefully constructed tuple to specify how you want your commandline arguments to be parsed. Each value in the tuple must be set to a <Type>Arg of the appropriate type, which specifies how that argument will appear, what values it can take and provides a help string for the user.

Features:

  • Type-safe - Therapist will check that supplied values are of the expected type and makes defining your own types easy
  • Approachable - Therapist provides only one macro which you may never need to use
  • Powerful - Therapist has patterns for both single-file scripts and scripts with multiple commands split between files (e.g. git)
  • Flexible - Supports defaults, choices, using default values from environment variables, options that are not shown in help messages, required options and optional arguments
  • Batteries-included - Generates beautiful (short and long-form) help messages, though you are free to roll your own (also provides fish-shell completions)
  • Conventional - Follows standard conventions: --yes/no or --[no-]format as variants imply what you would expect
  • Helpful - Intelligent suggestions git blme -> did you mean git blame?
  • Tested - Therapist has lots of tests, many of which function as usage examples

A simple 'Hello world' example:

import therapist

# The parser is specified as a tuple
let spec = (
    # Name is a positional argument, by virtue of being surrounded by < and >
    name: newStringArg(@["<name>"], help="Person to greet"),
    # --times is an optional argument, by virtue of starting with - and/or --
    times: newIntArg(@["-t", "--times"], defaultVal=1, help="How many times to greet"),
    # --version will cause 0.1.0 to be printed
    version: newMessageArg(@["--version"], "0.1.0", help="Prints version"),
    # --help will cause a help message to be printed
    help: newHelpArg(@["-h", "--help"], help="Show help message"),
)
# `args` and `command` are included in tests but would normally be picked up from the commandline
spec.parseOrQuit(prolog="Greeter", args="-t 2 World", command="hello")
# If a help message or version was requested or a parse error generated it would be printed
# and then the parser would call `quit`. Getting past `parseOrQuit` implies we're ok.
# `spec` has now been modified to reflect the supplied arguments
for i in 1..spec.times.value:
    echo "Hello " & spec.name.value

doAssert spec.name.seen
doAssert spec.name.value == "World"
doAssert spec.times.seen
doAssert spec.times.value == 2

The above parser generates the following help message

Greeter

Usage:
  hello <name>
  hello (--version | -h | --help)

Arguments:
  <name>               Person to greet

Options:
  -t, --times=<times>  How many times to greet [default: 1]
  --version            Prints version
  -h, --help           Show help message

The constructor for each <Type>Arg type takes the form:

# doctest: skip
proc newStringArg*(variants: seq[string], help: string, defaultVal="", choices=newSeq[string](),
                    helpvar="", required=false, optional=false, multi=false, env="", helpLevel=0)
  • Every argument must be declared with one or more variants. There are three types of argument:
    • Positional Arguments are declared in variants as <value> whose value is determined by the order of arguments provided. They are required unless optional=true
    • Optional Arguments are declared in variants as -o (short form) or --option (long form) which may take an argument or simply be counted. They are optional unless required=true
    • Commands (declared in variants as command) are expected to be entered by the user as written. The remainder of the arguments are parsed by a subparser which may have a different specification to the main parser
  • Options may be interleaved with arguments, so > markup input.txt -o output.html is the same as > markup -o output.html input.txt
  • Options that take a value derive from ValueArg and may be entered as -o <value>, -o:<value> or -o=<value> (similarly for the long form i.e. --option <value> etc). Short options that do not take a value may be repeated, e.g. -vvv and short options can take values without a separator e.g. -o<value>
  • A CountArg is a special type of Arg that counts how many times it is seen, without taking a value (sometimes called a flag).
  • CountArg also allows some special variant formats. If you specify --[no]option, then --option will count upwards (args.count>0) and --nooption will count downwards (args.count<0). Alternatively -y/-n or --yes/--no will count upwards for -y or --yes and downwards for -n or --no. Note that args.seen will return true if args.count!=0.
  • If a command is seen, parsing will switch to that command immediately. So in > pal --verbose push --force, the base parser receives --verbose, and the push command parser receives --force
  • If an argument has been seen arg.seen will return true. The values will also be entered into a values seq, with the most recently seen value stored in value. The number of times the argument has been seen can be found in arg.count
  • If -- is seen, the remainder of the arguments will be taken to be positional arguments, even if they look like options or commands
  • A defaultVal value may be provided in case the argument is not seen. Additionally an env key can be provided (e.g. env=USER). If env is set to a key that is set in the environment, the default value will be set to that value e.g. $USER).
  • Arguments are expected to be seen at most once, unless multi=true
  • If there are only a set number of acceptable values for an argument, they can be listed in choices
  • A helpvar may be provided for use in the autogenerated help (e.g. helpvar="n" would lead to a help message saying --number=<n>)
  • Within the help message, arguments are usually grouped into Commands, Arguments and Options. If you want to group them differently, use the group parameter to define new groups. Groups and arguments will be shown the order that they are appear in the tuple definition.
  • If helpLevel is set to a value x greater than 0 the argument will only be shown in a help message if the HelpArg is defined showLevel set to a value greater than or equal to x
  • If you want to define a new ValueArg type defineArg is a macro that will fill in the boilerplate for you

Argument types provided out of the box

  • ValueArg - base class for arguments that take a value
    • StringArg - expects a string
    • IntArg - expects an integer
    • FloatArg - expects a floating point number
    • BoolArg - expects a boolean (on/off, true/false)
    • FileArg - expects a string argument that must point to an existing file
    • DirArg - expects a string argument that must point to an existing directory
    • PathArg - expects a string that must point to an existing file or directory
  • CountArg - expects no value, simply counts how many times the argument is seen
  • HelpArg - if seen, prints an auto-generated help message
  • FishCompletionArg - if seen, prints a fish completion script
  • MessageArg - if seen, prints a message (e.g. version number)

Creating your own argument type

Creating your own ValueArg is as simple as defining a parse method that turns a string into a value of an appropriate type (or raises a ValueError for invalid input). Suppose we want to create a DateArg type that only accepts ISO-formatted dates:

import therapist
import times

let DEFAULT_DATE = initDateTime(1, mJan, 2000, 0, 0, 0, 0)
proc parseDate(value: string): DateTime = parse(value, "YYYY-MM-dd")
defineArg[DateTime](DateArg, newDateArg, "date", DateTime, parseDate, DEFAULT_DATE)

Now we can call newDateArg to ask the user to supply a date

Examples

At the other extreme, you can create complex parsers with subcommands (the example below may be familiar to those who have seen docopt.nim). Note that the help message is slightly different; this is in part because parser itself is stricter. For example, --moored is only valid inside the mine subcommand, and as such, will only appear in the help for that command, shown if you run navel_fate mine --help.

import options
import strutils
import therapist

let prolog = "Navel Fate."

let create = (
      name: newStringArg(@["<name>"], multi=true, help="Name of new ship")
)
let move = (
      name: newStringArg(@["<name>"], help="Name of ship to move"),
      x: newIntArg(@["<x>"], help="x grid reference"),
      y: newIntArg(@["<y>"], help="y grid reference"),
      speed: newIntArg(@["--speed"], defaultVal=10, help="Speed in knots"),
      help: newHelpArg()
)
let shoot = (
      x: newIntArg(@["<x>"], help="Name of new ship"),
      y: newIntArg(@["<y>"], help="Name of new ship"),
      help: newHelpArg()
)
let state = (
      moored: newCountArg(@["--moored"], help="Moored (anchored) mine"),
      drifting: newCountArg(@["--drifting"], help="Drifting mine"),
)
let mine = (
      action: newStringArg(@["<action>"], choices = @["set", "remove"], help="Action to perform"),
      x: newIntArg(@["<x>"], help="Name of new ship"),
      y: newIntArg(@["<y>"], help="Name of new ship"),
      state: state,
      help: newHelpArg()
)
let ship = (
      create: newCommandArg(@["new"], create, help="Create a new ship"),
      move: newCommandArg(@["move"], move, help="Move a ship"),
      shoot: newCommandArg(@["shoot"], shoot, help="Shoot at another ship"),
      help: newHelpArg()
)
let spec = (
      ship: newCommandArg(@["ship"], ship, help="Ship commands"),
      mine: newCommandArg(@["mine"], mine, help="Mine commands"),
      help: newHelpArg()
)

let (success, message) = spec.parseOrMessage(prolog="Navel Fate.", args="--help", command="navel_fate")

let expected = """
Navel Fate.

Usage:
  navel_fate ship new <name>...
  navel_fate ship move <name> <x> <y>
  navel_fate ship shoot <x> <y>
  navel_fate mine (set|remove) <x> <y>
  navel_fate (-h | --help)

Commands:
  ship        Ship commands
  mine        Mine commands

Options:
  -h, --help  Show help message""".strip()

doAssert success and message.isSome
doAssert message.get == expected

Many more examples are available in the source code and in the nimdoc for the various functions.

Advanced

No parsing framework can have the full flexibility of a hand-written parser. Thus not all posix utilities can be expressed with a fixed grammar. Suppose we want to implement something like xargs, so we want to be able to capture arbitrary commands vertbatim. Here, we can use -- to terminate parsing and have the rest of the arguments be collected (therapist cannot gather arbitrary arguments and options verbatim without parsing them).

import strutils
import therapist

let prolog = "Therapist xargs"

let spec = (
  command:   newStringArg("<command>", "Command to run"),
  arguments: newStringArg("<arguments>", "Any arguments for the command", multi=true, optional=true),
  verbose:   newFlagArg("-v, --verbose", "Show command being run"),
  help:      newHelpArg("-h, --help")
)

let (success, message) = spec.parseOrMessage(prolog, args="-v -- tar -0 -cvzf files.tar.gz", command="targs")

if not success:
  echo message.get

doAssert spec.verbose.seen
doAssert spec.command.value == "tar"
doAssert spec.arguments.values == @["-0", "-cvzf", "files.tar.gz"]

If however you are writing a utility and you want to collect any extra options or arguments, either because their values are unpredictable or because you want to parse them yourself, you can use parseWithExtras (similar to python's parse_known_args()), but note that this supports the full option parsing funtionality of therapist (i.e. -abcd will be parsed as -a, -b, -c and -d as per the example below). Note that options without values will be parsed as CountArgs, but options with values will be captured as StringArgs.

import tables

import therapist

let prolog = "Sets colours for your ships in navel fate"

let spec = (
  help: newHelpArg("-h, --help")
)

let args = "historical -ab --victory=red --mayflower=blue --hind=golden --extended-colours"

let extras = spec.parseWithExtras(prolog, command="set_ship_colours", args=args)

doAssert extras.extra_args.value == "historical"
doAssert "--extended-colours" in extras.extra_options
doAssert "-a" in extras.extra_options
doAssert "-b" in extras.extra_options
doAssert StringArg(extras.extra_options["--victory"]).value == "red"
doAssert StringArg(extras.extra_options["--mayflower"]).value == "blue"
doAssert StringArg(extras.extra_options["--hind"]).value == "golden"

Possible features therapist does not have

In rough order of likelihood of being added:

  • Ints and floats being limited to a range rather than a set of discrete values
  • Support for +w and -w to equate to w=true and w=false
  • Generation of bash / powershell completion scripts
  • Dependent option requirements i.e. because --optionA appears, --optionB is required
  • Case/style insensitive matching
  • Partial matches for commands i.e. pal pus is the same as pal push, if that is the only unambiguous match
  • Support for alternate option characters (e.g. /) or different option semantics (e.g. java-style single - -option)

Installation

Clone the repository and then run:

> nimble install

Or simply:

> nimble install therapist

Contributing

The code lives on bitbucket. Pull requests (with tests) and bug reports welcome!

Compatibility

For now, tests are run against the latest stable version of the 1.0.x, 1.2.x, 1.4.x and 1.6.x branches. Dropping support for earlier verions may be considered for future releases, but only if it adds meaningful functionality / simplicity / maintainability. Note that --orc and --arc are not supported before nim version 2.x due to issues in the peg module from the standard library.

Internally, Therapist uses CamelCase.

License

This library is made available under the LGPL. Use it to make any software you like, open source or not, but if you make improvements to therapist itself, please contribute them back.

Alternatives and prior art

This is therapist. There are many argument parsers like it, but this one is mine. Which one you prefer is likely a matter of taste. If you want to explore alternatives, you might like to look at:

  • parseopt - for if you like to parse your args as they are flung at you, old school style
  • nim-argparse - looks nice, but heavy use of macros, which makes it a little too magic for me
  • docopt.nim - you get to craft your help message, but how you use the results (and indeed what the spec actually means) has always felt inscrutable to me
  • cligen - the fastest way to generate a commandline parser if you already have the function you want (think argh from python for nim). More complex use cases look a bit less elegant to my eyes, but you're still going to be winning the code golf competition

Changes

0.3.0

  • Breaking: Use a macro for defineArg to allow generated constructors to have documentation (requires ArgType parameter)
  • Breaking: Pass specification parameter to register method.
  • Generate completion scripts for fish shell
  • Options and arguments that can be repeated show ... in their help text
  • Add newFlagArg as an alias for newCountArg with multi=false
  • Support paragraph-style help output by setting helpStyle=HelpStyle.hsParagraphs on help args, using longHelp parameter if present
  • Show HelpArgs and MessageArgs all in one usage example
  • Show [choices: high|medium|low] hint in help for options
  • Add parseWithExtras to allow capturing unspecified arguments and options

0.2.0 2022-07-16

  • Breaking: Switch to using defaultVal consistently everywhere (previously, some used default)
  • Add parseCopy to get back a copy of your specification rather than a modified one
  • Add parseOrHelp to show both error and help message on ParseError (@squattingmonk)
  • Add support for --[no-]colour as well as --[no]colour (idea from @squattingmonk)
  • Added convenience versions of newXXXArg where variants can be provided as a comma-separated string
  • Add newHelpCommandArg and newMessageCommandArg

0.1.0 2020-05-23

Initial release

Types

Arg = ref object of RootObj
  ## The help string for the argument
  ## Longer version of help for the argument
  count*: int                ## How many times the argument was seen
  ## Set to true to make an option required
  ## Set to true to make a positional argument optional
  ## Set to true to allow the argument to appear more than once
  ## The name of an environment variable to use as a default value
  ## The name of a variable to use as an example name in help messages
  ## The group of help messages the argument should appear in
  ## The help level that governs when the argument is shown
  
Base class for arguments
ArgError = object of CatchableError
  nil
Base Exception for module
BoolArg {.inject.} = ref object of ValueArg
  value*: bool
  values*: seq[bool]
  
CommandArg = ref object of Arg
  specification*: Specification
  
CommandArg represents a subcommand, which will be processed with its own parser
CountArg = ref object of Arg
  
Counts the number of times this argument appears
DirArg {.inject.} = ref object of ValueArg
  value*: string
  values*: seq[string]
  
FileArg {.inject.} = ref object of ValueArg
  value*: string
  values*: seq[string]
  
FloatArg = ref object of ValueArg
  value*: float
  values*: seq[float]
  
An argument or option whose value is a float
HelpArg = ref object of CountArg
  
If this argument is provided, a MessageError containing a help message will be raised
HelpCommandArg = ref object of CommandArg
  
HelpCommandArg allows you to create a command that prints help
HelpStyle = enum
  hsColumns,                ## Variants and help text shown on the same line
  hsParagraphs               ## Variants and help text shown on different lines
IntArg = ref object of ValueArg
  value*: int
  values*: seq[int]
  
An argument or option whose value is an int
MessageArg = ref object of CountArg
  
If this argument is provided, a MessageError containing a message will be raised
MessageCommandArg = ref object of CommandArg
  
MessageCommandArg allows you to create a command that prints a message
MessageError = object of ArgError
  nil
Indicates parsing ended early (e.g. because user asked for help). Expected behaviour is that the exception message will be shown to the user and the program will terminate indicating success
ParseError = object of ArgError
  nil
 Indicates parsing ended early (e.g. because user didn't supply correct options). Expected behaviour is that the exception message will be shown to the user and the program will terminate indicating failure.
PathArg {.inject.} = ref object of ValueArg
  value*: string
  values*: seq[string]
  
PromptArg = ref object of Arg
  
Base class for arguments whose value is read from a prompt not an argument
SpecificationError = object of Defect
  nil
 Indicates an error in the specification. This error is thrown during an attempt to create a parser with an invalid specification and as such indicates a programming error
StringArg = ref object of ValueArg
  value*: string
  values*: seq[string]
  
An argument or option whose value is a string
StringPromptArg = ref object of PromptArg
  value*: string
  values*: seq[string]
  
URLArg {.inject.} = ref object of ValueArg
  value*: Uri
  values*: seq[Uri]
  
ValueArg = ref object of Arg
  nil
Base class for arguments that take a value

Procs

proc cloneArg[A: Arg](arg: A): A
proc initArg[A, T](arg: var A; variants: seq[string]; help: string;
                   longHelp: string; defaultVal: T; choices: seq[T];
                   helpVar = ""; group = ""; required: bool; optional: bool;
                   multi: bool; env: string; helpLevel: Natural)

If you define your own ValueArg type, you can call this function to initialise it. It copies the parameter values to the ValueArg object and initialises the value field with either the value from the env environment key (if supplied and if the key is present in the environment) or defaultVal

Since: 0.1.0

proc initMessageArg[MA](arg: var MA; variants: seq[string]; help: string;
                        longHelp: string; group = ""; helpLevel: Natural = 0)
TODO: Rename me
proc newBoolArg(variants`gensym148: seq[string]; help`gensym148: string;
                longHelp`gensym148 = ""; defaultVal`gensym148 = false;
                choices`gensym148 = newSeq[bool](); helpvar`gensym148 = "";
                group`gensym148 = ""; required`gensym148 = false;
                optional`gensym148 = false; multi`gensym148 = false;
                env`gensym148 = ""; helpLevel`gensym148: Natural = 0): BoolArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be a boolean
proc newBoolArg(variants`gensym148: string; help`gensym148: string;
                longHelp`gensym148 = ""; defaultVal`gensym148 = false;
                choices`gensym148 = newSeq[bool](); helpvar`gensym148 = "";
                group`gensym148 = ""; required`gensym148 = false;
                optional`gensym148 = false; multi`gensym148 = false;
                env`gensym148 = ""; helpLevel`gensym148: Natural = 0): BoolArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be a boolean
proc newCommandArg[S, O](variants: seq[string]; specification: S; help = "";
                         longHelp = ""; prolog = ""; epilog = ""; group = "";
                         helpLevel: Natural = 0;
                         handle: proc (spec: S; opts: O); options: O): CommandArg
A CommandArg represents a command which will then use its own parser to parse the remainder of the arguments. This is how you would implement a multi-command tool like mercurial or git.
  • variants: how to invoke the command
  • specification: the specification of the parser for the command
  • help: the short help string for the command
  • prolog: the prolog to be used in the generated help message for the command
  • epilog: the epilog to be used in the generated help message for the command
  • group: how to group the command in the main help message
  • helpLevel: set to a number greater than 0 to have the command only shown in the autogenerated help when invoked by a HelpArg or HelpCommandArg with helpLevel set (i.e. verbose help)
  • handle: a function that can handle a parsed spec of the command
  • options: any options that you want to make available to the command from the main specification

For a simple tool, you might choose to use commands the same way as any other argument, all in one file

import strutils
import uri

let cloneSpec = (
    url: newURLArg("<url>", help="Repository to clone")
    # ... and the rest...
)
let cloneProlog = "Help for the clone command"
let cloneEpilog = "Example: hg clone https://www.mercurial-scm.org/repo/hg"
let prolog = "Nim-based reimplementation of mercurial"
let spec = (
    clone: newCommandArg("clone", cloneSpec, help="Clone a remote repository"),
    help: newHelpCommandArg("help", help="Show help")
    # ... and the rest ...
)
var parsed = parseCopy(spec, prolog, command="hg", args="help")
doAssert parsed.success and parsed.message.isSome
let expected = """
Nim-based reimplementation of mercurial

Usage:
  hg clone <url>
  hg help

Commands:
  clone  Clone a remote repository
  help   Show help""".strip()
doAssert parsed.message.get == expected

let (success, message) = parseOrMessage(spec, prolog, command="hg",
                           args="clone https://www.mercurial-scm.org/repo/hg")
doAssert success
doAssert spec.clone.seen
# Note how the original cloneSpec has been modified
doAssert cloneSpec.url.seen
doAssert cloneSpec.url.value == parseUri("https://www.mercurial-scm.org/repo/hg")
# Clone the remote repository

For simple tools, this may be all you need, but there are downsides. In particular, if you want to implement the clone command in a different file then its definition will end up in one file and its implementation in another. For more complex, multi-file tools, a different pattern is available. Here, handle is called to implement the action implied by the command and options is used to pass through options that are defined at the top level

# In common.nim

# Options that will be defined at the top level and passed to subsidiary commands.
# If there are none, then this is not required
type
    HgOptions* = tuple[
      verbose: CountArg
    ]

# In clone.nim -- note how this now contains both the argument definitions
# and implementation

# import common

const
    CLONE_PROLOG = "Help for the clone command"
    CLONE_EPILOG = "Example: hg clone https://www.mercurial-scm.org/repo/hg"

type
    CloneSpec* = tuple[
        url: URLArg
    ]

proc runCloneCommand(spec: CloneSpec, options: HgOptions) =
    doAssert options.verbose.seen
    doAssert spec.url.seen
    # Clone the repository
    discard

proc getCloneCommand*(options: HgOptions): CommandArg =
    let spec = (
        url: newURLArg("<url>", help="Repository to clone")
    )
    newCommandArg("clone", spec, prolog=CLONE_PROLOG, epilog=CLONE_EPILOG,
        help="Clone a local or remote repository", handle=runCloneCommand, options=options)


# In hg.nim

# import clone
# import common

const
    PROLOG = "Nim-based re-implementation of mercurial"

let options: HgOptions = (
    verbose: newCountArg("-v, --verbose", help="More verbose output"),
)

let spec = (
    clone: getCloneCommand(options),
    help: newHelpCommandArg("help", help="Show help"),
    verbose: options.verbose
    # ... and the rest ...
)


let (success, message) = parseOrMessage(spec, PROLOG, command="hg",
                            args="-v clone https://www.mercurial-scm.org/repo/hg")
doAssert success and message.isNone

The remainder of the implementation is left as an exercise for the reader

Since:

  • 0.1.0: Initial implementation
  • 0.2.0: handle arg and multi-file support
proc newCommandArg[S, O](variants: string; specification: S; help = "";
                         longHelp = ""; prolog = ""; epilog = ""; group = "";
                         helpLevel: Natural = 0;
                         handle: proc (spec: S; opts: O); options: O): CommandArg
Version of newCommandarg where variants is provided as a string
proc newCommandArg[S](variants: seq[string]; specification: S; help = "";
                      longHelp = ""; prolog = ""; epilog = ""; group = "";
                      helpLevel: Natural = 0; handle: proc (spec: S) = nil): CommandArg
Version of newCommandArg to be used when there is no need to capture options from the main parser
proc newCommandArg[S](variants: string; specification: S; help = "";
                      longHelp = ""; prolog = ""; epilog = ""; group = "";
                      helpLevel: Natural = 0; handle: proc (spec: S) = nil): CommandArg
Convenience version of newCommandArg where variants are provided as a string
proc newCountArg(variants: seq[string]; help: string; longHelp = "";
                 defaultVal = 0; choices = newSeq(); group = "";
                 required = false; optional = false; multi = true; env = "";
                 helpLevel: Natural = 0): CountArg {....raises: [], tags: [],
    forbids: [].}
A CountArg counts how many times it has been seen. When using a CountArg, alternate forms of variant are valid:
  • --[no]option or --[no-]option imply that --option counts up and --nooption or --no-option count down
  • -y/-n or --yes/--no imply that -y or --yes count up and -n or --no count down.

Except in the case when an equal number of count ups and count downs have been seen, arg.seen should report seen whether or not the current count is greater than or less than zero.

If you only expect the argument to be used once, you can use newFlagArg to make this clear, which is equivalent to calling newCountArg with multi=false

import options

let spec = (
    verbosity: newCountArg(@["-v", "--verbosity"], help="Verbosity"),
    assume: newFlagArg("-y/-n, --yes/--no", help="Assume yes (or no) at any prompts"),
    unicode: newFlagArg("--[no-]unicode", help="Check input is valid unicode (or not)")
)
let (success, message) = parseOrMessage(spec, args="-v -v -v -n --unicode", command="hello")
doAssert success and message.isNone
doAssert spec.verbosity.seen
doAssert spec.verbosity.count == 3
doAssert spec.assume.seen
doAssert spec.assume.count == -1
doAssert spec.unicode.seen
doAssert spec.unicode.count == 1

Since: 0.1.0

proc newCountArg(variants: string; help: string; longHelp = ""; defaultVal = 0;
                 choices = newSeq(); group = ""; required = false;
                 optional = false; multi = true; env = "";
                 helpLevel: Natural = 0): CountArg {....raises: [],
    tags: [RootEffect], forbids: [].}

Convenience method where variants are provided as a comma-separated string

Since: 0.2.0

proc newDirArg(variants`gensym168: seq[string]; help`gensym168: string;
               longHelp`gensym168 = ""; defaultVal`gensym168 = "";
               choices`gensym168 = newSeq[string](); helpvar`gensym168 = "";
               group`gensym168 = ""; required`gensym168 = false;
               optional`gensym168 = false; multi`gensym168 = false;
               env`gensym168 = ""; helpLevel`gensym168: Natural = 0): DirArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be an existing directory
proc newDirArg(variants`gensym168: string; help`gensym168: string;
               longHelp`gensym168 = ""; defaultVal`gensym168 = "";
               choices`gensym168 = newSeq[string](); helpvar`gensym168 = "";
               group`gensym168 = ""; required`gensym168 = false;
               optional`gensym168 = false; multi`gensym168 = false;
               env`gensym168 = ""; helpLevel`gensym168: Natural = 0): DirArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be an existing directory
proc newFileArg(variants`gensym158: seq[string]; help`gensym158: string;
                longHelp`gensym158 = ""; defaultVal`gensym158 = "";
                choices`gensym158 = newSeq[string](); helpvar`gensym158 = "";
                group`gensym158 = ""; required`gensym158 = false;
                optional`gensym158 = false; multi`gensym158 = false;
                env`gensym158 = ""; helpLevel`gensym158: Natural = 0): FileArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be an existing file
proc newFileArg(variants`gensym158: string; help`gensym158: string;
                longHelp`gensym158 = ""; defaultVal`gensym158 = "";
                choices`gensym158 = newSeq[string](); helpvar`gensym158 = "";
                group`gensym158 = ""; required`gensym158 = false;
                optional`gensym158 = false; multi`gensym158 = false;
                env`gensym158 = ""; helpLevel`gensym158: Natural = 0): FileArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be an existing file
proc newFishCompletionArg(variants: seq;
                          help = "Show a completion script for fish shell";
                          longHelp = ""; group = ""; helpLevel: Natural = 0): FishCompletionArg
proc newFishCompletionArg(variants: string;
                          help = "Show a completion script for fish shell";
                          longHelp = ""; group = ""; helpLevel: Natural = 0): FishCompletionArg {.
    ...raises: [], tags: [RootEffect], forbids: [].}
proc newFishCompletionCommandArg(variants: seq[string]; help: string;
                                 longHelp = ""; group = ""; helpLevel = 0;
                                 showLevel: Natural = 0): FishCompletionCommandArg {.
    ...raises: [], tags: [], forbids: [].}
proc newFishCompletionCommandArg(variants: string; help: string; longHelp = "";
                                 group = ""; helpLevel = 0;
                                 showLevel: Natural = 0): FishCompletionCommandArg {.
    ...raises: [], tags: [RootEffect], forbids: [].}
proc newFlagArg(variants: seq[string]; help: string; longHelp = "";
                defaultVal = 0; choices = newSeq(); group = "";
                required = false; optional = false; multi = false; env = "";
                helpLevel: Natural = 0): CountArg {....raises: [], tags: [],
    forbids: [].}

Alias for newCountArg where multi=false i.e. intended to capture if a particular option is present or not (e.g. the -r in cp -r).

Since: 0.3.0

proc newFlagArg(variants: string; help: string; longHelp = ""; defaultVal = 0;
                choices = newSeq(); group = ""; required = false;
                optional = false; multi = false; env = "";
                helpLevel: Natural = 0): CountArg {....raises: [],
    tags: [RootEffect], forbids: [].}
proc newFloatArg(variants: seq[string]; help: string; longHelp = "";
                 defaultVal = 0.0; choices = newSeq(); helpvar = ""; group = "";
                 required = false; optional = false; multi = false; env = "";
                 helpLevel: Natural = 0): FloatArg {....raises: [Exception],
    tags: [ReadEnvEffect, RootEffect], forbids: [].}
A FloatArg takes a float value
import options

let spec = (
    number: newFloatArg(@["-f", "--float"], help="A fraction input")
)
let (success, message) = parseOrMessage(spec, args="-f 0.25", command="hello")
doAssert success and message.isNone
doAssert spec.number.seen
doAssert spec.number.value == 0.25

Since: 0.1.0

proc newFloatArg(variants: string; help: string; longHelp = "";
                 defaultVal = 0.0; choices = newSeq(); helpvar = ""; group = "";
                 required = false; optional = false; multi = false; env = "";
                 helpLevel: Natural = 0): FloatArg {....raises: [Exception],
    tags: [ReadEnvEffect, RootEffect], forbids: [].}

Convenience method where variants are provided as a comma-separated string

Since: 0.2.0

proc newHelpArg(variants = @["-h", "--help"]; help = "Show help message";
                longHelp = ""; group = ""; helpLevel: Natural = 0;
                showLevel: Natural = 0; helpStyle = HelpStyle.hsColumns): HelpArg {.
    ...raises: [], tags: [], forbids: [].}

If a help arg is seen, a help message will be shown.

showLevel is compared to the helpLevel of each arg. If the arg's helpLevel is greater than the showLevel, the arg will be hidden from the help message. The help arg has its own helpLevel, so you can hide help args from help messages with a lower showLevel

Note: args with a helpLevel higher than any helpArg's showLevel will never be shown. This may be desirable in some cases.

import options
import strutils
let spec = (
    name: newStringArg(@["<name>"], help="Someone to greet"),
    times: newIntArg(@["-t", "--times"], help="How many times to greet them", helpvar="n"),
    help: newHelpArg(@["-h", "--help"], help="Show a help message"),
)
let prolog = "Greet someone"
let (success, message) = parseOrMessage(spec, prolog=prolog, args="-h", command="hello")
doAssert success and message.isSome
let expected = """
Greet someone

Usage:
  hello <name>
  hello (-h | --help)

Arguments:
  <name>           Someone to greet

Options:
  -t, --times=<n>  How many times to greet them
  -h, --help       Show a help message""".strip()
doAssert message.get == expected

Since: 0.1.0

proc newHelpArg(variants: string; help = "Show help message"; longHelp = "";
                group = ""; helpLevel: Natural = 0; showLevel: Natural = 0;
                helpStyle = HelpStyle.hsColumns): HelpArg {....raises: [],
    tags: [RootEffect], forbids: [].}

Convenience method where variants are provided as a comma-separated string

Since: 0.2.0

proc newHelpCommandArg(variants = @["help"]; help = "Show help message";
                       longHelp = ""; group = ""; helpLevel: Natural = 0;
                       showLevel: Natural = 0; helpStyle = HelpStyle.hsColumns): HelpCommandArg {.
    ...raises: [Exception], tags: [RootEffect], forbids: [].}

Equivalent of newHelpArg where help is a command not an option i.e. > hg help not > hg --help

Since: 0.2.0

proc newHelpCommandArg(variants: string; help = "Show help message";
                       longHelp = ""; group = ""; helpLevel: Natural = 0;
                       showLevel: Natural = 0; helpStyle = HelpStyle.hsColumns): HelpCommandArg {.
    ...raises: [Exception], tags: [RootEffect], forbids: [].}
proc newIntArg(variants: seq[string]; help: string; longHelp = "";
               defaultVal = 0; choices = newSeq(); helpvar = ""; group = "";
               required = false; optional = false; multi = false; env = "";
               helpLevel: Natural = 0): IntArg {....raises: [Exception],
    tags: [ReadEnvEffect, RootEffect], forbids: [].}
An IntArg takes an integer value
import options

let spec = (
    number: newIntArg(@["-n", "--number"], help="An integer input")
)
let (success, message) = parseOrMessage(spec, args="-n 10", command="hello")
doAssert success and message.isNone
doAssert spec.number.seen
doAssert spec.number.value == 10

Since: 0.1.0

proc newIntArg(variants: string; help: string; longHelp = ""; defaultVal = 0;
               choices = newSeq(); helpvar = ""; group = ""; required = false;
               optional = false; multi = false; env = ""; helpLevel: Natural = 0): IntArg {.
    ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}

Convenience method where variants are provided as a comma-separated string

Since: 0.2.0

proc newMessageArg(variants: seq[string]; message: string; help: string;
                   longHelp = ""; group = ""; helpLevel: Natural = 0): MessageArg {.
    ...raises: [], tags: [], forbids: [].}
If a MessageArg is seen, a message will be shown. Might be used to display a version number (as per example below) or to display a hand-rolled help message.
import options

let vspec = (
    version: newMessageArg(@["-v", "--version"], "0.1.0", help="Show the version")
)
let (success, message) = parseOrMessage(vspec, args="-v", command="hello")
doAssert success and message.isSome
doAssert message.get == "0.1.0"

Since: 0.1.0

proc newMessageArg(variants: string; message: string; help: string;
                   longHelp = ""; group = ""; helpLevel: Natural = 0): MessageArg {.
    ...raises: [], tags: [RootEffect], forbids: [].}

Convenience method where variants are provided as a comma-separated string

Since: 0.2.0

proc newMessageCommandArg(variants: seq; message: string;
                          help = "Show help message"; longHelp = ""; group = "";
                          helpLevel: Natural = 0): MessageCommandArg
proc newMessageCommandArg(variants: seq[string]; message: string;
                          help = "Show help message"; longHelp = ""; group = "";
                          helpLevel: Natural = 0): MessageCommandArg {.
    ...raises: [Exception], tags: [RootEffect], forbids: [].}

Equivalent of newMessageArg where help is a command not an option i.e. > hg version not > hg --version

Since: 0.2.0

proc newPathArg(variants`gensym178: seq[string]; help`gensym178: string;
                longHelp`gensym178 = ""; defaultVal`gensym178 = "";
                choices`gensym178 = newSeq[string](); helpvar`gensym178 = "";
                group`gensym178 = ""; required`gensym178 = false;
                optional`gensym178 = false; multi`gensym178 = false;
                env`gensym178 = ""; helpLevel`gensym178: Natural = 0): PathArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be an existing file or directory
proc newPathArg(variants`gensym178: string; help`gensym178: string;
                longHelp`gensym178 = ""; defaultVal`gensym178 = "";
                choices`gensym178 = newSeq[string](); helpvar`gensym178 = "";
                group`gensym178 = ""; required`gensym178 = false;
                optional`gensym178 = false; multi`gensym178 = false;
                env`gensym178 = ""; helpLevel`gensym178: Natural = 0): PathArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be an existing file or directory
proc newStringArg(variants: seq[string]; help: string; longHelp = "";
                  defaultVal = ""; choices = newSeq(); helpvar = ""; group = "";
                  required = false; optional = false; multi = false; env = "";
                  helpLevel: Natural = 0): StringArg {....raises: [Exception],
    tags: [ReadEnvEffect, RootEffect], forbids: [].}
Creates a new Arg.
import options
import unittest

let spec = (
    src: newStringArg(@["<source>"], multi=true, help="Source file(s)"),
    dst: newStringArg(@["<destination>"], help="Destination")
)
let (success, message) = parseOrMessage(spec, args="this and_this to_here", command="cp")
test "Message test":
    check(success and message.isNone)
    check(spec.src.values == @["this", "and_this"])
    check(spec.dst.value == "to_here")
  • variants determines how the Arg is presented to the user and whether the arg is a positional argument (Argument) or an optional argument (Option)
    • Options take the form -o or --option (default to optional - override with required=true)
    • Arguments take the form <value> (default to required - override wiith optional=true)
    • Commands take the form command
  • help is a short form help message to explain what the argument does
  • longHelp may be used to provide a longer version of the help message to be used with the paragraph help style
  • defaultVal is a default value
  • choices is a set of allowed values for the argument
  • helpvar is a dummy variable name shown to the user in the help message forValueArg (i.e. --option <helpvar>). Defaults to the longest supplied variant
  • required implies that an optional argument must appear or parsing will fail (only relevant for options)
  • optional implies that a positional argument does not have to appear (only relevant for arguments)
  • multi implies that an Option may appear multiple times or an Argument consume multiple values
  • helpLevel allows help messages to exclude the arg if it is low-priority, enabling --help and --extended-help help messages. Lower values indicate a higher priority. A value of 0 means the arg will always be shown in help messages.

Notes:

  • multi is greedy -- the first time it is seen it will consume as many arguments as it can, while still allowing any remaining arguments to match
  • required and optional are mutually exclusive, but required=false does not imply optional=true and vice versa.

Since: 0.1.0

proc newStringArg(variants: string; help: string; longHelp = "";
                  defaultVal = ""; choices = newSeq(); helpvar = ""; group = "";
                  required = false; optional = false; multi = false; env = "";
                  helpLevel: Natural = 0): StringArg {....raises: [Exception],
    tags: [ReadEnvEffect, RootEffect], forbids: [].}

Convenience method where variants are provided as a comma-separated string

Since: 0.2.0

proc newStringPromptArg(variants: seq[string]; help: string; longHelp = "";
                        defaultVal = ""; choices = newSeq(); helpvar = "";
                        group = ""; required = false; optional = false;
                        multi = false; prompt: string; secret: bool; env = "";
                        helpLevel: Natural = 0): StringPromptArg {.
    ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
Experimental: Creates an argument whose value is read from a prompt rather than the commandline (e.g. a password)
  • prompt - prompt to display to the user to request input
  • secret - whether to display what the user tyeps (set to false for passwords)

Since: 0.1.0

proc newURLArg(variants`gensym189: seq[string]; help`gensym189: string;
               longHelp`gensym189 = ""; defaultVal`gensym189 = parseUri("");
               choices`gensym189 = newSeq[Uri](); helpvar`gensym189 = "";
               group`gensym189 = ""; required`gensym189 = false;
               optional`gensym189 = false; multi`gensym189 = false;
               env`gensym189 = ""; helpLevel`gensym189: Natural = 0): URLArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be a URI
proc newURLArg(variants`gensym189: string; help`gensym189: string;
               longHelp`gensym189 = ""; defaultVal`gensym189 = parseUri("");
               choices`gensym189 = newSeq[Uri](); helpvar`gensym189 = "";
               group`gensym189 = ""; required`gensym189 = false;
               optional`gensym189 = false; multi`gensym189 = false;
               env`gensym189 = ""; helpLevel`gensym189: Natural = 0): URLArg {.
    inject, ...raises: [Exception], tags: [ReadEnvEffect, RootEffect], forbids: [].}
An argument where the supplied value must be a URI
proc parse(specification: tuple; prolog = ""; epilog = "";
           args: seq[string] = commandLineParams();
           command = extractFilename(getAppFilename()))

Uses the provided specification to parse the input, which defaults to the commandline parameters

Parameters:

  • prolog - free text that is shown before the autogenerated content in help messages
  • epilog - free text that is shown after the autogenerated content in help messages
  • args - a sequence of arguments to be parsed (defaults to commandLineParams())
  • command - the name of the program being run (defaults to getAppFilename())

Behaviour:

  • If the specification is incorrect (i.e. programmer error), SpecificationError is thrown
  • If the parse fails, ParserError is thrown
  • If the parse succeeds, but the user should be shown a message a MessageError is thrown
  • Otherwise, the parse has suceeded

Since: 0.1.0

proc parse(specification: tuple; prolog = ""; epilog = ""; args: string;
           command = extractFilename(getAppFilename()))

Convenience method where args are provided as a space-separated string

Since: 0.2.0

proc parseCopy[S: tuple](specification: S; prolog = ""; epilog = "";
                         args: seq[string] = commandLineParams();
                         command = extractFilename(getAppFilename())): tuple[
    success: bool, message: Option[string], spec: Option[S]]

Version of parse, similar to parseOrMessage that returns a copy of the specification if the parse was successful. Crucially this lets you re-use the original specification, should you wish. This is probably the proc you want for writing tests

Since: 0.2.0

proc parseCopy[S: tuple](specification: S; prolog = ""; epilog = "";
                         args: string;
                         command = extractFilename(getAppFilename())): tuple[
    success: bool, message: Option[string], spec: Option[S]]

Version of parseCopy that accepts args as a string for convenience

Since: 0.2.0

proc parseOrHelp(spec: tuple; prolog = ""; epilog = "";
                 args: seq[string] = commandLineParams();
                 command: string = extractFilename(getAppFilename()))

Attempts to parse the input. If the parse fails, shows the user the error message and help message, then quits. If the user has asked for a message (e.g. help), shows the message and quits.

Since: 0.2.0

proc parseOrHelp(spec: tuple; prolog = ""; epilog = ""; args: string;
                 command: string = extractFilename(getAppFilename()))

Convenience version of parseOrHelp that takes a string for args.

Since: 0.2.0

proc parseOrMessage(spec: tuple; prolog = ""; epilog = "";
                    args: seq[string] = commandLineParams();
                    command = extractFilename(getAppFilename())): tuple[
    success: bool, message: Option[string]]

Version of parse that returns success if the parse was sucessful. If the parse fails, or the result of the parse is an informationl message for the user, Option[str] will containing an appropriate message

Since: 0.1.0

proc parseOrMessage(spec: tuple; prolog = ""; epilog = ""; args: string;
                    command: string): tuple[success: bool,
    message: Option[string]]

Version of parseOrMessage that accepts args as a string for convenience

Since: 0.2.0

proc parseOrQuit(spec: tuple; prolog = ""; epilog = "";
                 args: seq[string] = commandLineParams();
                 command = extractFilename(getAppFilename()))

Attempts to parse the input. If the parse fails or the user has asked for a message (e.g. help), show a message and quit. This is probably the proc you want for a simple commandline script

Since: 0.1.0

proc parseOrQuit(spec: tuple; prolog = ""; epilog = ""; args: string;
                 command: string)

Version of parseOrQuit taking args as a string for convenience

Since: 0.1.0

proc parseWithExtras(specification: tuple; prolog = ""; epilog = "";
                     args: seq[string] = commandLineParams();
                     command = extractFilename(getAppFilename())): tuple[
    extra_args: StringArg, extra_options: OrderedTable[string, Arg]]

Uses the provided specification to parse the input, which defaults to the commandline parameters. This variant also allows unrecognised options to be passed which will be collected and returned in either extra_args or extra_options depending on their form

Parameters:

  • prolog - free text that is shown before the autogenerated content in help messages
  • epilog - free text that is shown after the autogenerated content in help messages
  • args - a sequence of arguments to be parsed (defaults to commandLineParams())
  • command - the name of the program being run (defaults to getAppFilename())

Behaviour:

  • If the specification is incorrect (i.e. programmer error), SpecificationError is thrown
  • If the parse fails, ParserError is thrown
  • If the parse succeeds, but the user should be shown a message a MessageError is thrown
  • If arguments or options were provided that were not in the specification they are in the returned tuple

Since: 0.3.0

proc parseWithExtras(specification: tuple; prolog = ""; epilog = "";
                     args: string; command: string): tuple[
    extra_args: StringArg, extra_options: OrderedTable[string, Arg]]

Convenience method where args are provided as a space-separated string

Since: 0.3.0

proc render_help(spec: tuple; prolog = ""; epilog = "";
                 command = extractFilename(getAppFilename());
                 showLevel: Natural = 0): string
Renders a help message to be shown for spec. Each arg's helpLevel is compared to showLevel: if the helpLevel is greater, the arg will not be shown in the help message.
func seen(arg: Arg): bool {....raises: [], tags: [], forbids: [].}

seen returns true if the argument was seen in the input

Since: 0.1.0

Methods

method parse(arg: Arg; value: string; variant: string) {.base,
    ...raises: [ValueError], tags: [], forbids: [].}
parse is called when a value is seen for an argument. If you write your own Arg you will need to provide a parse implementation. If the value cannot be parsed, a ParseError is raised with a user-friendly explanation
method register(arg: Arg; variant: string; command: string; spec: Specification) {.
    base, ...raises: [ParseError, ValueError], tags: [], forbids: [].}
register is called by the parser when an argument is seen. If you want to interupt parsing e.g. to print help, now is the time to do it
method register(arg: CountArg; variant: string; command: string;
                spec: Specification) {....raises: [ParseError, ValueError],
                                       tags: [], forbids: [].}

Macros

macro defineArg[T](TypeName: untyped; cons: untyped; name: string;
                   ArgType: typedesc; parseT: proc (value: string): T;
                   defaultT: T; comment = "")
defineArg allows you to define your own ValueArg type simply by providing a proc that can parse a string into a T.
  • T The type of the parsed value
  • TypeName The name of your ValueArg type
  • cons The name of the constructor for your new type
  • name What to call this type in help messages i.e. Expected a <name> got ...
  • ArgType The type of the value (i.e. same as T)
  • parseT A proc that parses a value into a T, raising ValueError or ParseError on failure
  • defaultT The default value to use if none is provided (default(T) is often a good bet, but is not defined for all types.)
  • comment The docstring to use in the cons constructor

Notes:

  • If parseT fails by raising a ValueError an error message will be written for you. To provide a custom error message, raise a ParseError
  • The error messages can get gnarly, parameters in docstring contain gensym for unknown reasons
import times

# Decide on your default value
let DEFAULT_DATE = initDateTime(1, mJan, 2000, 0, 0, 0, 0)

# Define a parser
proc parseDate(value: string): DateTime = parse(value, "YYYY-MM-dd")

defineArg[DateTime](DateArg, newDateArg, "date", DateTime, parseDate, DEFAULT_DATE)

# We can now use newDateArg to define an argument that takes a date

let spec = (
  date: newDateArg(@["<date>"], help="Date to change to")
)
spec.parse(args="1999-12-31", "set-party-date")

doAssert(spec.date.value == initDateTime(31, mDec, 1999, 0, 0, 0, 0))

Since:

  • 0.1.0: Initial definition
  • 0.3.0: Switch to a macro. ArgType now required, comment now possible

Templates

template check_choices[T](arg: Arg; value: T; variant: string)

check_choices checks that value has been set to one of the acceptable choices values

Since: 0.1.0