README

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:

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)

Argument types provided out of the box

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:

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:

Changes

0.3.0

0.2.0 2022-07-16

0.1.0 2020-05-23

Initial release