argparse 子命令错误信息

7 投票
3 回答
2364 浏览
提问于 2025-04-18 17:27

考虑以下的Python 2代码:

from argparse import ArgumentParser

p = ArgumentParser(prog="test")
p.add_argument('--bar')
sp = p.add_subparsers()
sp1 = sp.add_parser('foo')
sp1.add_argument('--baz')

p.parse_args(['foo', '--bar'])

我收到的错误信息是:

usage: test [-h] [--bar BAR] {foo} ...
test: error: unrecognized arguments: --bar

这似乎意味着 --bartest 的一个无法识别的参数。但实际上,它是 foo 子命令的一个无法识别的参数。

我觉得错误信息应该是:

usage: test foo [-h] [--baz BAZ]
foo: error: unrecognized arguments: --bar

这是argparse的一个bug吗?我能否让argparse给出正确的错误信息?

3 个回答

0

我已经用以下方法解决了这个问题:

from argparse import ArgumentParser, _

class _ArgumentParser(ArgumentParser):
    # "parse_known_args" is made to behave exactly like "parse_args".
    def parse_known_args(self, args=None, namespace=None):
        args, argv = super().parse_known_args(args, namespace)
        if argv:
            msg = _('unrecognized arguments: %s')
            self.error(msg % ' '.join(argv))
        return args, argv

p = ArgumentParser(prog="test")
p.add_argument('--bar')
sp = p.add_subparsers(parser_class=_ArgumentParser)
sp1 = sp.add_parser('foo')
sp1.add_argument('--baz')

p.parse_args(['foo', '--bar'])

我收到了以下错误信息:

usage: test foo [-h] [--baz BAZ]
test foo: error: unrecognized arguments: --bar

我觉得这正是我想要的,对吧?

我完全不明白为什么Python会有parse_known_args这个东西。我觉得它一点用处都没有,反正我认为应该用parse_args

1

我不一定会说这是个错误,更像是一种简单的方式来生成错误信息。

默认的错误信息只是简单地说 "{PROG}: error: unrecognized arguments: {ALL_UNRECOGNIZED_ARGS}",不管这些不被识别的参数是和哪个(子)解析器有关。

如果你看看 parse_args() 是怎么生成错误的,你会发现这些信息并不是很容易获取:

def parse_args(self, args=None, namespace=None):
    args, argv = self.parse_known_args(args, namespace)
    if argv:
        msg = _('unrecognized arguments: %s')
        self.error(msg % ' '.join(argv))
    return args

调用 args, argv = parse_known_args(args) 会简单地解析已知的参数,并把它们作为命名空间 args 返回,同时把所有剩下的未知参数返回为 argv

所以你可以直接使用 parse_known_args()。你也可以通过

sp = p.add_subparsers(dest='cmd')

把子命令的名称存储到命名空间中,然后用 args.cmd 来查看用户尝试调用的是哪个子命令。

根据这些信息,视你的解析器结构有多简单,你也许可以生成一个更有帮助的错误信息(见 parser.error())。

不过这也不可能做到完美。如果用户在主解析器和一个子解析器中都指定了一个未知参数,这两个不被识别的参数会留在 argv 中,而我看不出有什么明显的方法来判断它们分别是给了命令行上的哪个命令。

4

如果我调整一下你的脚本

p = ArgumentParser(prog="test")
p.add_argument('--bar')
sp = p.add_subparsers(dest='cmd')
sp1 = sp.add_parser('foo')
sp1.add_argument('--baz')
print p.parse_known_args()

输出结果是

1517:~/mypy$ python2.7 stack25333847.py foo --bar
(Namespace(bar=None, baz=None, cmd='foo'), ['--bar'])

解析器 p 遇到了 foo,这是允许的 sp 选项之一。所以它把解析工作交给了子解析器 sp1。但是 sp1 不认识 --bar,于是它把这个不认识的参数返回给主解析器。主解析器的默认行为是把这个参数直接传出去,就好像它自己也不认识这个字符串一样。

由于 --barfoo 后面,所以两个解析器都不认识它。对于 ['foo', '--boo'] 也是一样的情况。

将解析工作交给子解析器是在 sp__call__ 方法中完成的(这是子解析器的操作)。其中一部分是:

def __call__(self, parser, namespace, values, option_string=None):
    ...
    # parse all the remaining options into the namespace
    # store any unrecognized options on the object, so that the top
    # level parser can decide what to do with them
    namespace, arg_strings = parser.parse_known_args(arg_strings, namespace)
    if arg_strings:
        vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
        getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)

所以,处理不被识别的参数是由主解析器来负责的(也就是调用 parse_args 而不是 parse_known_args 的那个解析器)。


如果出现其他错误,比如没有给 --baz 提供值,就会在子解析器中生成错误信息:

1523:~/mypy$ python2.7 stack25333847.py foo --baz
usage: test foo [-h] [--baz BAZ]
test foo: error: argument --baz: expected one argument

我找到了一种生成的方法:

usage: test foo [-h] [--baz BAZ]
test foo: error: unrecognized arguments: --bar

虽然这个方法不简单。我创建了一个 argparse._SubParsersAction 的子类;给它一个新的 __call__ 方法,这个方法使用 parse_args 而不是 parse_known_args。我还需要修改主解析器的注册信息。(如果需要的话,我可以添加代码)。

撰写回答