在子解析器 args 之后添加*** argparse 参数

Posted

技术标签:

【中文标题】在子解析器 args 之后添加*** argparse 参数【英文标题】:Add top level argparse arguments after subparser args 【发布时间】:2018-04-08 07:02:19 【问题描述】:

如何允许在使用来自子解析器的子命令后添加***程序参数?

我有一个程序,其中包含多个子解析器以允许子命令,从而改变程序的行为。以下是它的设置示例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse


def task_a():
    print('did task_a')

def task_c():
    print('did task_c')

def task_d():
    print('did task_d')


def run_foo(args):
    a_arg = args.a
    c_arg = args.c
    if a_arg:
        task_a()
    if c_arg:
        task_c()


def run_bar(args):
    a_arg = args.a
    d_arg = args.d
    if a_arg:
        task_a()
    if d_arg:
        task_d()

def parse():
    '''
    Run the program
    arg parsing goes here, if program was run as a script
    '''
    # create the top-level parser
    parser = argparse.ArgumentParser()
    # add top-level args
    parser.add_argument("-a", default = False, action = "store_true", dest = 'a')

    # add subparsers
    subparsers = parser.add_subparsers(title='subcommands', description='valid subcommands', help='additional help', dest='subparsers')

    # create the parser for the "foo" command
    parser_foo = subparsers.add_parser('foo')
    parser_foo.set_defaults(func = run_foo)
    parser_foo.add_argument("-c", default = False, action = "store_true", dest = 'c')

    # create the parser for the "bar" downstream command
    parser_bar = subparsers.add_parser('bar')
    parser_bar.set_defaults(func = run_bar)
    parser_bar.add_argument("-d", default = False, action = "store_true", dest = 'd')

    # parse the args and run the default parser function
    args = parser.parse_args()
    args.func(args)

if __name__ == "__main__":
    parse()

当我运行程序时,我可以调用一个带有参数的子命令,如下所示:

$ ./subparser_order.py bar -d
did task_d

$ ./subparser_order.py foo -c
did task_c

但如果我想包含顶层的参数,我必须这样称呼它:

$ ./subparser_order.py -a foo -c
did task_a
did task_c

但是,我认为这很令人困惑,特别是如果有很多*** args 和很多子命令 args;子命令foo 被夹在中间,难以辨别。

我宁愿能够像subparser_order.py foo -c -a 这样调用程序,但这不起作用:

$ ./subparser_order.py foo -c -a
usage: subparser_order.py [-h] [-a] foo,bar ...
subparser_order.py: error: unrecognized arguments: -a

实际上,指定子命令后根本无法调用***args:

$ ./subparser_order.py foo -a
usage: subparser_order.py [-h] [-a] foo,bar ...
subparser_order.py: error: unrecognized arguments: -a

是否有允许在子命令之后包含***参数的解决方案?

【问题讨论】:

我尝试使用add_parser()parents= 参数执行此操作,但我无法弄清楚。看起来子命令中的参数会覆盖***参数。但如果你想在一堆子命令之间共享一堆选项,至少这是你应该使用的。 【参考方案1】:

其实是有办法的。您可以使用parse_known_args,获取命名空间和未解析的参数并将它们传递回parse_args 调用。它将在第 2 次传递中合并和覆盖,并且从那里剩余的任何参数仍然会引发解析器错误。

简单示例,设置如下:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-a', action='store_true')
sp = parser.add_subparsers(dest='subargs')
sp_1 = sp.add_parser('foo')
sp_1.add_argument('-b', action='store_true')
print(parser.parse_args())

按照 argparse 工作的正确顺序:

- $ python3 argparse_multipass.py
Namespace(a=False, subargs=None)
- $ python3 argparse_multipass.py -a
Namespace(a=True, subargs=None)
- $ python3 argparse_multipass.py -a foo
Namespace(a=True, b=False, subargs='foo')
- $ python3 argparse_multipass.py foo
Namespace(a=False, b=False, subargs='foo')
- $ python3 argparse_multipass.py foo -b
Namespace(a=False, b=True, subargs='foo')
- $ python3 argparse_multipass.py -a foo -b
Namespace(a=True, b=True, subargs='foo')

现在,您无法在子解析器启动后解析参数:

- $ python3 argparse_multipass.py foo -b -a
usage: argparse_multipass.py [-h] [-a] foo ...
argparse_multipass.py: error: unrecognized arguments: -a

但是,您可以执行多遍来获取您的论点。这是更新的代码:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-a', action='store_true')
sp = parser.add_subparsers(dest='subargs')
sp_1 = sp.add_parser('foo')
sp_1.add_argument('-b', action='store_true')
args = parser.parse_known_args()
print('Pass 1: ', args)
args = parser.parse_args(args[1], args[0])
print('Pass 2: ', args)

以及它的结果:

