I have a script that has multiple commands, with each command taking it's own set of required and/or optional arguments, by using add_subparser.
=->test.py -h
usage: test.py [-h] <command> ...
positional arguments:
<command> Available Commands
cmd1 Command 1
cmd2 Command 2
cmd3 Command 3
cmd4 Command 4
optional arguments:
-h, --help show this help message and exit
=->test.py cmd1 -h
usage: test.py cmd1 [-h] --flag1 FLAG1
optional arguments:
-h, --help show this help message and exit
--flag1 FLAG1 Test flag
=->test.py cmd2 -h
usage: test.py cmd2 [-h] [--flag2 FLAG2]
optional arguments:
-h, --help show this help message and exit
--flag2 FLAG2 Test flag
I'd like to somehow separate these commands into groups so that users see something like the following:
=->test.py -h
usage: test.py [-h] <command> ...
First Group:
cmd1 Command 1
cmd2 Command 2
Second Group:
cmd3 Command 3
cmd4 Command 4
optional arguments:
-h, --help show this help message and exit
But, doesn't look like add_argument_group and add_subparsers work together.
Any way to achieve this?
You are right, argument groups and subparsers don't work together. That's because subparsers (or rather their names) are not arguments.
The sp = parser.add_subparsers(...) command creates an argument, or technically an instance of a subclass of argparse.Action. This is a positional argument. The add_parser command creates a parser object (i.e. calls argparse.ArgumentParser), and adds it, along with its name (and aliases) to a dictionary owned by this action. And the names populate the choices attribute of the Action.
That subparsers Action could belong to an argument group, but since there can only be one such action, it doesn't help you group the help lines.
You can control, to some extent, the help by using a description, and omitting the help for subparsers
import argparse
description = """
First Group:
cmd1 Command 1
cmd2 Command 2
Second Group:
cmd3 Command 3
cmd4 Command 4"""
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
sp = parser.add_subparsers(title='commands',description=description)
sp.add_parser('cmd1')
sp.add_parser('cmd2')
sp.add_parser('cmd3')
sp.add_parser('cmd4')
parser.print_help()
produces
1343:~/mypy$ python stack32017020.py
usage: stack32017020.py [-h] {cmd1,cmd2,cmd3,cmd4} ...
optional arguments:
-h, --help show this help message and exit
commands:
First Group:
cmd1 Command 1
cmd2 Command 2
Second Group:
cmd3 Command 3
cmd4 Command 4
{cmd1,cmd2,cmd3,cmd4}
http://bugs.python.org/issue9341 - allow argparse subcommands to be grouped
talks about doing what you want. The patch that I proposed isn't trivial. But you are welcome to test it.
I had this problem, and resolved it by creating a group in the parent parser, and a function where it creates a dummy group for each subparser of a given subparsers, and it replaces the object of the dummy group by the wanted group, while keeping its address, so the group does belong to each subparser !
Here is an example with a little bit of context :
import argparse
import ctypes
parent_parser = argparse.ArgumentParser(description="Redacted")
subparsers = parent_parser.add_subparsers()
subparser_email = subparsers.add_parser("email", parents=[parent_parser], add_help=False)
subparser_gen = subparsers.add_parser("gen", parents=[parent_parser], add_help=False)
group_emails = subparser_email.add_argument_group("Main inputs")
group_gen = subparser_gen.add_argument_group("Emails generation")
group_matchers = parent_parser.add_argument_group("Matchers") # The group we want on each subparser
[...] # add the arguments to your groups here
def add_group_to_subparsers(group: argparse._ArgumentGroup, subparsers: argparse._SubParsersAction, name: str):
for name, subparser in subparsers._name_parser_map.items():
dummy_group = subparser.add_argument_group(name)
ctypes.memmove(id(dummy_group), id(group), object.__sizeof__(dummy_group))
add_group_to_subparsers(group_matchers, subparsers, "Matchers")
parent_parser.parse_args()
Related
What I need is:
pro [-a xxx | [-b yyy -c zzz]]
I tried this but does not work. Could someone help me out?
group= parser.add_argument_group('Model 2')
group_ex = group.add_mutually_exclusive_group()
group_ex.add_argument("-a", type=str, action = "store", default = "", help="test")
group_ex_2 = group_ex.add_argument_group("option 2")
group_ex_2.add_argument("-b", type=str, action = "store", default = "", help="test")
group_ex_2.add_argument("-c", type=str, action = "store", default = "", help="test")
Thanks!
add_mutually_exclusive_group doesn't make an entire group mutually exclusive. It makes options within the group mutually exclusive.
What you're looking for is subcommands. Instead of prog [ -a xxxx | [-b yyy -c zzz]], you'd have:
prog
command 1
-a: ...
command 2
-b: ...
-c: ...
To invoke with the first set of arguments:
prog command_1 -a xxxx
To invoke with the second set of arguments:
prog command_2 -b yyyy -c zzzz
You can also set the sub command arguments as positional.
prog command_1 xxxx
Kind of like git or svn:
git commit -am
git merge develop
Working Example
# create the top-level parser
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='help for foo arg.')
subparsers = parser.add_subparsers(help='help for subcommand', dest="subcommand")
# create the parser for the "command_1" command
parser_a = subparsers.add_parser('command_1', help='command_1 help')
parser_a.add_argument('a', type=str, help='help for bar, positional')
# create the parser for the "command_2" command
parser_b = subparsers.add_parser('command_2', help='help for command_2')
parser_b.add_argument('-b', type=str, help='help for b')
parser_b.add_argument('-c', type=str, action='store', default='', help='test')
Test it
>>> parser.print_help()
usage: PROG [-h] [--foo] {command_1,command_2} ...
positional arguments:
{command_1,command_2}
help for subcommand
command_1 command_1 help
command_2 help for command_2
optional arguments:
-h, --help show this help message and exit
--foo help for foo arg.
>>>
>>> parser.parse_args(['command_1', 'working'])
Namespace(subcommand='command_1', a='working', foo=False)
>>> parser.parse_args(['command_1', 'wellness', '-b x'])
usage: PROG [-h] [--foo] {command_1,command_2} ...
PROG: error: unrecognized arguments: -b x
Good luck.
While Jonathan's answer is perfectly fine for complex options, there is a very simple solution which will work for the simple cases, e.g. 1 option excludes 2 other options like in
command [- a xxx | [ -b yyy | -c zzz ]]
or even as in the original question:
pro [-a xxx | [-b yyy -c zzz]]
Here is how I would do it:
parser = argparse.ArgumentParser()
# group 1
parser.add_argument("-q", "--query", help="query")
parser.add_argument("-f", "--fields", help="field names")
# group 2
parser.add_argument("-a", "--aggregation", help="aggregation")
I am using here options given to a command line wrapper for querying a mongodb. The collection instance can either call the method aggregate or the method find with to optional arguments query and fields, hence you see why the first two arguments are compatible and the last one isn't.
So now I run parser.parse_args() and check it's content:
args = parser.parse_args()
if args.aggregation and (args.query or args.fields):
print "-a and -q|-f are mutually exclusive ..."
sys.exit(2)
Of course, this little hack is only working for simple cases and it would become a nightmare to check all the possible options if you have many mutually exclusive options and groups. In that case you should break your options in to command groups like Jonathan suggested.
There is a python patch (in development) that would allow you to do this.
http://bugs.python.org/issue10984
The idea is to allow overlapping mutually exclusive groups. So usage might look like:
pro [-a xxx | -b yyy] [-a xxx | -c zzz]
Changing the argparse code so you can create two groups like this was the easy part. Changing the usage formatting code required writing a custom HelpFormatter.
In argparse, action groups don't affect the parsing. They are just a help formatting tool. In the help, mutually exclusive groups only affect the usage line. When parsing, the parser uses the mutually exclusive groups to construct a dictionary of potential conflicts (a can't occur with b or c, b can't occur with a, etc), and then raises an error if a conflict arises.
Without that argparse patch, I think your best choice is to test the namespace produced by parse_args yourself (e.g. if both a and b have nondefault values), and raise your own error. You could even use the parser's own error mechanism.
parser.error('custom error message')
If you don't want subparsers, this can currently be done with mutually exclusive groups, but fair warning, it involves accessing private variables so use it at your own risk. The idea is you want -a to be mutually exclusive with -b and -c, but -b and -c don't want to be mutually exclusive with each other
import argparse
p = argparse.ArgumentParser()
# first set up a mutually exclusive group for a and b
g1 = p.add_mutually_exclusive_group()
arg_a = g1.add_argument('-a') # save this _StoreAction for later
g1.add_argument('-b')
# now set up a second group for a and c
g2 = p.add_mutually_exclusive_group()
g2.add_argument('-c')
g2._group_actions.append(arg_a) # this is the magic/hack
Now we've got -a exclusive to both -c and -b.
a = p.parse_args(['-a', '1'])
# a.a = 1, a.b = None, a.c = None
a = p.parse_args(['-a', '1', '-b', '2'])
# usage: prog.py [-h] [-a A | -b B] [-c C]
# prog.py: error: argument -b: not allowed with argument -a
Note, it does mess up the help message, but you could probably override that, or just ignore it because you've got the functionality you want, which is probably more important anyway.
If you want to ensure if we're using any of b and c, we have to use both of them, then simply add the required=True keyword arg when instantiating the mutually exclusive groups.
In my CLI script I am using argparse to take in a few optional arguments and then a positional argument. The positional argument is used to determine a subparser to use which in turn runs a function that calls an external program that takes its own arguments. So, the command-line usage looks something like this:
myscript [OPTIONS] subcommand [SUBCOMMAND_OPTIONS]
Now my problem is that there are conflicts between my OPTIONS I've declared and the SUBCOMMAND_OPTIONS declared in the external program. The easy fix is to ensure I rename all conflicts in myscript but I can't do this for all options - most notably the "-h" option for help. Ideally I'd like argparse to stop parsing immediately after it encounters the subcommand and simply pass on the rest of the args to the external program.
So, the following invocation should show the help text for myscript:
myscript -h
While, in contrast the following should show the help text from the external program invoked by the "bar" subparser:
myscript --foo bar -h
Some more code to make the above clearer:
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo', action='store_true')
>>> subparsers = parser.add_subparsers()
>>> subparsers.add_parser("bar")
>>> parser.parse_known_args("--foo bar --test".split())
(Namespace(foo=True), ['--test'])
# cool - this is what I want, I'll just pass --test on to the external program
>>> parser.parse_known_args("--foo bar -h".split())
usage: bar [-h]
optional arguments:
-h, --help show this help message and exit
# unfortunately the above argparse help message is NOT what I wanted, instead I was looking for the result below:
(Namespace(foo=True), ['-h'])
>>> parser.parse_known_args("bar --test -- -h".split())
# this works, sort of, it requires educating the end-user to use the '--' parameter and I'd like to avoid that if possible.
(Namespace(foo=False), ['--test', '--', '-h'])
Your initial description was sufficiently close to subparsers that it takes some careful reading to identify what's wrong (for you).
From comments it sounds like the biggest fault is that the subparser captures the -h give you a help message, rather than passing it through to the extras. Subparsers, just like a main parser, takes a add_help=False parameter.
p=argparse.ArgumentParser()
p.add_argument('foo')
p.add_argument('--bar')
sp=p.add_subparsers(dest='cmd')
sp1=sp.add_parser('cmd1') # with a subparser help
sp2=sp.add_parser('cmd2', add_help=False) # will ignore -h
producing
p.parse_known_args('-h'.split()) # top level help
p.parse_known_args('--bar xxx foo cmd1 -h'.split())
# usage: ipython foo cmd1 [-h]
# exit msg
p.parse_known_args('--bar xxx foo cmd2 -h'.split())
# (Namespace(bar='xxx', cmd='cmd2', foo='foo'), ['-h'])
p.parse_known_args('foo cmd2 test -o --more --bar xxx'.split())
# (Namespace(bar=None, cmd='cmd2', foo='foo'),
# ['test', '-o', '--more', '--bar', 'xxx'])
In a comment I mentioned a couple of nargs values, argparse.PARSER and argparse.REMAINDER. To the main parser, subparsers are just a positional with a PARSER nargs (and choices). It's a specialaction` type, which goes on to invoke another parser based on the 1st value.
REMAINDER is like the * nargs, except that it takes everything, even strings that look like flags. PARSER is like +, requiring at least one string.
p=argparse.ArgumentParser()
p.add_argument('foo')
p.add_argument('--bar')
p.add_argument('rest', nargs=argparse.REMAINDER)
producing
In [32]: p.parse_args('--bar yyy foo'.split())
Out[32]: Namespace(bar='yyy', foo='foo', rest=[])
In [33]: p.parse_args('--bar yyy foo -h'.split())
Out[33]: Namespace(bar='yyy', foo='foo', rest=['-h'])
In [34]: p.parse_args('--bar yyy foo cmd2 test -o --more --bar xxx'.split())Out[34]: Namespace(bar='yyy', foo='foo', rest=['cmd2', 'test', '-o', '--more', '--bar', 'xxx'])
The REMAINDER note in the argparse docs is:
argparse.REMAINDER. All the remaining command-line arguments are gathered into a list. This is commonly useful for command line utilities that dispatch to other command line utilities:
and has an example similar to my last one.
>>> parser = argparse.ArgumentParser(prog='PROG')
>>> parser.add_argument('--foo')
>>> parser.add_argument('command')
>>> parser.add_argument('args', nargs=argparse.REMAINDER)
>>> print(parser.parse_args('--foo B cmd --arg1 XX ZZ'.split()))
Namespace(args=['--arg1', 'XX', 'ZZ'], command='cmd', foo='B')
The normal sub-commands support in argparse does this just fine. Just note that when reusing argument names, you should specify a custom dest to make sure that your main commands’ values are not overwritten:
parser = argparse.ArgumentParser()
parser.add_argument('--foo', action='store_true')
subparsers = parser.add_subparsers()
parser_foo = subparsers.add_parser('foo')
parser_foo.add_argument('--foo', dest='foo_foo')
parser_foo.add_argument('--bar', dest='foo_bar')
parser_bar = subparsers.add_parser('bar')
parser_bar.add_argument('--foo', dest='bar_foo')
Examples:
>>> parser.parse_args('-h'.split())
usage: [-h] [--foo] {foo,bar} ...
positional arguments:
{foo,bar}
optional arguments:
-h, --help show this help message and exit
--foo
>>> parser.parse_args('foo -h'.split())
usage: foo [-h] [--foo FOO_FOO] [--bar FOO_BAR]
optional arguments:
-h, --help show this help message and exit
--foo FOO_FOO
--bar FOO_BAR
>>> parser.parse_args('bar -h'.split())
usage: bar [-h] [--foo BAR_FOO]
optional arguments:
-h, --help show this help message and exit
--foo BAR_FOO
>>> parser.parse_args('--foo foo --foo test --bar baz'.split())
Namespace(foo=True, foo_bar='baz', foo_foo='test')
The arpgarse module has a subparser feature, which you can combine with argparse.REMAINDER to capture any arguments without declaring those explicitly.
Update:
If you want to have more control than argparse provides, it might be worthwile to look into the click package instead. It has special support for ignoring unknown options and preventing options like --help to be handled. Details at
http://click.pocoo.org/4/api/#click.Context.ignore_unknown_options
What I need is:
pro [-a xxx | [-b yyy -c zzz]]
I tried this but does not work. Could someone help me out?
group= parser.add_argument_group('Model 2')
group_ex = group.add_mutually_exclusive_group()
group_ex.add_argument("-a", type=str, action = "store", default = "", help="test")
group_ex_2 = group_ex.add_argument_group("option 2")
group_ex_2.add_argument("-b", type=str, action = "store", default = "", help="test")
group_ex_2.add_argument("-c", type=str, action = "store", default = "", help="test")
Thanks!
add_mutually_exclusive_group doesn't make an entire group mutually exclusive. It makes options within the group mutually exclusive.
What you're looking for is subcommands. Instead of prog [ -a xxxx | [-b yyy -c zzz]], you'd have:
prog
command 1
-a: ...
command 2
-b: ...
-c: ...
To invoke with the first set of arguments:
prog command_1 -a xxxx
To invoke with the second set of arguments:
prog command_2 -b yyyy -c zzzz
You can also set the sub command arguments as positional.
prog command_1 xxxx
Kind of like git or svn:
git commit -am
git merge develop
Working Example
# create the top-level parser
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='help for foo arg.')
subparsers = parser.add_subparsers(help='help for subcommand', dest="subcommand")
# create the parser for the "command_1" command
parser_a = subparsers.add_parser('command_1', help='command_1 help')
parser_a.add_argument('a', type=str, help='help for bar, positional')
# create the parser for the "command_2" command
parser_b = subparsers.add_parser('command_2', help='help for command_2')
parser_b.add_argument('-b', type=str, help='help for b')
parser_b.add_argument('-c', type=str, action='store', default='', help='test')
Test it
>>> parser.print_help()
usage: PROG [-h] [--foo] {command_1,command_2} ...
positional arguments:
{command_1,command_2}
help for subcommand
command_1 command_1 help
command_2 help for command_2
optional arguments:
-h, --help show this help message and exit
--foo help for foo arg.
>>>
>>> parser.parse_args(['command_1', 'working'])
Namespace(subcommand='command_1', a='working', foo=False)
>>> parser.parse_args(['command_1', 'wellness', '-b x'])
usage: PROG [-h] [--foo] {command_1,command_2} ...
PROG: error: unrecognized arguments: -b x
Good luck.
While Jonathan's answer is perfectly fine for complex options, there is a very simple solution which will work for the simple cases, e.g. 1 option excludes 2 other options like in
command [- a xxx | [ -b yyy | -c zzz ]]
or even as in the original question:
pro [-a xxx | [-b yyy -c zzz]]
Here is how I would do it:
parser = argparse.ArgumentParser()
# group 1
parser.add_argument("-q", "--query", help="query")
parser.add_argument("-f", "--fields", help="field names")
# group 2
parser.add_argument("-a", "--aggregation", help="aggregation")
I am using here options given to a command line wrapper for querying a mongodb. The collection instance can either call the method aggregate or the method find with to optional arguments query and fields, hence you see why the first two arguments are compatible and the last one isn't.
So now I run parser.parse_args() and check it's content:
args = parser.parse_args()
if args.aggregation and (args.query or args.fields):
print "-a and -q|-f are mutually exclusive ..."
sys.exit(2)
Of course, this little hack is only working for simple cases and it would become a nightmare to check all the possible options if you have many mutually exclusive options and groups. In that case you should break your options in to command groups like Jonathan suggested.
There is a python patch (in development) that would allow you to do this.
http://bugs.python.org/issue10984
The idea is to allow overlapping mutually exclusive groups. So usage might look like:
pro [-a xxx | -b yyy] [-a xxx | -c zzz]
Changing the argparse code so you can create two groups like this was the easy part. Changing the usage formatting code required writing a custom HelpFormatter.
In argparse, action groups don't affect the parsing. They are just a help formatting tool. In the help, mutually exclusive groups only affect the usage line. When parsing, the parser uses the mutually exclusive groups to construct a dictionary of potential conflicts (a can't occur with b or c, b can't occur with a, etc), and then raises an error if a conflict arises.
Without that argparse patch, I think your best choice is to test the namespace produced by parse_args yourself (e.g. if both a and b have nondefault values), and raise your own error. You could even use the parser's own error mechanism.
parser.error('custom error message')
If you don't want subparsers, this can currently be done with mutually exclusive groups, but fair warning, it involves accessing private variables so use it at your own risk. The idea is you want -a to be mutually exclusive with -b and -c, but -b and -c don't want to be mutually exclusive with each other
import argparse
p = argparse.ArgumentParser()
# first set up a mutually exclusive group for a and b
g1 = p.add_mutually_exclusive_group()
arg_a = g1.add_argument('-a') # save this _StoreAction for later
g1.add_argument('-b')
# now set up a second group for a and c
g2 = p.add_mutually_exclusive_group()
g2.add_argument('-c')
g2._group_actions.append(arg_a) # this is the magic/hack
Now we've got -a exclusive to both -c and -b.
a = p.parse_args(['-a', '1'])
# a.a = 1, a.b = None, a.c = None
a = p.parse_args(['-a', '1', '-b', '2'])
# usage: prog.py [-h] [-a A | -b B] [-c C]
# prog.py: error: argument -b: not allowed with argument -a
Note, it does mess up the help message, but you could probably override that, or just ignore it because you've got the functionality you want, which is probably more important anyway.
If you want to ensure if we're using any of b and c, we have to use both of them, then simply add the required=True keyword arg when instantiating the mutually exclusive groups.
I have made a Python script that is doing a lot of actions, so it has many options, so I divided it to subparsers that also use parent parsers for common options grouping.
I want a help option that will show the help for all commands with their options, is it possible without overriding the format_help method?
I saw a similar question, but the grouping is not critical for me, I just want the options there.
For example:
general_group = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,add_help=False)
general_group.add_argument('--threads', action='store_true', default=False)
second_group = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,add_help=False)
second_group.add_argument('--sleep', action='store', default=60, type=int)
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
subparsers=parser.add_subparsers(dest='action')
subparsers.add_parser('Restart',parents=[general_group,second_group])
subparsers.add_parser('Start',parents=[general_group])
args = parser.parse_args()
In this case I would like that if someone runs ./script.py -h they'll see the threads option in the help.
The problem is that in the lines:
subparsers=parser.add_subparsers(dest='action')
subparsers.add_parser('Restart',parents=[general_group,second_group])
subparsers.add_parser('Start',parents=[general_group])
You are adding general_group as parent to the subparsers, so the main parser does not know about them, which results in ./script.py -h to not show --threads. If you plan to put it as parent of all the subparsers then you should put it as top parser parent:
parser = argparse.ArgumentParser(parents=[general_group])
subparsers=parser.add_subparsers(dest='action')
subparsers.add_parser('Restart',parents=[second_group])
subparsers.add_parser('Start')
Which results in:
$ python script.py -h
usage: script.py [-h] [--threads] {Restart,Start} ...
positional arguments:
{Restart,Start}
optional arguments:
-h, --help show this help message and exit
--threads
Note however that in this case the option is part only of the parent parser and not the subparsers, which means that the following:
$python script.py --threads Start
is correct, while:
$ python script.py Start --threads
usage: script.py [-h] [--threads] {Restart,Start} ...
script.py: error: unrecognized arguments: --threads
Because --threads is not "inherited" by the subparser. If you want to have --threads also in the subparser you must specify it in its parents argument:
parser = argparse.ArgumentParser(parents=[general_group])
subparsers=parser.add_subparsers(dest='action')
subparsers.add_parser('Restart',parents=[general_group, second_group])
subparsers.add_parser('Start', parents=[general_group])
This should do what you want:
$ python script.py -h
usage: script.py [-h] [--threads] {Restart,Start} ...
positional arguments:
{Restart,Start}
optional arguments:
-h, --help show this help message and exit
--threads
$ python script.py Start -h
usage: script.py Start [-h] [--threads]
optional arguments:
-h, --help show this help message and exit
--threads
I am trying to make my python script very user friendly, so I like to write some sort of help for it. What is your advise for this? I could just put in some logic that if the user passed help as a paramater to the script, they get help. Is there a best practise or convention for this?
Use argparse.
For example, with test.py:
import argparse
parser=argparse.ArgumentParser(
description='''My Description. And what a lovely description it is. ''',
epilog="""All is well that ends well.""")
parser.add_argument('--foo', type=int, default=42, help='FOO!')
parser.add_argument('bar', nargs='*', default=[1, 2, 3], help='BAR!')
args=parser.parse_args()
Running
% test.py -h
yields
usage: test.py [-h] [--foo FOO] [bar [bar ...]]
My Description. And what a lovely description it is.
positional arguments:
bar BAR!
optional arguments:
-h, --help show this help message and exit
--foo FOO FOO!
All is well that ends well.
Best practice is to use argparse to handle all your commandline arguments. It includes a default --help which you can customize to your likings.
Here's the simplest example:
import argparse
parser = argparse.ArgumentParser(description='This is my help')
args = parser.parse_args()
Which results in:
% python argparse_test.py -h
usage: argparse_test.py [-h]
This is my help
optional arguments:
-h, --help show this help message and exit
You can define all your arguments with argparse and set a help message for each one of them. The resulting filtered/validated arguments are returned by parser.parse_args().
An alternative to the built-in argparse is a 3rd-party package called Click which features "automatic help page generation" and "arbitrary nesting of commands" (which also produces nested help pages). Internally, it's based on argparse, but, for me, makes the creation of complex CLI more convenient using decorators.
Here's a sample code:
import click
#click.command()
#click.argument("things", nargs=-1)
#click.option("-t", show_default=True, default="int", help="Data type")
#click.option("-o", help="Output format")
def combine(things, t):
"""Combines things into a single element"""
pass
if __name__ == "__main__":
combine()
And the generated help page:
$ python myapp.py --help
Usage: myapp.py [OPTIONS] [THINGS]...
Combines things into a single element
Options:
-t TEXT Data type [default: int]
-o TEXT Output format
--help Show this message and exit.
One of the nice things about it is that it uses the method docstrings as part of the help page, which is convenient because the docstring can now be used both for developer documentation and for script usage help.
You can also have nested command groups:
import click
#click.command()
#click.argument("numbers", nargs=-1)
#click.option("-e", help="Extra option for add")
def add(numbers, e):
"""Adds numbers"""
print(f"This method should add {numbers}")
#click.command()
#click.argument("numbers", nargs=-1)
#click.option("-e", help="Extra option for mul")
def mul(numbers, e):
"""Multiplies numbers"""
print(f"This method should multiply {numbers}")
#click.group()
def calc():
pass
calc.add_command(add)
calc.add_command(mul)
if __name__ == "__main__":
calc()
And it will produce nested help pages:
$ python myapp.py --help
Usage: myapp.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
add Adds numbers
mul Multiplies numbers
$ python myapp.py add --help
Usage: myapp.py add [OPTIONS] [NUMBERS]...
Adds numbers
Options:
-e TEXT Extra option for add
--help Show this message and exit.
$ python myapp.py mul --help
Usage: myapp.py mul [OPTIONS] [NUMBERS]...
Multiplies numbers
Options:
-e TEXT Extra option for mul
--help Show this message and exit.
For more information, see the Documenting Scripts section of the docs.