How to show help for all subparsers in argparse? - python

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

Related

Python: how to add '-help' to argparse help command list?

Is there a way to include '-help' command to argparse help list?
I wish to have something like this on output, if i am typing '-help'.
optional arguments:
-h, -help, --help show this help message and exit
Thanks
As #Akaisteph7 suggested:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-help', action="help", help="second help :)")
parser.add_argument('-f', '--foo')
parser.print_help()
0945:~/mypy$ python3 stack57058526.py
usage: stack57058526.py [-h] [-help] [-f FOO]
optional arguments:
-h, --help show this help message and exit
-help second help :)
-f FOO, --foo FOO
Changing to:
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-h','--help','-help', action="help", help="replacement help")
0946:~/mypy$ python3 stack57058526.py
usage: stack57058526.py [-h] [-f FOO]
optional arguments:
-h, --help, -help replacement help
-f FOO, --foo FOO
Adding the '-help' flag to the default help requires modifying a couple of 'private' attributes:
parser = argparse.ArgumentParser()
parser._actions[0].option_strings += ['-help']
parser._option_string_actions['-help'] = parser._option_string_actions['-h']
0947:~/mypy$ python3 stack57058526.py
usage: stack57058526.py [-h] [-f FOO]
optional arguments:
-h, --help, -help show this help message and exit
-f FOO, --foo FOO
If you want to build this change into your local version of argparse, you could modify this block of code in the ArgumentParser.__init__ method:
if self.add_help:
self.add_argument(
default_prefix+'h', default_prefix*2+'help',
action='help', default=SUPPRESS,
help=_('show this help message and exit'))
Whether you change a local copy of argparse.py, or subclass ArgumentParser is up to you.
While this is possible to do, it is not recommended. Single dashes are only meant to be used with single letters. In general, you should follow recommendations as they are there for a reason.
If you really want to add it however, you can do it with:
parser.add_argument("-help", action="help")
argparse can work with any prefix characters.
For instance to support POSIX, Cmd.exe and PowerShell-type queries you could use:
p = ArgumentParser(prefix_chars="-/", add_help=False)
p.add_argument("-help", "--help", "-h", "/?", action="help")
Remember to use add_help=False or you'll have two help commands!

Using only one argument in argparse for multiple cases

$ script.py status
$ script.py terminate
$ script.py tail /tmp/some.log
As you can see the script can perform 3 tasks. The last task requires an additional argument (path of the file).
I want to use add_argument only once like below.
parser.add_argument("command")
And then check what command was requested by user and create conditionals based upon the same. If the command is tail I need to access the next argument (file path)
You might have to create a sub-parser for each command. This way it is extendable if those other commands also need arguments at some point. Something like this:
import argparse
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='global optional argument')
subparsers = parser.add_subparsers(dest="command", help='sub-command help')
# create the parser for the "status" command
parser_status = subparsers.add_parser('status', help='status help')
# create the parser for the "tail" command
parser_tail = subparsers.add_parser('tail', help='tail help')
parser_tail.add_argument('path', help='path to log')
print parser.parse_args()
The dest keyword of the add_subparsers ensures that you can still get the command name afterwards, as explained here and in the documentation.
Example usage:
$ python script.py status
Namespace(command='status', foo=False)
$ python script.py tail /tmp/some.log
Namespace(command='tail', foo=False, path='/tmp/some.log')
Note that any global argument needs to come before the command:
$ python script.py tail /tmp/some.log --foo
usage: PROG [-h] [--foo] {status,tail} ...
PROG: error: unrecognized arguments: --foo
$ python script.py --foo tail /tmp/some.log
Namespace(command='tail', foo=True, path='/tmp/some.log')

How to use argsparse so the script options with the same affect will appear in the same 'help' line

I'm using argsparse to parse the options passed to my python scripts.
I want to enable passing '-a', and to allow passing '-b' with the same affect.
No problem, I'll call parser.add_argument() twice, with the same description:
parser.add_argument('-a', help='do something')
parser.add_argument('-b', help='do something')
But now when displaying the script help, I will see both, as such:
-a do something
-b do something
This is ugly.
I would prefer to have {-a, -b} or {-a|b).
I could not find in argsparse documentation any way around this (admittedly, not critical) issue.
You could try to pass both arguments to the same add_argument call:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-a', '-b', help='do something')
parser.parse_args(['--help'])
output
usage: test.py [-h] [-a A]
optional arguments:
-h, --help show this help message and exit
-a A, -b A do something

Grouping argparse subparser arguments

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()

Writing a help for python script

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.

Categories

Resources