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
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
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.
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]
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
- variants determines how the Arg is presented to the user and whether the arg is a positional
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.
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
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