简体   繁体   中英

Python: command-line arguments --foo and --no-foo

For parsing boolean command-line options using Python's built-in argparse package, I am aware of this question and its several answers: Parsing boolean values with argparse .

Several of the answers (correctly, IMO) point out that the most common and straightforward idiom for boolean options (from the caller's point of view) is to accept both --foo and --no-foo options, which sets some value in the program to True or False , respectively.

However, all the answers I can find don't actually accomplish the task correctly, it seems to me. They seem to generally fall short on one of the following:

  1. A suitable default can be set ( True , False , or None ).
  2. Help text given for program.py --help is correct and helpful, including showing what the default is.
  3. Either of (I don't really care which, but both are sometimes desirable):
    • An argument --foo can be overridden by a later argument --no-foo and vice versa;
    • --foo and --no-foo are incompatible and mutually exclusive.

What I'm wondering is whether this is even possible at all using argparse .

Here's the closest I've come, based on answers by @mgilson and @fnkr:

def add_bool_arg(parser, name, help_true, help_false, default=None, exclusive=True):
    if exclusive:
        group = parser.add_mutually_exclusive_group(required=False)
    else:
        group = parser
    group.add_argument('--' + name, dest=name, action='store_true', help=help_true)
    group.add_argument('--no-' + name, dest=name, action='store_false', help=help_false)
    parser.set_defaults(**{name: default})


parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
add_bool_arg(parser, 'foo', "Do foo", "Don't foo", exclusive=True)
add_bool_arg(parser, 'bar', "Do bar", "Don't bar", default=True, exclusive=False)

That does most things well, but the help-text is confusing:

usage: argtest.py [-h] [--foo | --no-foo] [--bar] [--no-bar]

optional arguments:
  -h, --help  show this help message and exit
  --foo       Do foo (default: None)
  --no-foo    Don't foo (default: None)
  --bar       Do bar (default: True)
  --no-bar    Don't bar (default: True)

A better help text would be something like this:

usage: argtest.py [-h] [--foo | --no-foo] [--bar] [--no-bar]

optional arguments:
  -h, --help      show this help message and exit
  --foo --no-foo  Whether to foo (default: None)
  --bar --no-bar  Whether to bar (default: True)

But I don't see a way to accomplish that, since "--*" and "--no-*" must always be declared as separate arguments (right?).

In addition to the suggestions at the SO question mentioned above, I've also tried creating a custom action using techniques shown in this other SO question: Python argparse custom actions with additional arguments passed . These fail immediately saying either "error: argument --foo: expected one argument" , or (if I set nargs=0 ) "ValueError: nargs for store actions must be > 0" . From poking into the argparse source, it looks like this is because actions other than the pre-defined 'store_const', 'store_true', 'append', etc. must use the _StoreAction class, which requires an argument.

Is there some other way to accomplish this? If someone has a combination of ideas I haven't thought of yet, please let me know!

(BTW- I'm creating this new question, rather than trying to add to the first question above, because the original question above was actually asking for a method to handle --foo TRUE and --foo FALSE arguments, which is different and IMO less commonly seen.)

One of the answers in your linked question , specifically the one by Robert T. McGibbon , includes a code snippet from an enhancement request that was never accepted into the standard argparse. It works fairly well, though, if you discount one annoyance. Here is my reproduction, with a few small modifications, as a stand-alone module with a little bit of pydoc string added, and an example of its usage:

import argparse
import re

class FlagAction(argparse.Action):
    """
    GNU style --foo/--no-foo flag action for argparse
    (via http://bugs.python.org/issue8538 and
    https://stackoverflow.com/a/26618391/1256452).

    This provides a GNU style flag action for argparse.  Use
    as, e.g., parser.add_argument('--foo', action=FlagAction).
    The destination will default to 'foo' and the default value
    if neither --foo or --no-foo are specified will be None
    (so that you can tell if one or the other was given).
    """
    def __init__(self, option_strings, dest, default=None,
                 required=False, help=None, metavar=None,
                 positive_prefixes=['--'], negative_prefixes=['--no-']):
        self.positive_strings = set()
        # self.negative_strings = set()
        # Order of strings is important: the first one is the only
        # one that will be shown in the short usage message!  (This
        # is an annoying little flaw.)
        strings = []
        for string in option_strings:
            assert re.match(r'--[a-z]+', string, re.IGNORECASE)
            suffix = string[2:]
            for positive_prefix in positive_prefixes:
                s = positive_prefix + suffix
                self.positive_strings.add(s)
                strings.append(s)
            for negative_prefix in negative_prefixes:
                s = negative_prefix + suffix
                # self.negative_strings.add(s)
                strings.append(s)
        super(FlagAction, self).__init__(option_strings=strings, dest=dest,
                                         nargs=0, default=default,
                                         required=required, help=help,
                                         metavar=metavar)

    def __call__(self, parser, namespace, values, option_string=None):
        if option_string in self.positive_strings:
            setattr(namespace, self.dest, True)
        else:
            setattr(namespace, self.dest, False)


if __name__ == '__main__':
    p = argparse.ArgumentParser()
    p.add_argument('-a', '--arg', help='example')
    p.add_argument('--foo', action=FlagAction, help='the boolean thing')
    args = p.parse_args()
    print(args)

(this code works in Python 2 and 3 both).

Here is the thing in action:

$ python flag_action.py -h
usage: flag_action.py [-h] [-a ARG] [--foo]

optional arguments:
  -h, --help         show this help message and exit
  -a ARG, --arg ARG  example
  --foo, --no-foo    the boolean thing

Note that the initial usage message does not mention the --no-foo option. There is no easy way to correct this other than to use the group method that you dislike.

$ python flag_action.py -a something --foo
Namespace(arg='something', foo=True)
$ python flag_action.py --no-foo
Namespace(arg=None, foo=False)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM