src/therapist

    Dark Mode
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.

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` 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.
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
  hello -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 template 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
  • 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", 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.

Possible features therapist does not have

In rough order of likelihood of being added:

  • Options for help format from columns (current) to paragraphs
  • 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
  • Integration with bash / fish completion scripts
  • Dependent option requirements i.e. because --optionA appears, --optionB is required, or one of --left or --right 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

Contributing

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

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.2.0

  • 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
  variants: seq[string]
  help: string               ## The help string for the argument
  count*: int                ## How many times the argument was seen
  required: bool             ## Set to true to make an option required
  optional: bool             ## Set to true to make a positional argument optional
  multi: bool                ## Set to true to allow the argument to appear more than once
  env: string                ## The name of an environment variable to use as a default value
  helpVar: string            ## The name of a variable to use as an example name in help messages
  group: string              ## The group of help messages the argument should appear in
  helpLevel: Natural         ## The help level that governs when the argument is shown
  kind: ArgKind
Base class for arguments
ArgError = object of CatchableError
  nil
Base Exception for module
BoolArg {.inject.} = ref object of ValueArg
  defaultVal: T
  value*: T
  values*: seq[T]
  choices: seq[T]
CommandArg = ref object of Arg
  specification*: Specification
  handler: proc ()
CommandArg represents a subcommand, which will be processed with its own parser
CountArg = ref object of Arg
  defaultVal: int
  choices: seq[int]
  down: HashSet[string]
Counts the number of times this argument appears
DirArg {.inject.} = ref object of ValueArg
  defaultVal: T
  value*: T
  values*: seq[T]
  choices: seq[T]
FileArg {.inject.} = ref object of ValueArg
  defaultVal: T
  value*: T
  values*: seq[T]
  choices: seq[T]
FloatArg = ref object of ValueArg
  defaultVal: float
  value*: float
  values*: seq[float]
  choices: seq[float]
An argument or option whose value is a float
HelpArg = ref object of CountArg
  showLevel: Natural
If this argument is provided, a MessageError containing a help message will be raised
HelpCommandArg = ref object of CommandArg
  showLevel: Natural
HelpCommandArg allows you to create a command that prints help
IntArg = ref object of ValueArg
  defaultVal: int
  value*: int
  values*: seq[int]
  choices: seq[int]
An argument or option whose value is an int
MessageArg = ref object of CountArg
  message: string
If this argument is provided, a MessageError containing a message will be raised
MessageCommandArg = ref object of CommandArg
  message: string
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
  defaultVal: T
  value*: T
  values*: seq[T]
  choices: seq[T]
PromptArg = ref object of Arg
  prompt: string
  secret: bool
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
  defaultVal: string
  value*: string
  values*: seq[string]
  choices: seq[string]
An argument or option whose value is a string
StringPromptArg = ref object of PromptArg
  defaultVal: string
  value*: string
  values*: seq[string]
  choices: seq[string]
URLArg {.inject.} = ref object of ValueArg
  defaultVal: T
  value*: T
  values*: seq[T]
  choices: seq[T]
ValueArg = ref object of Arg
  nil
Base class for arguments that take a value

Procs

proc initArg[A, T](arg: var A; variants: seq[string]; help: 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 newBoolArg(variants`gensym151: seq[string]; help`gensym151: string;
                defaultVal`gensym151: T = false;
                choices`gensym151 = newSeq[T](); helpvar`gensym151 = "";
                group`gensym151 = ""; required`gensym151 = false;
                optional`gensym151 = false; multi`gensym151 = false;
                env`gensym151 = ""; helpLevel`gensym151: Natural = 0): BoolArg {.
    inject, ...raises: [ValueError], tags: [ReadEnvEffect].}
