Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pattern groups #313

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
79 changes: 78 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ the return dictionary will be:
Help message format
======================================================================

Help message consists of 2 parts:
Help message consists of 3 parts:

- Usage pattern, e.g.::

Expand All @@ -200,6 +200,13 @@ Help message consists of 2 parts:
--quiet print less text
--verbose print more text

- Group descriptions (optional), e.g.::

Group 1:
<arg1> --opt1 [--opt2=ARG2]

Group2: command2 | command3

Their format is described below; other text is ignored.

Usage pattern format
Expand Down Expand Up @@ -243,6 +250,9 @@ Each pattern can consist of the following elements:
conventions of ``--options`` or ``<arguments>`` or ``ARGUMENTS``,
plus two special commands: dash "``-``" and double dash "``--``"
(see below).
- **-groups-**. Groups are words that start and end with a dash (``-``), e.g.
``-my_group-``. Every group defined in usage patterns has to be
described in its own section. See "Group description format" below.

Use the following constructs to specify patterns:

Expand Down Expand Up @@ -360,6 +370,73 @@ The rules are as follows:
# will be './here ./there', because it is not repeatable
--not-repeatable=<arg> [default: ./here ./there]

Group descriptions format
----------------------------------------------------------------------

The only function of groups is to make usage patterns more readable to
humans. Under the hood, docopt will replace group elements (e.g.
``-my_group-``) with their respective patterns.

**Group description** has to define a pattern of argument, option, and
command elements. Group elements withing groups are not allowed.

::

My Group: --an_option | (--another_option | command) [-o <arg>]

Case for group names is irrelevant. Underscores (``_``) in group elements
are translated to spaces when looking for group description.

It is possible to span pattern definitions on multiple lines. This
definition is equivalent to the previous example::

My Group:
--an_option |
(--another_option | command)
[-o <arg>]

Since groups are just readability replacements for other patterns,
they can be enclosed in optional or required parenthesis, etc.
These are all valid usage patterns using groups::

Usage: prog [-v] -input- [-out_file- | (-out_db- [--create])]

Input: <in_file>

Out File: <out_file>

Out DB:
<db_name>
[-u USERNAME [-p PASSWORD]]
[<host>]

Options:
...

The indentation is completely optional, at all levels, and has no relevance
to finding definitions. However, it does make the usage instructions more
readable, and is therefore encouraged.

Also, group descriptions can be placed below or above "Options" section, the
order bears has relevance to parsing.

Similarly to Options, group patterns can have descriptions on every line,
separated by at least two spaces. Unlike with Options, having comments on a
separate line is not supported::

Out DB:
<db_name> database name # GOOD, 2 spaces
[-u USERNAME [-p PASSWORD]]
database credentials # BAD, will be mistaken for a pattern!
[<host>] local or remote host name # BAD, has only 1 space!


Every group that is defined in usage patterns (e.g. ``-my_group-``) must
also be described.

Avoid naming your groups "Options" or "Usage", because that will collide
with other Docopt features.

Examples
----------------------------------------------------------------------

Expand Down
25 changes: 23 additions & 2 deletions docopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,27 @@ def parse_argv(tokens, options, options_first=False):
return parsed


def parse_groups(usage, doc):
original_usage = usage
placeholder_re = re.compile(r'(?=[\s\t[(]-([a-zA-Z0-9\-_]+)-([\s\t\])]|$))')
matches = placeholder_re.finditer(usage)
for match in matches:
group_name = match.group(1)
group_lines = parse_section('%s:' % group_name.replace('_', ' '), doc)
try:
group_lines = group_lines[0].strip().partition(":")[2].split('\n')
except IndexError:
raise DocoptLanguageError(
'group "%s:" (case-insensitive) not found.' % group_name)
group_pattern = ' '.join(
line.strip().partition(' ')[0]
for line in group_lines if line.strip())
usage = usage.replace('-%s-' % group_name,
'(%s)' % group_pattern)
doc = doc.replace('\n'.join(group_lines), '')
return usage, doc


def parse_defaults(doc):
defaults = []
for s in parse_section('options:', doc):
Expand Down Expand Up @@ -558,9 +579,9 @@ def docopt(doc, argv=None, help=True, version=None, options_first=False):
if len(usage_sections) > 1:
raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
DocoptExit.usage = usage_sections[0]

