How to parse multiple nested sub-commands using python argparse?
我正在实现一个命令行程序,它具有如下接口:
1 | cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{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 |
现在,在第一次解析之后,所有链接的命令都存储在
我提出了同样的问题,似乎我得到了一个更好的答案。
解决方案是,我们不应该简单地将子排序器嵌套在另一个子排序器中,而是可以添加子排序器跟踪,并在另一个子排序器之后添加分析器跟踪。
代码告诉你如何:
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() |
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 |
它使用
请注意,在这个基本示例中,所有全局选项只添加到第一个选项名称空间,而不添加到后续的名称空间。
这种方法在大多数情况下都可以正常工作,但有三个重要限制:
- 不可能对不同的子命令使用相同的可选参数,如
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) |
这将提高
原因是内部方法
考虑到这些限制,可以选择简单地恢复为单个多选参数,而不是子命令:
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帮助:
对于
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} |
对于
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')) |
您可以自己拆分命令行(在命令名上拆分
使用
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 |