如何使用python argparse解析多个嵌套的子命令?

How to parse multiple nested sub-commands using python argparse?

我正在实现一个命令行程序,它具有如下接口:

1
cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]

我已经阅读了argparse文档。我可以使用argparse中的add_argument作为可选参数来实现GLOBAL_OPTIONS。以及使用子命令的{command [COMMAND_OPTS]}

从文档来看,我似乎只能有一个子命令。但正如您所看到的,我必须实现一个或多个子命令。使用argparse解析此类命令行参数的最佳方法是什么?


@米吉尔森对这个问题有一个很好的回答。但是拆分sys.argv本身的问题是,我丢失了argparse为用户生成的所有好的帮助消息。所以我就这样做了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import argparse

## This function takes the 'extra' attribute from global namespace and re-parses it to create separate namespaces for all other chained commands.
def parse_extra (parser, namespace):
  namespaces = []
  extra = namespace.extra
  while extra:
    n = parser.parse_args(extra)
    extra = n.extra
    namespaces.append(n)

  return namespaces

argparser=argparse.ArgumentParser()
subparsers = argparser.add_subparsers(help='sub-command help', dest='subparser_name')

parser_a = subparsers.add_parser('command_a', help ="command_a help")
## Setup options for parser_a

## Add nargs="*" for zero or more other commands
argparser.add_argument('extra', nargs ="*", help = 'Other commands')

## Do similar stuff for other sub-parsers

现在,在第一次解析之后,所有链接的命令都存储在extra中。我重新分析它,但它不是空的,以获取所有链接的命令并为它们创建单独的名称空间。我得到了argparse生成的更好的用法字符串。


我提出了同样的问题,似乎我得到了一个更好的答案。

解决方案是,我们不应该简单地将子排序器嵌套在另一个子排序器中,而是可以添加子排序器跟踪,并在另一个子排序器之后添加分析器跟踪。

代码告诉你如何:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
parent_parser = argparse.ArgumentParser(add_help=False)                                                                                                  
parent_parser.add_argument('--user', '-u',                                                                                                              
                    default=getpass.getuser(),                                                                                                          
                    help='username')                                                                                                                    
parent_parser.add_argument('--debug', default=False, required=False,                                                                                    
                           action='store_true', dest="debug", help='debug flag')                                                                        
main_parser = argparse.ArgumentParser()                                                                                                                  
service_subparsers = main_parser.add_subparsers(title="service",                                                                                        
                    dest="service_command")                                                                                                              
service_parser = service_subparsers.add_parser("first", help="first",                                                                                    
                    parents=[parent_parser])                                                                                                            
action_subparser = service_parser.add_subparsers(title="action",                                                                                        
                    dest="action_command")                                                                                                              
action_parser = action_subparser.add_parser("second", help="second",                                                                                    
                    parents=[parent_parser])                                                                                                            

args = main_parser.parse_args()


parse_known_args返回一个名称空间和一个未知字符串列表。这与选中答案中的extra类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--foo')
sub = parser.add_subparsers()
for i in range(1,4):
    sp = sub.add_parser('cmd%i'%i)
    sp.add_argument('--foo%i'%i) # optionals have to be distinct

rest = '--foo 0 cmd2 --foo2 2 cmd3 --foo3 3 cmd1 --foo1 1'.split() # or sys.argv
args = argparse.Namespace()
while rest:
    args,rest =  parser.parse_known_args(rest,namespace=args)
    print args, rest

生产:

1
2
3
Namespace(foo='0', foo2='2') ['cmd3', '--foo3', '3', 'cmd1', '--foo1', '1']
Namespace(foo='0', foo2='2', foo3='3') ['cmd1', '--foo1', '1']
Namespace(foo='0', foo1='1', foo2='2', foo3='3') []

另一个循环将为每个子排序器提供自己的命名空间。这允许位置名称重叠。

1
2
3
4
argslist = []
while rest:
    args,rest =  parser.parse_known_args(rest)
    argslist.append(args)


您可以尝试arghandler。这是argparse的扩展,显式支持子命令。


@vikas提供的解决方案无法用于子命令特定的可选参数,但该方法是有效的。这里有一个改进的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import argparse

# create the top-level parser
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='foo help')
subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name')

# create the parser for the"command_a" command
parser_a = subparsers.add_parser('command_a', help='command_a help')
parser_a.add_argument('bar', type=int, help='bar help')

# create the parser for the"command_b" command
parser_b = subparsers.add_parser('command_b', help='command_b help')
parser_b.add_argument('--baz', choices='XYZ', help='baz help')

# parse some argument lists
argv = ['--foo', 'command_a', '12', 'command_b', '--baz', 'Z']
while argv:
    print(argv)
    options, argv = parser.parse_known_args(argv)
    print(options)
    if not options.subparser_name:
        break

它使用parse_known_args而不是parse_argsparse_args一旦遇到当前子排序器未知的参数,就会中止,parse_known_args将它们作为返回元组中的第二个值返回。在这种方法中,剩余的参数将再次被送入解析器。因此,对于每个命令,都会创建一个新的名称空间。

请注意,在这个基本示例中,所有全局选项只添加到第一个选项名称空间,而不添加到后续的名称空间。

