After looking at about a dozen questions, I can't seem to find an answer.
I have a python CLI i've written using argparse. I have a main command that does nothing but regurgitate help text and then 4 subcommands. My boss wants a very specific output for the help text. He has me write it out as a text file and then we use that text file to display the help text.
However, in some circumstances, it STILL outputs parts of the argparse help text.
For example, if I run my program with no subcommands, it just outputs our help text from the file. But if I use "-h" or "--help" it will output our help text, followed by the list of positional and optional arguments and other argparse stuff. We don't want that.
I could use "add_help=False" but we want the user to be able to type -h and still get our help text. If we set add help to false, it will display our help text followed by the error "-h not recognized".
Also doing this does nothing for when the user uses -h after a subcommand. I set help=None and usage is set to my custom help text for each subcommand, but it still shows the boilerplate argparse info at the end.
This is what I want to happen: user types in the main command with no subcommands prints my custom help text and nothing else. The user types the main command, no subcommand, followed by -h/--help and it prints my custom help text and nothing else. User types in the main command, one of the subcommands, followed by -h/--help and it outputs my help text and nothing else. User types the main command, a subcommand, and then wrong arguments or too many/ too few arguments displays my help text and nothing else. Basically I only ever want it to print nothing, or print just my help text.
how do I do that? here is my main function where the parsers and subparsers are all configured:
def main():
# Import help text from file
p = Path(__file__).with_name("help.txt")
with p.open() as file:
help_text = file.read()
# Configure the top level Parser
parser = argparse.ArgumentParser(prog='myprog', description='My program', usage=help_text)
subparsers = parser.add_subparsers()
# Create Subparsers to give subcommands
parser_one = subparsers.add_parser('subcommandone', prog='subcommandone', usage=help_text, help=None)
parser_one.add_argument('arg1', type=str)
parser_one.add_argument('-o', '--option1', default='mydefault', type=str)
parser_two= subparsers.add_parser('subcommandtwo', usage=help_text, help=None, prog='subcommandtwo')
parser_three= subparsers.add_parser('subcommandthree', usage=help_text, help=None, prog='subcommandthree')
parser_four= subparsers.add_parser('subcommandfour', usage=help_text, help=None, prog='subcommandfour')
# Assign subparsers to their respective functions
parser_one.set_defaults(func=functionone)
parser_two.set_defaults(func=functiontwo)
parser_three.set_defaults(func=functionthree)
parser_four.set_defaults(func=functionfour)
parser.set_defaults(func=base_case)
# Parse the arguments and call appropriate functions
args = parser.parse_args()
if len(sys.argv) == 1:
args.func(args, parser)
else:
args.func(args)
Any thoughts?
You can use sys.exit() after the help text has been displayed, and before the parsing has begun, to avoid problems with "-h not recognized".
So anywhere before the line
# Parse the arguments and call appropriate functions
add
if len(sys.argv) == 1 or '-h' in sys.argv or '--help' in sys.argv:
print(help_text)
sys.exit(1)
In situations where that is not good enough you can subclass argparse.HelpFormatter like so
usage_help_str = 'myscript command [options]'
epilog_str = "More info can be found at https://..."
class Formatter(argparse.HelpFormatter):
# override methods and stuff
def formatter(prog):
return Formatter(prog)
parser = argparse.ArgumentParser(formatter_class=formatter, epilog=epilog_str, usage=usage_help_str, add_help=False)
I tried looking around for documentation on subclassing the helpFormatter, but I couldn't find anything. It looks like people are just looking at the source code to figure out how to subclass it.
Related
#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s')
parser.add_argument('--choice', '-c', default = argparse.SUPPRESS)
args = parser.parse_args()
def main(selection, choice):
print(selection)
print(choice)
if __name__=='__main__':
main(args.selection, args.choice)
The example provided is just to provide something simple and short that accurately articulates the actual problem I am facing in my project. My goal is to be able to ignore an argument within the code body when it is NOT typed into the terminal. I would like to be able to do this through passing the argument as a parameter for a function. I based my code off of searching 'suppress' in the following link: https://docs.python.org/3/library/argparse.html
When I run the code as is with the terminal input looking like so: python3 stackquestion.py -s cheese, I receive the following error on the line where the function is called:
AttributeError: 'Namespace' object has no attribute 'choice'
I've tried adding the following parameter into parser like so:
parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
I've also tried the above with
parser.add_argument('--choice', '-c')
But I get the same issue on the same line.
#Barmar answered this question in the comments. Using 'default = None' in parser.add_argument works fine; The code runs without any errors. I selected the anser from #BorrajaX because it's a simple solution to my problem.
According to the docs:
Providing default=argparse.SUPPRESS causes no attribute to be added if the command-line argument was not present:
But you're still assuming it will be there by using it in the call to main:
main(args.selection, args.choice)
A suppressed argument won't be there (i.e. there won't be an args.choice in the arguments) unless the caller specifically called your script adding --choice="something". If this doesn't happen, args.choice doesn't exist.
If you really want to use SUPPRESS, you're going to have to check whether the argument is in the args Namespace by doing if 'choice' in args: and operate accordingly.
Another option (probably more common) can be using a specific... thing (normally the value None, which is what argparse uses by default, anyway) to be used as a default, and if args.choice is None, then assume it hasn't been provided by the user.
Maybe you could look at this the other way around: You want to ensure selection is provided and leave choice as optional?
You can try to set up the arguments like this:
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s', required=True)
parser.add_argument('--choice', '-c')
args = parser.parse_args()
if __name__ == '__main__':
if args.choice is None:
print("No choice provided")
else:
print(f"Oh, the user provided choice and it's: {args.choice}")
print(f"And selection HAS TO BE THERE, right? {args.selection}")
I'm using argparse and I have various groups which have set of its own options.
Now with the --help option I do not want to show all the options by default. Only a set of groups options are to be shown for --help.
Other group options should be shown based on other help options, as --help_1, --help_2:
For example:
--help' to show Group 2 and 3
--help_1' to show Group 11 and 12
--help_2' to show Group 22 and 23
I know that we can disable the default --help option with using add_help=False but how do I get to display only selected group specific helps.
We can get the list of groups from the parser using _action_groups attribute, but they do not expose any print_help() option as such.
My sample code:
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--help_a', action='store_true')
parser.add_argument('--help_b', action='store_true')
group1 = parser.add_argument_group("Feature 1")
group1.add_argument('--foo1')
group2 = parser.add_argument_group("Feature 2")
group2.add_argument('--foo2')
group3 = parser.add_argument_group("Feature 3")
group3.add_argument('--foo3')
# TODO: --help_a to only print "Feature 1" groups help
# and --help_b to print Feature 2 and 3's help.
EDIT: Using subparser and adding parsers(instead of group) will solve the above. But subparser doesn't fit in my case, as I am parsing it always, I only need to customize help to be displayed.
Here's the custom format_help approach:
import argparse
def format_help(self, groups=None):
# self == parser
formatter = self._get_formatter()
# usage
formatter.add_usage(self.usage, self._actions,
self._mutually_exclusive_groups)
# description
formatter.add_text(self.description)
if groups is None:
groups = self._action_groups
# positionals, optionals and user-defined groups
for action_group in groups:
formatter.start_section(action_group.title)
formatter.add_text(action_group.description)
formatter.add_arguments(action_group._group_actions)
formatter.end_section()
# epilog
formatter.add_text(self.epilog)
# determine help from format above
return formatter.format_help()
<your parser>
args = parser.parse_args()
# _action_groups[:2] are the default ones
if args.help_a:
print(format_help(parser, [parser._action_groups[2]]))
parser.exit()
if args.help_b:
print(format_help(parser, parser._action_groups[3:]))
parser.exit()
Sample runs
1444:~/mypy$ python stack40718566.py --help_a
usage: stack40718566.py [-h] [--help_a] [--help_b] [--foo1 FOO1] [--foo2 FOO2]
[--foo3 FOO3]
Feature 1:
--foo1 FOO1
1444:~/mypy$ python stack40718566.py --help_b
usage: stack40718566.py [-h] [--help_a] [--help_b] [--foo1 FOO1] [--foo2 FOO2]
[--foo3 FOO3]
Feature 2:
--foo2 FOO2
Feature 3:
--foo3 FOO3
So it's just like the default format_help, except it takes a groups parameter. It could even replace the default method in an ArgumentParser subclass.
We could also create a custom Help Action class that behaves like the standard help, except that it takes some sort of group_list parameter. But this post-parsing action is simpler to code and test.
I recommend against what you are trying to do.
You are solving a problem that isn't yours to solve. It is the job of your script to return usage information. It isn't your problem if that is a lot of text. The thing that you could do, you are doing: Put arguments into groups that make sense for the user. But the amount of text is not a problem of data structure but of data presentation.
Secondly, you would be following a convention nobody is using. There usually are
man command
command --help
command subcommand --help
Anything else would be confusing to first time users.
Also, if you have a lot of argument groups a person would always need to consult --help to find out which --help_* they would have to consult next. This can be frustrating to users when you could just present it in --help right away.
If you use multiple help pages, you would prevent the reuse of your help text. Searching, for example: Multiple pages cannot be searched without switching between them manually.
The right way to do is pass text through a paginator like less. This allows users to read the text page by page, search through it (press /) or save it to file:
command --help | less
For convenience some commands, like git log, even check if the output is an interactive terminal and automatically pass the output through less. This would mean
command --help > help.txt
saves the help to file, while
command --help
shows the help text in pagination, and searchable.
So my recommendation for you on both Windows and UNIX is
import os
import sys
import argparse
import subprocess
def less(data):
if sys.stdout.isatty():
if os.name == 'posix':
cmd = "less"
elif os.name == 'nt':
cmd = "more"
process = subprocess.Popen([cmd], stdin=subprocess.PIPE)
try:
process.stdin.write(data)
process.communicate()
except IOError:
pass
else:
print data
class MyArgumentParser(argparse.ArgumentParser):
def print_help(self, file=None):
less(self.format_help())
self.exit()
parser = MyArgumentParser(prog='PROG')
group1 = parser.add_argument_group("Feature 1")
group1.add_argument('--foo1')
group2 = parser.add_argument_group("Feature 2")
group2.add_argument('--foo2')
group3 = parser.add_argument_group("Feature 3")
group3.add_argument('--foo3')
# parse some argument lists
print parser.parse_args()
I want to use the argparse library because of its flexibility, but I am having trouble disabling the default help dialogue to show a custom one from a text file. All I want to do is display the text from the text file when the "-h" or "--help" option is passed. Here is an example of how I am trying this:
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("file", type=str, nargs='+')
parser.add_argument("-xmin", type=float)
parser.add_argument("-xmax", type=float)
parser.add_argument("-ymin", type=float)
parser.add_argument("-ymax", type=float)
parser.add_argument("-h", "--help", action="store_true")
args = parser.parse_args()
if args.help is True:
print isip_get_help()
exit(-1)
But it still outputs:
nedc_[1]: python isip_plot_det.py -h
usage: isip_plot_det.py [-xmin XMIN] [-xmax XMAX] [-ymin YMIN] [-ymax YMAX]
[-h]
file [file ...]
isip_plot_det.py: error: too few arguments
Any ideas?
What you are getting is an error message, not the help (i.e. it's not produced by your -h).
isip_plot_det.py: error: too few arguments
The error message shows the usage part of the normal help. You can change that with a usage parameter:
parser = ArgumentParser(usage = 'my custom usage line')
You can also test the usage display with
parser.print_usage()
or
astr = parser.format_usage()
to get a printable string.
The normal help argument uses a special help action class. Its call method is:
def __call__(self, parser, namespace, values, option_string=None):
parser.print_help()
parser.exit()
Notice that it displays the help with parser.print_help(), and then exits. That occurs as soon as it parses the -h string. That way it doesn't produce any errors like the too few arguments or unrecognized arguments (which are produced at the end of parsing).
So another way of customizing the help is to subclass ArgumentParser, and define your own print_help method. You can also customize the exit and error methods.
The default print_help is:
def print_help(self, file=None):
if file is None:
file = sys.stdout
self._print_message(self.format_help(), file)
You could customize format_help instead.
class MyParser(argparse.ArgumentParser):
def format_help(self):
return 'my custom help message\n second line\n\n'
Sample usage:
In [104]: parser=MyParser(usage='custom usage')
In [105]: parser.parse_args(['-h'])
my custom help message
second line
...
In [106]: parser.parse_args(['unknown'])
usage: custom usage
ipython3: error: unrecognized arguments: unknown
...
The failure is caused by the missing required argument file. The reason why action would not be subjected to this validation requirement is simply because they are executed first, i,e, args.help will be set to True. However, once the parser completes parsing the arguments it would have triggered a sys.exit due to validation failure, and this ends with printing out the default usage and your code (that prints your desired help message) will simply never be executed (you can try calling your program as is with a -h file arguments and your help message should print).
You can either add your custom action using parser.register (not officially supported as it's considered private API, but it works - read argparse.py to see how it all works), or alternatively subclass ArgumentParser and override the print_help method to call isip_get_help().
You could try something like this:
class MyArgumentParser(argparse.ArgumentParser):
def print_help(self, file=None):
print(isip_get_help())
exit(-1)
parser = MyArgumentParser()
...
Leave the default add_help in, but with print_help overridden to call your custom help it should work.
I've looked through dozens of similar SO questions but haven't find suitable solutions so please forgive me in case of a dublicate.
I have a problem similar to this one
I want to have a parser+subparser pair, with --help option causing help being shown for the both if subparser is "activated".
The only way I was able to get full (parser + subparser) help is:
common_parser = argparse.ArgumentParser(add_help=False)
common_parser.add_argument('-c', required = True)
parser = argparse.ArgumentParser(parents=[common_parser])
subparsers = parser.add_subparsers(dest="sub")
subparser = subparsers.add_parser("sub_option", parents=[common_parser])
subparser.add_argument('-o', required = False)
settings = parser.parse_args()
But then script requires the -c option to be entered twice (apparently for parser and subparser). If I don't use parents then I get normal behaviour but I don't the help I get for subparser contains only -o description (I want also -c to be shown)
P.S. Python 2.7
For a start let's distinguish between parsing behavior and help display.
I assume you have multiple subparsers (otherwise why use the subparser mechanism). -o is an argument specific to sub_option, and presumably the other subparsers have their own arguments.
What is the purpose of -c? Is it something that is common to all subparsers? Since subparsers are required, it doesn't make sense to talk about an argument that only matters to the main parser.
One way to deal with a common argument is to define it for all subparsers. The parents mechanism that you use saves you a bit of typing. Just omit it from the main parser definition. That gets rid of the problem with having to supply it twice.
If -c is defined for all the subparsers, then there isn't a need to show it in the main parser help, is there?
The whole subparser mechanism is cleanest when the subparser command is the 1st argument string, with all of its arguments, positionals and options following. It's possible to define arguments for the main parser, but it often complicates both use and help.
The issue of display the help for both the main parser and (all) the subparsers has come up before, both on SO, and on the python bug/issues. There isn't a simple solution. Some tools that may help are:
generate help under program control with parser.print_help(), and subparse.print_help().
add_subparsers() command takes parameters like prog, title and description which can be used to control the help, including the usage of the subparser help.
add_subparser() takes the same sort of parameters as ArgumentParser (since it defines a parser), description and usage may be useful.
Looking a previous question
How to show help for all subparsers in argparse?
I realized that if -c is not required, it can be defined for both the main and subparser, and appear as expected in the helps. But by making it 'required', both parsers have to see it - but only the value seen by the subparser appears in the namespace.
Also positionals defined in the main parser appear in the subparser usage.
Another subparsers help display question
argparse subparser monolithic help output
http://bugs.python.org/issue20333 argparse subparser usage message hides main parser usage discusses the question of how much of main parser usage should show up in the subparse usage line. Currently just positionals (defined before the subparsers) show up. In the patch I suggest adding required optionals as well. But you can always fudge this by defining your own prog for subparsers.
I want the help option act the same as --help.
sidenote: I have created a program with the same command line behavior as svn or hg. I managed to do this with subparsers. However I want to make things consistent. That's why I want help to work.
You can do the following to create an alias and make program help act the same as program --help
import argparse
def help(args):
args.parser.print_help()
parser = argparse.ArgumentParser(description='my program')
subparsers = parser.add_subparsers()
p_help = subparsers.add_parser('help')
p_help.set_defaults(func=help, parser=parser)
args = parser.parse_args()
args.func(args)
It seems to me that you want to define help as another subparser. I would naively say that you could then link it to a print_help() function that would copy the output of your standard --help, but I wonder if there is a way to call the native help() function.