Python:命令行参数 --foo 和 --no-foo

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

对于使用 Python 的内置 argparse 包解析布尔命令行选项,我知道这个问题及其几个答案:Parsing boolean values with argparse.

几个答案(正确的,IMO)指出布尔选项最常见和最直接的习惯用法(从调用者的角度来看)是接受 --foo--no-foo 选项, 将程序中的一些值分别设置为 TrueFalse

然而,在我看来,我能找到的所有答案实际上并没有正确地完成任务。他们似乎通常缺乏以下一项:

  1. 可以设置合适的默认值(TrueFalseNone)。
  2. program.py --help 提供的帮助文本是正确且有用的,包括显示默认值。
  3. 两者之一(我真的不在乎哪个,但有时两者都是可取的):
    • 参数 --foo 可以被后面的参数 --no-foo 覆盖,反之亦然;
    • --foo--no-foo 不兼容且互斥。

我想知道使用 argparse.

是否完全可行

根据@mgilson 和@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)

大多数情况下都很好,但帮助文本令人困惑:

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)

更好的帮助文本应该是这样的:

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)

但我看不出有什么方法可以做到这一点,因为“--*”和“--no-*”必须始终声明为单独的参数(对吗?)。

除了上述 SO 问题的建议之外,我还尝试使用其他 SO 问题中显示的技术创建自定义操作:Python argparse custom actions with additional arguments passed。这些立即失败说 "error: argument --foo: expected one argument",或(如果我设置 nargs=0"ValueError: nargs for store actions must be > 0"。从 argparse 来源来看,这看起来是因为预定义的 'store_const'、'store_true'、'append' 等以外的操作必须使用 _StoreAction class,这需要一个参数。

有没有其他方法可以做到这一点?如果有人有我还没有想到的想法组合,请告诉我!

(顺便说一句-我正在创建这个新问题,而不是尝试添加到上面的第一个问题,因为上面的原始问题实际上是在寻求一种方法来处理 --foo TRUE--foo FALSE 参数,这是不同的,IMO 不太常见。)

your linked question, specifically the one by Robert T. McGibbon, includes a code snippet from an enhancement request 中从未被标准 argparse 接受的答案之一。不过,如果你不考虑一个烦恼,它的效果相当好。这是我的复制品,经过一些小的修改,作为一个独立模块添加了一点 pydoc 字符串,以及它的用法示例:

import argparse
import re

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

    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)

(此代码适用于 Python 2 和 3)。

实际操作如下:

$ 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

请注意,初始 usage 消息未提及 --no-foo 选项。除了使用您不喜欢的组方法之外,没有简单的方法可以纠正此问题。

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