Template-defined constructor - see help for newStringArg for the meaning of parameters
proc newBoolArg(variants`gensym151: string; help`gensym151: string;
                defaultVal`gensym151: T = false;
                choices`gensym151 = newSeq[T](); helpvar`gensym151 = "";
                group`gensym151 = ""; required`gensym151 = false;
                optional`gensym151 = false; multi`gensym151 = false;
                env`gensym151 = ""; helpLevel`gensym151: Natural = 0): BoolArg {.
    inject, ...raises: [ValueError], tags: [ReadEnvEffect].}
proc newCommandArg[S, O](variants: seq[string]; specification: S; help = "";
                         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 = "";
                         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 = "";
                      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 = "";
                      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; defaultVal = 0;
                 choices = newSeq[int](); group = ""; required = false;
                 optional = false; multi = true; env = "";
                 helpLevel: Natural = 0): CountArg {....raises: [], tags: [].}
A CountArg counts how many times it has been seen
import options

let spec = (
    verbosity: newCountArg(@["-v", "--verbosity"], help="Verbosity")
)
let (success, message) = parseOrMessage(spec, args="-v -v -v", command="hello")
doAssert success and message.isNone
doAssert spec.verbosity.count == 3

Since: 0.1.0

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

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

Since: 0.2.0

proc newDirArg(variants`gensym171: seq[string]; help`gensym171: string;
               defaultVal`gensym171: T = ""; choices`gensym171 = newSeq[T]();
               helpvar`gensym171 = ""; group`gensym171 = "";
               required`gensym171 = false; optional`gensym171 = false;
               multi`gensym171 = false; env`gensym171 = "";
               helpLevel`gensym171: Natural = 0): DirArg {.inject,
    ...raises: [ValueError], tags: [ReadEnvEffect].}
Template-defined constructor - see help for newStringArg for the meaning of parameters
proc newDirArg(variants`gensym171: string; help`gensym171: string;
               defaultVal`gensym171: T = ""; choices`gensym171 = newSeq[T]();
               helpvar`gensym171 = ""; group`gensym171 = "";
               required`gensym171 = false; optional`gensym171 = false;
               multi`gensym171 = false; env`gensym171 = "";
               helpLevel`gensym171: Natural = 0): DirArg {.inject,
    ...raises: [ValueError], tags: [ReadEnvEffect].}
proc newFileArg(variants`gensym161: seq[string]; help`gensym161: string;
                defaultVal`gensym161: T = ""; choices`gensym161 = newSeq[T]();
                helpvar`gensym161 = ""; group`gensym161 = "";
                required`gensym161 = false; optional`gensym161 = false;
                multi`gensym161 = false; env`gensym161 = "";
                helpLevel`gensym161: Natural = 0): FileArg {.inject,
    ...raises: [ValueError], tags: [ReadEnvEffect].}
Template-defined constructor - see help for newStringArg for the meaning of parameters
proc newFileArg(variants`gensym161: string; help`gensym161: string;
                defaultVal`gensym161: T = ""; choices`gensym161 = newSeq[T]();
                helpvar`gensym161 = ""; group`gensym161 = "";
                required`gensym161 = false; optional`gensym161 = false;
                multi`gensym161 = false; env`gensym161 = "";
                helpLevel`gensym161: Natural = 0): FileArg {.inject,
    ...raises: [ValueError], tags: [ReadEnvEffect].}
proc newFloatArg(variants: seq[string]; help: string; defaultVal = 0.0;
                 choices = newSeq[float](); helpvar = ""; group = "";
                 required = false; optional = false; multi = false; env = "";
                 helpLevel: Natural = 0): FloatArg {....raises: [ValueError],
    tags: [ReadEnvEffect].}
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; defaultVal = 0.0;
                 choices = newSeq[float](); helpvar = ""; group = "";
                 required = false; optional = false; multi = false; env = "";
                 helpLevel: Natural = 0): FloatArg {....raises: [ValueError],
    tags: [ReadEnvEffect].}

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

Since: 0.2.0

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

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"; group = "";
                helpLevel, showLevel: Natural = 0): HelpArg {....raises: [],
    tags: [].}

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

Since: 0.2.0

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

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"; group = "";
                       helpLevel, showLevel: Natural = 0): HelpCommandArg {.
    ...raises: [Exception], tags: [RootEffect].}
proc newIntArg(variants: seq[string]; help: string; defaultVal = 0;
               choices = newSeq[int](); helpvar = ""; group = "";
               required = false; optional = false; multi = false; env = "";
               helpLevel: Natural = 0): IntArg {....raises: [ValueError],
    tags: [ReadEnvEffect].}
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; defaultVal = 0;
               choices = newSeq[int](); helpvar = ""; group = "";
               required = false; optional = false; multi = false; env = "";
               helpLevel: Natural = 0): IntArg {....raises: [ValueError],
    tags: [ReadEnvEffect].}

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

Since: 0.2.0

proc newMessageArg(variants: seq[string]; message: string; help: string;
                   group = ""; helpLevel: Natural = 0): MessageArg {....raises: [],
    tags: [].}
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; group = "";
                   helpLevel: Natural = 0): MessageArg {....raises: [], tags: [].}

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"; group = "";
                          helpLevel: Natural = 0): MessageCommandArg
proc newMessageCommandArg(variants: seq[string]; message: string;
                          help = "Show help message"; group = "";
                          helpLevel: Natural = 0): MessageCommandArg {.
    ...raises: [Exception], tags: [RootEffect].}

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`gensym181: seq[string]; help`gensym181: string;
                defaultVal`gensym181: T = ""; choices`gensym181 = newSeq[T]();
                helpvar`gensym181 = ""; group`gensym181 = "";
                required`gensym181 = false; optional`gensym181 = false;
                multi`gensym181 = false; env`gensym181 = "";
                helpLevel`gensym181: Natural = 0): PathArg {.inject,
    ...raises: [ValueError], tags: [ReadEnvEffect].}
