在subparser args之后添加顶级argparse参数

时间:2017-10-26 19:10:21

标签: python arguments argparse

如何在使用subparser的子命令后允许添加顶级程序参数?

我有一个包含多个子分段的程序,允许子命令,改变程序的行为。以下是其设置方式的示例:

#!/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()

当我运行程序时,我可以使用它的args调用子命令:

$ ./subparser_order.py bar -d
did task_d

$ ./subparser_order.py foo -c
did task_c

但是如果我想从顶层包含args,我必须这样称呼它:

$ ./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

事实上,在指定子命令后,您根本无法调用顶级参数:

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

是否有一个解决方案允许在子命令之后包含顶级args?

2 个答案:

答案 0 :(得分: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'])

您还可以向subparser添加具有相同标志/ dest的参数。

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,这种行为并不容易。

答案 1 :(得分:1)

实际上有一种方法可以做到。您可以使用parse_known_args,获取名称空间和未解析的参数,并将其传递回parse_args调用。它将在第二遍中进行合并和覆盖,并且从那里遗留下来的任何参数仍将引发解析器错误。

简单的例子,这里是设置:

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调用,以使解析器运行自己的错误路径。

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

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