Documentation
Command-line interfaces
- Why use document-based argument parsing?
- Basic example
- Where the declaration is read from
- Passing arguments to a script
- Language summary
- Choosing between matches
- Help and errors
- Compatibility notes
- Differences from Docopt
rash includes a command-line argument parser driven by the documentation in your script.
Rather than writing argument parsing code, you document how your script should be used, and rash
parses the arguments according to that specification.
The usage language is inspired by Docopt: “the program’s help message is the source of truth for command-line argument parsing logic”. Rash implements its own parser. It follows Docopt for the constructs described in this book, but it is not a complete Docopt implementation; see Differences from Docopt.
Why use document-based argument parsing?
- Documentation first: You write the help text that users will see, not abstract parsing rules
- Single source of truth: No risk of documentation being out of sync with the code
- Declarative: Describe what the interface looks like, not how to parse it
Basic example
#!/usr/bin/env -S rash --
#
# Copy files from source to dest dir
#
# Usage:
# copy.rh [options] <source>... <dest>
# copy.rh
#
# Options:
# -h --help show this help message and exit
# --mode MODE dest file permissions [default: 0644]
- copy:
src: "{{ item }}"
dest: "{{ dest }}/{{ item | split('/') | last }}"
mode: "{{ options.mode }}"
loop: "{{ source | default([]) }}"
./copy.rh --mode 0600 a.txt b.txt /tmp/dest
In this example:
- The usage patterns describe the command-line interface
- The arguments are parsed and made available as variables (
source,dest,options.mode) - No additional parsing code is needed
Where the declaration is read from
- The help text is the comment block that starts on the line right after the shebang. It ends at
the first line without
#, so do not leave a blank line after the shebang: the comments below it would not be read. The first space after each#is removed. Usage:(case-insensitive) declares the usage patterns, in one of two forms:- One line:
# Usage: my_script.rh <name>. This form holds exactly one pattern. - Block:
# Usage:on its own line, followed by one indented pattern per line. The block ends at an empty comment line (#) or at a non-indented line such asOptions:; comment lines holding only spaces or tabs are skipped. A block without any indented pattern is an error (Usage block declares no patterns).
- One line:
- The first word of each pattern is the program name and is ignored.
- Every help-text line that starts with an option (after indentation) describes it, conventionally
under an
Options:heading; a Markdown bullet such as- notedoes not. Separate the description with at least two spaces:-v --verbose Verbose outputdeclares an option that takes a value. See Options. - If there is no
Usage:, the arguments are not parsed and no variables are added. They are still available as a list of strings in{{ rash.args }}.
Passing arguments to a script
Arguments after the script path are passed to the script, but arguments starting with - are
read by rash itself unless they come after --:
rash copy.rh -- --mode 0600 a.txt /tmp/dest
A script using the shebang #!/usr/bin/env -S rash -- receives every argument directly, so it can
be called as ./copy.rh --mode 0600 a.txt /tmp/dest.
rash consumes the first --. A second one reaches the script, where it ends the script options:
rash tool.rh -- -v -- -x passes -x as a positional value (see [--]).
Language summary
| Syntax | Meaning | Variable |
|---|---|---|
name |
Command: the literal word name |
name: true/false, or a count if it can repeat |
<name>, NAME |
Positional argument | name: string, or list if it can repeat; omitted if not given |
-v, --verbose |
Option flag | options.verbose: true/false, or a count if it can repeat |
--port=<n>, -o FILE |
Option with a value | options.port: string, its [default: ...], or null |
[ ... ] |
Optional elements | |
( ... ) |
Required group | |
a \| b |
Mutually exclusive alternatives | |
elem... |
One or more repetitions of elem |
|
[options] |
Any described option that no usage pattern names | |
[--] |
Match the -- that ends the options |
__: true/false |
- |
Command: a lone - (by convention, standard input) |
_: true/false |
Command and positional names are ASCII words: a letter, then letters or digits, with words joined
by - or _ (lowercase for name and <name>, uppercase for NAME). <file1>, FILE-2 and
step2 are valid; <1st> and Run are not. Variable names replace - with _ and lowercase
NAME. Options are stored under options, keyed by their long name if they have one, so a usage
that declares options cannot also name a command or positional options. Groups can be nested up
to 64 levels deep.
A -- in the arguments always ends the options: every later argument is a positional value, even
if it starts with -. If no usage pattern declares [--], the -- itself is dropped; if one
does, -- is kept as a word that fills the [--] slot (or a positional where [--] cannot be).
A variable has the same type in every pattern: if <source> can repeat in one pattern, it is a
list in all of them.
See Syntax for the full description of each element, and Parser for the variables they produce.
Choosing between matches
When the arguments fit the declaration in more than one way, rash picks one result,
deterministically:
- Usage patterns are tried in declaration order: the first pattern that matches wins.
- Alternatives (
a | b) are tried in the order they are written. - Optional elements and repetitions take as many arguments as they can, but give arguments back when the rest of the pattern needs them.
| Declaration | Arguments | Result |
|---|---|---|
tool [<a>] [<b>] |
x |
a = "x", b omitted |
tool (<a> \| <b>) |
x |
a = "x", b omitted |
tool [<a>]... [<b>] |
x y |
a = ["x", "y"], b omitted |
tool [<a>] [<b>] <c> |
x |
c = "x", a and b omitted |
cp <source> <dest> and cp <source>... <directory> |
a b |
first pattern: source = ["a"], dest = "b" |
| (same) | a b c |
second pattern: source = ["a", "b"], directory = "c" |
Help and errors
Help. rash prints the help text, followed by a note about --, and exits with status 0
without running any task when:
- The arguments contain a help option before any
--: a declared flag whose long name is--help(with any short alias, such as-h --help), or a-hflag without a long name. It wins even if the other arguments are invalid or match no pattern (--unknown --helpshows the help). It must itself be written correctly (--help=yesis an error), and it is not a help request when it is the value of another option (--port --help).-his not a help option when it takes a value (-h <host>) or has another long name (-h, --human). - The arguments match a pattern through a
helpcommand, such astool (help | run <target>).
Usage errors. When the arguments match no usage pattern, rash prints [ERROR] and the help
text to stderr and exits with status 1. These more specific errors are reported the same way:
| Error | Cause |
|---|---|
Unknown option: --nope |
The option is not declared (long options cannot be abbreviated). |
Option --port requires a value |
A value option is the last argument. |
Usage block declares no patterns |
Usage: is followed by no indented pattern. |
Option --dry-run does not take a value |
A flag is given a value with =. |
Ambiguous option alias: -u |
The short alias is declared for two different options. |
Invalid usage identifier: Run |
The declaration contains an invalid command or positional name. |
Invalid usage grammar at token ... |
Unbalanced brackets, or groups nested deeper than 64 levels. |
`options` is a reserved name ... |
A command or positional named options in a usage with options. |
An ambiguous alias is only an error when it is used: the long aliases of both options keep working.
When the value placeholder of a described option is neither <value> nor upper case, as with
-v --verbose Verbose output, requires a value adds a reminder that a description must be
separated from the option by at least two spaces.
Compatibility notes
This release replaced the argument parser. Most interfaces behave as before, but these declarations behave differently.
Most likely to break: single-space option descriptions. An Options: line whose description
follows the option after a single space or a tab, such as -v --verbose Verbose output, now
declares an option that takes a value, as in Docopt: --verbose alone fails with
Option --verbose requires a value, and --verbose file.txt stores "file.txt". Previously the
option stored the truthy string "--verbose". Separate descriptions with at least two spaces.
Now accepted (previously rejected or broken):
- Optional options around a required command:
tool [--verbose] (start|stop) [--force]accepts a barestart. - Groups inside brackets are independently optional:
[(a | b) (c | d)]acceptscalone, and[(--aa | --bb) (--cc | --dd)]no longer rejects every call. - An option opening a group is an option:
(--either-this <and-that> | <or-this>)no longer fails withUnknown option: --either-this. - An option before
)is parsed correctly:(<key> | --all)accepts--alland setsoptions.all(no strayoptions["all)"]key). - Repeatable flags:
[--quiet | --verbose]...,[--verbose]... [--quiet]... <file>and[-a...]accept repetitions and count them. [--tag]...with--tag=VALUEdescribed underOptions:works like[--tag=<value>]...(the last value wins).[-o FILE] [--sorted | --quiet]accepts an empty call, and declarations that combine a short option cluster such as[-hsoFILE]with repeatable options no longer reject every call.- The
--separator ends the options:tool <file>...acceptsa -- -b(file = ["a", "-b"]), andtool [options] [--] <file>...accepts-v -- -x.-and--are commands with the variables_and__. - A help option shows the help wherever it appears and even if other arguments are invalid:
tool a b --helpandtool --unknown --helpshow the help instead of an error. - Digits in names (
<file1>,FILE-2,step2) and directly nested brackets ([[<x>]]). - Negative numbers as option values:
tool [--port=<p>] <x>accepts--port -5 a(previouslyUnknown option: -5). -as a positional value:tool <x>accepts-(x = "-").- An option written both explicitly and through
[options]:tool [options] [-v] <x>accepts-v a. - A
Usage:line with trailing spaces starts a usage block: every indented pattern after it is read (previously only the first one, as a one-line usage).
Now rejected (previously accepted, often with a wrong result):
- An option given more times than the pattern declares it:
tool [-a] [-b]rejects-a -a, andtool [-a] [-b] [-a]rejects-a -a -a. - A value option without its value fails with
Option -o requires a valueinstead of storing"-o". - A comment line holding only whitespace inside the usage block is skipped. Previously it was an
empty pattern, so
tool <a>followed by such a line also accepted no arguments at all. - A short alias declared for two options (
-u --sysupgradeand-u --upgrades) fails withAmbiguous option alias: -uwhen used, instead of silently picking one. The long aliases work. - A command or positional named
optionsin a usage that declares options. --help=yesfails withOption --help does not take a valueinstead of showing the help.- Invalid declarations and option values report the cause (
Invalid usage identifier: Run,Option --known does not take a value) instead of the whole help text or an empty message.
Value changes:
- When several matches are possible, the result follows Choosing between
matches. Previously the choice was arbitrary and could change from
one run to the next:
cp <source> <dest>/cp <source>... <directory>witha bsometimes setdirectory, andtool (<a> | <b>)withxusually setb. - A command that can occur more than once in a pattern (
go (up | down)...,tool a [a]) is always a count:0when absent and1when given once (previouslyfalse/trueuntil given twice). - An option written more than once in a pattern (
[-a] [-a]) or in a repeated group ([--quiet | --verbose]...) is a count (previouslytrue, or a mix of boolean and count). - A positional written more than once in a pattern (
[<x>] [<x>]) is always a list, even with a single value (previously the last value as a string). - Short options never fill positional slots:
tool [-asoFILE] [INPUT ...]with-ssetsoptions.s(previously it sometimes gaveinput = ["-s"]). - A value option takes the next argument as its value even if it starts with
-, as before and as in Docopt:tool [--port=<p>] [-v]with--port -vgivesoptions.port = "-v". Previouslyoptions.vwas also set totrue; now it staysfalse. - A
-hflag without a long name shows the help (previously it setoptions.hand could stand in for a positional), and--port --helpsetsoptions.portto--helpinstead of showing the help. - Help lines starting with
-that declare no option, such as Markdown bullets (- note), no longer add an emptyoptions[""]key.
Differences from Docopt
These Docopt behaviours are not supported:
- Options only match where the pattern declares them. With
tool [--verbose] (start|stop) [--force],start --forceworks but--force startandstart --verboseare rejected. Adjacent optional options, and the options in[options], can be given in any order among themselves. --is dropped without[--]. When no usage pattern declares[--],--in the arguments ends the options and is discarded; Docopt keeps it as a positional value. The variables of--and-are__and_.- No abbreviated long options.
--verbdoes not match--verbose. - One-line usage holds one pattern. Continuation lines after
Usage: tool ...are ignored, and repeating the program name on that line is not supported: inUsage: tool <x> | tool <y>the secondtoolis a command. Use the block form for several patterns. - Options inside an optional command group follow the command.
tool [cmd [-o]]acceptscmd -obut not-oalone; Docopt accepts both. - Option groups use the pattern-wide limit. In a group of two or more adjacent optional options
such as
[-a] [-b], each option may occur as many times as the whole pattern declares it, sotool [-a] [-b] cmd [-a] [-b]accepts-a -a cmd. A single optional option accepts one occurrence, sotool [-a] cmd [-a]rejects-a -a cmd. Docopt accepts both. [default: value]is case-sensitive.[Default: 80]sets no default; Docopt accepts it.- Bare
[options]accepts a repeated flag when it stands for two or more options:tool [options]accepts-a -aand reportstrue. Docopt rejects it. - Repeated value options keep the last value.
tool [--tag=<value>]...with--tag=one --tag=twogives"two"; Docopt gives["one", "two"]. - Missing positionals are omitted. An optional positional that was not given is not defined
(Docopt sets it to
null, or[]if repeatable); use{{ name | default(...) }}. - A usage reference to a value option may omit the value.
tool [--tag]with--tag=VALUEunderOptions:is accepted; Docopt rejects the declaration. - Stricter identifiers. Command and positional names are ASCII, cannot mix case and cannot
start with a digit (
Run,<File>and<1st>are invalid).
Rash also accepts some arguments that Docopt rejects, because it backtracks into optional and
repeated elements: tool [<a>] [<b>] <c> with x, and the cp patterns above with a b c.