usage, doc = parse_groups(usage_sections[0], doc)
options = parse_defaults(doc)
pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
pattern = parse_pattern(formal_usage(usage), options)
# [default] syntax for argument is disabled
#for a in pattern.flat(Argument):
# same_name = [d for d in arguments if d.name == a.name]
Expand Down
37 changes: 37 additions & 0 deletions examples/file_parser_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
File Parser the Mighty!

Process all files in a path and output data to a CSV file or a MySQL database.

Usage: file_parser_example.py -input- -output- -misc-
file_parser_example.py -h --help

Input:
-f FILE | (-d DIR [-r])

Output:
-o CSV | (-b DB -u USER -p PASS)

Misc:
[-v...] affects only stdout, not the log file
-l LOG log file is required

Options:
-l LOG, --log_file=LOG path to log file [default: 123]
-f FLE, --input_file=FILE path to a file to parse
-d DIR, --input_dir=DIR path to input directory
-r, --recursive look for files in all subdirectories
-o CSV, --output_file=CSV path to output CSV file
-b DB, --database=DB name of MySQL database
-u USER, --username=USER username for access to database
-p PASS, --password=PASS password for USER
-v verbosity for stdout, specify 0 to 4 times for different levels
-h --help this help message

"""
from docopt import docopt


if __name__ == '__main__':
arguments = docopt(__doc__)
print(arguments)
40 changes: 40 additions & 0 deletions test_docopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,3 +614,43 @@ def test_parse_section():
def test_issue_126_defaults_not_parsed_correctly_when_tabs():
section = 'Options:\n\t--foo=<arg> [default: bar]'
assert parse_defaults(section) == [Option(None, '--foo', 1, 'bar')]


def test_pattern_groups():
doc = """Usage: prog -group1-

Group1:
--foo
"""
with raises(DocoptExit):
docopt(doc, '')


def test_pattern_groups_one_missing():
doc = """Usage: prog -group_1- -group33-

Group 1:
--foo

Group-2:
baz | (--bar | --bap)
"""
try:
docopt(doc)
except DocoptLanguageError as e:
assert 'group "group33:"' in e.message
assert 'not found.' in e.message


def test_pattern_groups_dont_recurse():
doc = """Usage: prog -group1-

Group1:
--foo | -group1- | bar

"""
try:
a = docopt(doc, 'bar')
except RuntimeError as e:
assert False, '{}'.format(e)
assert a['bar'] == True and '-group1-' not in a
76 changes: 76 additions & 0 deletions testcases.docopt
Original file line number Diff line number Diff line change
Expand Up @@ -955,3 +955,79 @@ other options:
"""
$ prog --baz --egg
{"--foo": false, "--baz": true, "--bar": false, "--egg": true, "--spam": false}

r"""Usage: prog (((--foo))) ((((baz | (--bar | --bap)))))
"""
$ prog --foo baz
{"--bar": false, "--bap": false, "baz": true, "--foo": true}

r"""Usage: prog -group1- -group_2-

Group1: --foo

Group 2:
--bar | --baz
"""
$ prog --foo --baz
{"--foo": true, "--baz": true, "--bar": false}

r"""Usage: prog -group1- -group_2-

Group1: --foo descriotion of foo

Group 2:
--bar | --baz description of baz and bar
"""
$ prog --foo --baz
{"--foo": true, "--baz": true, "--bar": false}

r"""Usage: prog -group1- -group_2-

Groups:

Group1: --foo

Group 2:
--bar | --baz
"""
$ prog --foo --baz
{"--foo": true, "--baz": true, "--bar": false}

r"""Usage: prog -group1- -group-2-

Group1: --foo

Group-2:
--bar | --baz
"""
$ prog --foo --baz
{"--foo": true, "--baz": true, "--bar": false}

r"""Usage: prog -group1- -common_options- [options]

Group1: --foo

Common Options:
--bar | --baz

Options:
-a
-b
"""
$ prog --foo --baz -a
{"--foo": true, "--baz": true, "--bar": false, "-a": true, "-b": false}

r"""Usage: prog -group1- -common_options- [options]

Group1: --foo

Options:
-a
-b

Common Options:
--bar | --baz

"""
$ prog --foo --baz -a
{"--foo": true, "--baz": true, "--bar": false, "-a": true, "-b": false}