这种方法在大多数情况下都可以正常工作,但有三个重要限制:

  • 不可能对不同的子命令使用相同的可选参数,如myprog.py command_a --foo=bar command_b --foo=bar
  • 不能在子命令(nargs='?'nargs='+'nargs='*'中使用任何可变长度的位置参数。
  • 解析任何已知参数,而不在新命令处"中断"。例如,在具有上述代码的PROG --foo command_b command_a --baz Z 12中,--baz Z将由command_b消费,而不是由command_a消费。

这些限制是argparse的直接限制。下面是一个简单的示例,说明argparse的局限性(即使在使用单个子命令时也是如此):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('spam', nargs='?')
subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name')

# create the parser for the"command_a" command
parser_a = subparsers.add_parser('command_a', help='command_a help')
parser_a.add_argument('bar', type=int, help='bar help')

# create the parser for the"command_b" command
parser_b = subparsers.add_parser('command_b', help='command_b help')

options = parser.parse_args('command_a 42'.split())
print(options)

这将提高error: argument subparser_name: invalid choice: '42' (choose from 'command_a', 'command_b')

原因是内部方法argparse.ArgParser._parse_known_args()太贪婪,假定command_a是可选spam参数的值。特别是,当"拆分"可选参数和位置参数时,_parse_known_args()不查看路径的名称(如command_acommand_b),只查看它们出现在参数列表中的位置。它还假定任何子命令都将使用所有剩余的参数。argparse的这种限制也妨碍了多指挥子计划的正确实施。不幸的是,这意味着正确的实现需要完全重写argparse.ArgParser._parse_known_args()方法,即200多行代码。

考虑到这些限制,可以选择简单地恢复为单个多选参数,而不是子命令:

1
2
3
4
5
6
7
8
9
10
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--bar', type=int, help='bar help')
parser.add_argument('commands', nargs='*', metavar='COMMAND',
                 choices=['command_a', 'command_b'])

options = parser.parse_args('--bar 2 command_a command_b'.split())
print(options)
#options = parser.parse_args(['--help'])

甚至可以在使用信息中列出不同的命令,请参阅我的答案https://stackoverflow.com/a/49999185/428542


为了改进@mgilson的答案,我编写了一个小的解析方法,它将argv拆分为多个部分,并将命令的参数值放入命名空间的层次结构中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import sys
import argparse


def parse_args(parser, commands):
    # Divide argv by commands
    split_argv = [[]]
    for c in sys.argv[1:]:
        if c in commands.choices:
            split_argv.append([c])
        else:
            split_argv[-1].append(c)
    # Initialize namespace
    args = argparse.Namespace()
    for c in commands.choices:
        setattr(args, c, None)
    # Parse each command
    parser.parse_args(split_argv[0], namespace=args)  # Without command
    for argv in split_argv[1:]:  # Commands
        n = argparse.Namespace()
        setattr(args, argv[0], n)
        parser.parse_args(argv, namespace=n)
    return args


parser = argparse.ArgumentParser()
commands = parser.add_subparsers(title='sub-commands')

cmd1_parser = commands.add_parser('cmd1')
cmd1_parser.add_argument('--foo')

cmd2_parser = commands.add_parser('cmd2')
cmd2_parser.add_argument('--foo')

cmd2_parser = commands.add_parser('cmd3')
cmd2_parser.add_argument('--foo')


args = parse_args(parser, commands)
print(args)

它的行为正常,提供了良好的argparse帮助:

对于./test.py --help

1
2
3
4
5
6
7
usage: test.py [-h] {cmd1,cmd2,cmd3} ...

optional arguments:
  -h, --help        show this help message and exit

sub-commands:
  {cmd1,cmd2,cmd3}

对于./test.py cmd1 --help

1
2
3
4
5
usage: test.py cmd1 [-h] [--foo FOO]

optional arguments:
  -h, --help  show this help message and exit
  --foo FOO

并创建包含参数值的命名空间层次结构:

1
2
./test.py cmd1 --foo 3 cmd3 --foo 4
Namespace(cmd1=Namespace(foo='3'), cmd2=None, cmd3=Namespace(foo='4'))


您可以自己拆分命令行(在命令名上拆分sys.argv),然后只将与特定命令对应的部分传递给parse_args--如果需要,甚至可以使用相同的Namespace,使用namespace关键字。

使用itertools.groupby可以很容易地对命令行进行分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys
import itertools
import argparse    

mycommands=['cmd1','cmd2','cmd3']

def groupargs(arg,currentarg=[None]):
    if(arg in mycommands):currentarg[0]=arg
    return currentarg[0]

commandlines=[list(args) for cmd,args in intertools.groupby(sys.argv,groupargs)]

#setup parser here...
parser=argparse.ArgumentParser()
#...

namespace=argparse.Namespace()
for cmdline in commandlines:
    parser.parse_args(cmdline,namespace=namespace)

#Now do something with namespace...

未经测试的


另一个支持并行解析器的包是"声明性解析器"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import argparse
from declarative_parser import Parser, Argument

supported_formats = ['png', 'jpeg', 'gif']

class InputParser(Parser):
    path = Argument(type=argparse.FileType('rb'), optional=False)
    format = Argument(default='png', choices=supported_formats)

class OutputParser(Parser):
    format = Argument(default='jpeg', choices=supported_formats)

class ImageConverter(Parser):
    description = 'This app converts images'

    verbose = Argument(action='store_true')
    input = InputParser()
    output = OutputParser()

parser = ImageConverter()

commands = '--verbose input image.jpeg --format jpeg output --format gif'.split()

namespace = parser.parse_args(commands)

名称空间变成:

1
2
3
4
5
Namespace(
    input=Namespace(format='jpeg', path=<_io.BufferedReader name='image.jpeg'>),
    output=Namespace(format='gif'),
    verbose=True
)

免责声明:我是作者。需要python 3.6。安装使用:

1
pip3 install declarative_parser

这是文档,这是Github上的回购。


您可以使用optparse包

1
2
3
4
5
6
7
import optparse
parser = optparse.OptionParser()
parser.add_option("-f", dest="filename", help="corpus filename")
parser.add_option("--alpha", dest="alpha", type="float", help="parameter alpha", default=0.5)
(options, args) = parser.parse_args()
fname = options.filename
alpha = options.alpha