- $ python3 argparse_multipass.py
Pass 1:  (Namespace(a=False, subargs=None), [])
Pass 2:  Namespace(a=False, subargs=None)
- $ python3 argparse_multipass.py -a
Pass 1:  (Namespace(a=True, subargs=None), [])
Pass 2:  Namespace(a=True, subargs=None)
- $ python3 argparse_multipass.py -a foo
Pass 1:  (Namespace(a=True, b=False, subargs='foo'), [])
Pass 2:  Namespace(a=True, b=False, subargs='foo')
- $ python3 argparse_multipass.py foo
Pass 1:  (Namespace(a=False, b=False, subargs='foo'), [])
Pass 2:  Namespace(a=False, b=False, subargs='foo')
- $ python3 argparse_multipass.py foo -b
Pass 1:  (Namespace(a=False, b=True, subargs='foo'), [])
Pass 2:  Namespace(a=False, b=True, subargs='foo')
- $ python3 argparse_multipass.py -a foo -b
Pass 1:  (Namespace(a=True, b=True, subargs='foo'), [])
Pass 2:  Namespace(a=True, b=True, subargs='foo')
- $ python3 argparse_multipass.py foo -b -a
Pass 1:  (Namespace(a=False, b=True, subargs='foo'), ['-a'])
Pass 2:  Namespace(a=True, b=True, subargs='foo')

这将保持原始功能,但允许在子解析器启动时继续解析。此外,如果您执行以下操作,您可以完全摆脱无序解析:

ns, ua = parser.parse_known_args()
while len(ua):
    ns, ua = parser.parse_known_args(ua, ns)

它会继续解析参数以防它们出现故障,直到它完成所有参数的解析。请记住,如果存在未知的争论,这个争论将继续存在。介意添加如下内容:

pua = None
ns, ua = parser.parse_known_args()
while len(ua) and ua != pua:
    ns, ua = parser.parse_known_args(ua, ns)
    pua = ua
ns, ua = parser.parse_args(ua, ns)

只需保留以前未解析的参数对象并对其进行比较,当它中断时执行最终的parse_args 调用以使解析器运行自己的错误路径。

这不是最优雅的解决方案,但我遇到了完全相同的问题,即我在主解析器上的参数在子解析器中指定的内容之上还被用作可选标志。

请记住以下几点:此代码将使人们可以在运行中指定多个子解析器及​​其选项,这些参数调用的代码应该能够处理该问题。

【讨论】:

难道args = parser.parse_args(args[1], args[0]) 应该改为sp_1.parse_args.. 不,sp_1 是一个子解析器,因此只需在 parser 对象上调用 parse_args 即可运行子解析器。【参考方案2】:

一旦***解析器遇到“foo”,它就会将解析委托给parser_foo。这会修改 args 命名空间并返回。***解析器不会恢复解析。它只处理子解析器返回的任何错误。

In [143]: parser=argparse.ArgumentParser()
In [144]: parser.add_argument('-a', action='store_true');
In [145]: sp = parser.add_subparsers(dest='cmd')
In [146]: sp1 = sp.add_parser('foo')
In [147]: sp1.add_argument('-c', action='store_true');

In [148]: parser.parse_args('-a foo -c'.split())
Out[148]: Namespace(a=True, c=True, cmd='foo')

In [149]: parser.parse_args('foo -c'.split())
Out[149]: Namespace(a=False, c=True, cmd='foo')

In [150]: parser.parse_args('foo -c -a'.split())
usage: ipython3 [-h] [-a] foo ...
ipython3: error: unrecognized arguments: -a

您可以防止它因无法识别的参数而窒息,但它不会恢复解析:

In [151]: parser.parse_known_args('foo -c -a'.split())
Out[151]: (Namespace(a=False, c=True, cmd='foo'), ['-a'])

您还可以向子解析器添加具有相同标志/目标的参数。

In [153]: sp1.add_argument('-a', action='store_true')
In [154]: parser.parse_args('foo -c -a'.split())
Out[154]: Namespace(a=True, c=True, cmd='foo')

但子条目的默认值会覆盖顶层值(已针对此行为进行了错误/问题讨论)。

In [155]: parser.parse_args('-a foo -c'.split())
Out[155]: Namespace(a=False, c=True, cmd='foo')

可以使用两阶段解析器或​​自定义_SubParsersAction 类来解析额外的字符串。但是对于argparse,没有一种简单的方法可以解决这种行为。

【讨论】:

"您还可以将具有相同标志/目标的参数添加到子解析器。"是的,这是我试图避免的情况,因为我最终会在相同的 arg 的子解析器中出现多个重复的条目,这并不理想。谢谢你的解释。

以上是关于在子解析器 args 之后添加*** argparse 参数的主要内容,如果未能解决你的问题,请参考以下文章

Python 杂记:argparse 模块

python的argpare和click模块详解

在子解析器 AWS AppSync 中获取父对象

text Wordpress - 在子菜单周围添加div包装器

在 python 中调试 argpars

python argpare 模块的简单用法