Template-defined constructor - see help for newStringArg for the meaning of parameters
proc newPathArg(variants`gensym181: string; help`gensym181: string;
                defaultVal`gensym181: T = ""; choices`gensym181 = newSeq[T]();
                helpvar`gensym181 = ""; group`gensym181 = "";
                required`gensym181 = false; optional`gensym181 = false;
                multi`gensym181 = false; env`gensym181 = "";
                helpLevel`gensym181: Natural = 0): PathArg {.inject,
    ...raises: [ValueError], tags: [ReadEnvEffect].}
proc newStringArg(variants: seq[string]; help: string; defaultVal = "";
                  choices = newSeq[string](); helpvar = ""; group = "";
                  required = false; optional = false; multi = false; env = "";
                  helpLevel: Natural = 0): StringArg {....raises: [ValueError],
    tags: [ReadEnvEffect].}
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
  • 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
  • optional implies that a positional argument does not have to appear
  • 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; defaultVal = "";
                  choices = newSeq[string](); helpvar = ""; group = "";
                  required = false; optional = false; multi = false; env = "";
                  helpLevel: Natural = 0): StringArg {....raises: [ValueError],
    tags: [ReadEnvEffect].}

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

Since: 0.2.0

proc newStringPromptArg(variants: seq[string]; help: string; defaultVal = "";
                        choices = newSeq[string](); helpvar = ""; group = "";
                        required = false; optional = false; multi = false;
                        prompt: string; secret: bool; env = "";
                        helpLevel: Natural = 0): StringPromptArg {.
    ...raises: [ValueError], tags: [ReadEnvEffect].}
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`gensym192: seq[string]; help`gensym192: string;
               defaultVal`gensym192: T = parseUri("");
               choices`gensym192 = newSeq[T](); helpvar`gensym192 = "";
               group`gensym192 = ""; required`gensym192 = false;
               optional`gensym192 = false; multi`gensym192 = false;
               env`gensym192 = ""; helpLevel`gensym192: Natural = 0): URLArg {.
    inject, ...raises: [ValueError], tags: [ReadEnvEffect].}
Template-defined constructor - see help for newStringArg for the meaning of parameters
proc newURLArg(variants`gensym192: string; help`gensym192: string;
               defaultVal`gensym192: T = parseUri("");
               choices`gensym192 = newSeq[T](); helpvar`gensym192 = "";
               group`gensym192 = ""; required`gensym192 = false;
               optional`gensym192 = false; multi`gensym192 = false;
               env`gensym192 = ""; helpLevel`gensym192: Natural = 0): URLArg {.
    inject, ...raises: [ValueError], tags: [ReadEnvEffect].}
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 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: [].}

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: [].}
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) {.base,
    ...raises: [ParseError, ValueError], tags: [].}
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) {.
    ...raises: [ParseError, ValueError], tags: [].}

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

template defineArg[T](TypeName: untyped; cons: untyped; name: string;
                      parseT: proc (value: string): T; defaultT: T)

defineArg is a concession to the power of magic. If you want to define your own ValueArg for type T, you simply need to pass in a method that is able to parse a string into a T and a sensible default value. default(T) is often a good bet, but is not defined for all types.

If parseT fails by raising a ValueError an error message will be written for you. To provide a custom error message, raise a ParseError

Beware, the error messages can get gnarly, generated docstrings will be ugly

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", 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