开发者

Argparse: ignore multiple positional arguments when optional argument is specified

I'm trying to make argparse ignore the fact that two normally required positional arguments shouldn't be evaluated when an optional argument (-l) is specified.

Basically I'm trying to replicate the behavior of --help: when you specify the -h, all missing required arguments are ignored.

Example code:

parser = argparse.ArgumentParser(descriptio开发者_C百科n="Foo bar baz")
parser.add_argument('arg1', help='arg1 is a positional argument that does this')
parser.add_argument('arg2', help='arg2 is a positional argument that does this')
parser.add_argument('-l', '--list', dest='list', help='this is an optional argument that prints stuff')

options, args = parser.parse_args()

if options.list:
   print "I list stuff"

And of course, if I run it now, I get :

error: too few arguments

I tried different things like nargs='?', but couldn't get anything working.

This question is quite similar but wasn't answered.


Unfortunately, argparse isn't quite flexible enough for this. The best you can do is to make arg1 and arg2 optional using nargs="?" and check yourself whether they are given if needed.

The internal help action is implemented by printing the help message and exiting the program as soon as -h or --help are encountered on the command line. You could write a similar action yourself, something like

class MyAction(argparse.Action):
    def __call__(self, parser, values, namespace, option_string):
        print "Whatever"
        parser.exit()

(Warning: untested code!)

There are definite downsides to the latter approac, though. The help message will unconditionally show arg1 and arg2 as compulsory arguments. And parsing the command line simply stops when encountering -l or --list, ignoring any further arguments. This behaviour is quite acceptable for --help, but is less than desirable for other options.


I ran into this issue and decided to use subcommands. Subcommands might be overkill, but if you find your program not using some of the positional arguments in many instances (as I did), then subcommands might be a good solution.

For your given example, I'd use something like the following:

parser = argparse.ArgumentParser(description="Foo bar baz")
subparsers = parser.add_subparsers(description='available subcommands')

parser_main = subparsers.add_parser('<main_command_name>')
parser_main.add_argument('arg1', help='arg1 is a positional argument that does this')
parser_main.add_argument('arg2', help='arg2 is a positional argument that does this')

parser_list = subparsers.add_parser('list', help='this is a subcommand that prints stuff')

options, args = parser.parse_args()

I left out some details that you might want to include (like set_defaults(func=list)), which are mentioned in the argparse documentation.


The cleanest approach I've been able to find so far is to split the parsing into two stages. First check for -l/--list:

parser = argparse.ArgumentParser(description="Foo bar baz")
parser.add_argument('-l', '--list', dest='list', action='store_true',
                    help='this is an optional argument that prints stuff')

options, remainder = parser.parse_known_args()

Now, since you used parse_known_args, you won't get an error up to here, and you can decide what to do with the remainder of the arguments:

if options.list:
    print "I list stuff"
else:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('arg1', help='arg1 is a positional argument that does this')
    parser.add_argument('arg2', help='arg2 is a positional argument that does this')
    options = parser.parse_args(remainder)

You may want to set the usage option in the first parser to make the help string a bit nicer.


I think the nargs='*' is helpful.

Positional arguments is ignorable, then you can use if to check the positional arguments is true or false.

http://docs.python.org/library/argparse.html#nargs


I may have found a solution here. True, it is a dirty hack, but it works.

Note: all the following applies to Python 3.3.2.

As per the answer here, parse_args checks which actions are required and throws an error if any of them are missing. I propose to override this behavior.

By subclassing ArgumentParser we can define a new ArgumentParser.error method (original here) that will check whether the error was thrown because some arguments are missing and take necessary action. Code follows:

import argparse
import sys
from gettext import gettext as _

class ArgumentParser(argparse.ArgumentParser):
    skip_list = []

    def error(self, message):
        # Let's see if we are missing arguments
        if message.startswith('the following arguments are required:'):
            missingArgs = message.split('required: ')[1].split(', ')
            newArgs = []    # Creating a list of whatever we should not skip but is missing
            for arg in missingArgs:
                if arg not in self.skip_list:
                    newArgs.append(arg)
                else:
                    self.skip_list.remove(arg)  # No need for it anymore
            if len(newArgs) == 0:
                return  # WARNING! UNTESTED! MAY LEAD TO SPACETIME MELTDOWN!
            else:   # Some required stuff is still missing, so we show a corrected error message
                message = _('the following arguments are required: %s') % ', '.join(newArgs)

        self.print_usage(sys.stderr)    # Original method behavior
        args = {'prog': self.prog, 'message': message}
        self.exit(2, _('%(prog)s: error: %(message)s\n') % args)

The new method first checks whether the error is because arguments are missing from the command line (see here for the code that generates the error). If so, the method gets the names of the arguments from the error message and puts them into a list (missingArgs).

Then, we iterate over this list and check which arguments should be skipped, and which are still required. To determine which arguments to skip, we compare them against skip_list. It is a field in our ArgumentParser subclass that should contain the names of the arguments to skip even when they are required by the parser. Please note that arguments that happen to be in skip_list are removed from it when they are found.

If there are still required arguments that are missing from the command line, the method throws a corrected error message. If all the missing arguments should be skipped, however, the method returns.

WARNING! The original definition of ArgumentParser.error states that if it is overridden in a subclass it should not return, but rather exit or raise an exception. Therefore, what is shown here is potentially unsafe and may cause your program to crash, your computer to catch fire or worse - IT MAY EVAPORATE ALL YOUR TEA. However, it seems like in this particular case (missing required arguments) it is safe to return from the method. But it might not be. You have been warned.

In order to fill skip_list we could use code like this:

class SpecialHelp(argparse._HelpAction):
    def __call__(self, parser, namespace, values, option_string=None):
        parser.print_help()
        print()
        for action in parser._actions:
            if action != self and action.required:
                parser.skip_list.append(argparse._get_action_name(action))

This particular class imitates the built-in help action, but instead of exiting it inserts all the remaining required arguments into skip_list.

Hope my answer helps and best of luck.


It may be ugly, but that is what I normally do.

def print_list():
    the_list = ["name1", "name2"]
    return "{0}".format(the_list)

...
parser.add_argument("-l", "--list", action='version',
                    version=print_list(), help="print the list")
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