therapist

Type-safe commandline parsing with minimal magic

Pure Nim score 15/100 · tests present · no docs generated

Summary

Latest Version Unknown
License MIT
CI Status Failing
Downloads 0
Last Indexed 2026-07-21 05:24

Installation

nimble install therapist
choosenim install therapist
git clone https://bitbucket.org/maxgrenderjones/therapist

OS Compatibility

Platform Linux macOS Windows FreeBSD OpenBSD NetBSD Android iOS WASM Embedded
therapist - - - - - - -

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.

.. image:: https://img.shields.io/bitbucket/pipelines/maxgrenderjones/therapist

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:

.. code-block:: nim

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

.. code-block:: sh

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:

.. code-block:: nim

# doctest: skip proc newStringArg*(variants: seq[string], help: string, defaultVal="", choices=newSeqstring, 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:

.. code-block:: nim

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.

.. code-block:: nim

import options import strutils import therapist

let prolog = "Navel Fate."

let create = ( name: newStringArg(@[""], multi=true, help="Name of new ship") ) let move = ( name: newStringArg(@[""], help="Name of ship to move"), x: newIntArg(@[""], help="x grid reference"), y: newIntArg(@[""], help="y grid reference"), speed: newIntArg(@["--speed"], defaultVal=10, help="Speed in knots"), help: newHelpArg() ) let shoot = ( x: newIntArg(@[""], help="Name of new ship"), y: newIntArg(@[""], 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(@[""], choices = @["set", "remove"], help="Action to perform"), x: newIntArg(@[""], help="Name of new ship"), y: newIntArg(@[""], 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 ... navel_fate ship move navel_fate ship shoot navel_fate mine (set|remove) 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).

.. code-block:: nim

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() <https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.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.

.. code-block:: nim

import tables
import unittest

import therapist

suite "Ship Colours":

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

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

  test "Extras":
    let args = "historical -abb --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"
    require(extras.extra_options["--extended-colours"] of CountArg)
    doAssert "--extended-colours" in extras.extra_options
    doAssert "-a" in extras.extra_options
    doAssert "-b" in extras.extra_options
    doAssert extras.extra_options["-b"].count==2
    require(extras.extra_options["--victory"] of StringArg)
    doAssert StringArg(extras.extra_options["--victory"]).value == "red"
    doAssert StringArg(extras.extra_options["--mayflower"]).value == "blue"
    doAssert StringArg(extras.extra_options["--hind"]).value == "golden"

Note that if you are using the extras mechanic, options must be consistent in whether they do or don't supply a value (which can only done using the =<value> syntax).

.. code-block:: nim import therapist import unittest

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

expect(ParseError): let no_value_extras = spec_no_value.parseWithExtras("Set Colours", command="set_colours", args="--victory --victory=red foo")

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

expect(ParseError): let value_extras = spec_no_value.parseWithExtras("Set Colours", command="set_colours", args="--victory=red --victory foo")

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:

.. code:: sh

nimble install

Or simply:

.. code:: sh

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

.. include:: CHANGELOG.rst

.. _bitbucket: https://bitbucket.org/maxgrenderjones/therapist .. _parseopt: https://nim-lang.org/docs/parseopt.html .. _nim-argparse: https://github.com/iffy/nim-argparse .. _docopt.nim: https://github.com/docopt/docopt.nim .. _nimdoc: https://maxgrenderjones.bitbucket.io/therapist/latest/therapist.html .. _Therapist: https://maxgrenderjones.bitbucket.io/therapist/latest/therapist.html .. _cligen: https://github.com/c-blake/cligen .. _argh: https://pythonhosted.org/argh/