如何使用python argparse解析多个嵌套子命令?
我正在实现一个命令行程序,其界面如下:如何使用python argparse解析多个嵌套子命令?,python,command-line-arguments,argparse,Python,Command Line Arguments,Argparse,我正在实现一个命令行程序,其界面如下: cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...] 我已经通过了考试。我可以使用argparse中的add_argument将GLOBAL_OPTIONS实现为可选参数。以及{command[command_OPTS]}使用 从文档来看,我似乎只有一个子命令。但正如您所看到的,我必须实现一个或多个子命令。使用argparse解析此类命令行参数的最佳方法是
cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]
我已经通过了考试。我可以使用argparse
中的add_argument
将GLOBAL_OPTIONS
实现为可选参数。以及{command[command_OPTS]}
使用
从文档来看,我似乎只有一个子命令。但正如您所看到的,我必须实现一个或多个子命令。使用
argparse
解析此类命令行参数的最佳方法是什么?您始终可以自己拆分命令行(在命令名上拆分sys.argv
),然后只将与特定命令相对应的部分传递给parse_args
——如果需要,甚至可以使用相同的名称空间
,使用名称空间关键字
使用itertools可以轻松地对命令行进行分组。groupby
:
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...
未经测试您可以使用软件包optpass
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
@米吉尔森对这个问题有很好的解释。但拆分sys.argv本身的问题是,我丢失了Argparse为用户生成的所有好的帮助消息。所以我就这样做了:
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生成的更好的用法字符串。我提出了相同的问题,似乎我得到了更好的答案
解决方案是,我们不应该简单地用另一个子parser嵌套子parser,而是可以在另一个子parser之后添加子parser following和解析器
代码告诉您如何:
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
返回名称空间和未知字符串列表。这类似于选中答案中的额外
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
产生:
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') []
另一个循环将为每个子parser提供自己的名称空间。这允许位置名称重叠
argslist = []
while rest:
args,rest = parser.parse_known_args(rest)
argslist.append(args)
你可以试试。这是argparse的一个扩展,明确支持子命令。改进了@mgilson的答案,我编写了一个小型解析方法,将argv拆分为多个部分,并将命令的参数值放入命名空间的层次结构中:
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
:
usage: test.py [-h] {cmd1,cmd2,cmd3} ...
optional arguments:
-h, --help show this help message and exit
sub-commands:
{cmd1,cmd2,cmd3}
usage: test.py cmd1 [-h] [--foo FOO]
optional arguments:
-h, --help show this help message and exit
--foo FOO
对于/test.py cmd1--help
:
usage: test.py [-h] {cmd1,cmd2,cmd3} ...
optional arguments:
-h, --help show this help message and exit
sub-commands:
{cmd1,cmd2,cmd3}
usage: test.py cmd1 [-h] [--foo FOO]
optional arguments:
-h, --help show this help message and exit
--foo FOO
并创建包含参数值的命名空间层次结构:
./test.py cmd1 --foo 3 cmd3 --foo 4
Namespace(cmd1=Namespace(foo='3'), cmd2=None, cmd3=Namespace(foo='4'))
另一个支持并行解析器的包是“声明式解析器” 名称空间变成:
Namespace(
input=Namespace(format='jpeg', path=<_io.BufferedReader name='image.jpeg'>),
output=Namespace(format='gif'),
verbose=True
)
这里是,这里是。对于子命令特定的可选参数,@Vikas提供的解决方案失败,但该方法是有效的。以下是一个改进的版本:
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_args
parse_args
一旦遇到当前子parser未知的参数,就会中止,parse_known_args
将它们作为返回的元组中的第二个值返回。在这种方法中,剩余的参数将再次提供给解析器。因此,对于每个命令,都会创建一个新的名称空间
请注意,在这个基本示例中,所有全局选项仅添加到第一个选项名称空间,而不添加到后续名称空间
这种方法适用于大多数情况,但有三个重要的局限性:
- 不可能对不同的子命令使用相同的可选参数,例如
myprog.py command\u a--foo=bar command\u b--foo=bar
- 不能对子命令使用任何可变长度的位置参数(
或nargs='?'
或nargs='+'
)nargs='*'
- 解析任何已知参数,在新命令下不“中断”。例如,在带有上述代码的
中,PROG--foo command_b command_a--baz Z Z 12
将由--baz Z
消耗,而不是由command_b
消耗command_a
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)
这将引发错误:参数子parser\u name:无效选择:“42”(从“command\u a”、“command\u b”中选择)
原因是内部方法argparse.ArgParser.\u parse\u known\u args()
过于贪婪,并假定command\u a
是可选的spam
参数的值。特别是,当“拆分”可选参数和位置参数时,\u parse\u known\u args()
不会查看片段的名称(如命令a
或命令b
),而只查看它们出现在参数列表中的位置。它还假设任何子命令都将使用所有剩余的参数。
argparse
的这一限制也妨碍了多命令子parser的正确实现。不幸的是,这意味着正确的实现需要完全重写argparse.ArgParser.\u parse\u known\u args()
方法,这是200多行代码
考虑到这些限制,可以选择简单地恢复为单个多选参数,而不是子命令:
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'])
甚至可以在使用信息中列出不同的命令,请参见我的答案,使用和()构建了完整的Python 2/3示例:
from\uuuuu future\uuuuu导入打印功能
从argparse导入ArgumentParser
从随机导入randint
def main():
parser=get_parser()
输入_sum_cmd=['sum_cmd','--sum']
输入\u min\u cmd=['min\u cmd','--min']
args,rest=parser.parse\u known\u args(
#”“求和`
输入\u sum\u cmd+
['-a',str(randint(21,30)),
'-b',str(randint(51,80))]+
#“敏`
输入\最小\命令+
['-y',str(float(randint(64,79)),
'-z',str(float(randint(91120))+.5)]
)
打印('args:\t',args,
'\nrest:\t',rest',\n',sep
# argtest.py
import sys
import argparse
def init_args():
def parse_args_into_namespaces(parser, commands):
'''
Split all command arguments (without prefix, like --) in
own namespaces. Each command accepts extra options for
configuration.
Example: `add 2 mul 5 --repeat 3` could be used to a sequencial
addition of 2, then multiply with 5 repeated 3 times.
'''
class OrderNamespace(argparse.Namespace):
'''
Add `command_order` attribute - a list of command
in order on the command line. This allows sequencial
processing of arguments.
'''
globals = None
def __init__(self, **kwargs):
self.command_order = []
super(OrderNamespace, self).__init__(**kwargs)
def __setattr__(self, attr, value):
attr = attr.replace('-', '_')
if value and attr not in self.command_order:
self.command_order.append(attr)
super(OrderNamespace, self).__setattr__(attr, value)
# 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)
# Globals arguments without commands
args = OrderNamespace()
cmd, args_raw = 'globals', split_argv.pop(0)
args_parsed = parser.parse_args(args_raw, namespace=OrderNamespace())
setattr(args, cmd, args_parsed)
# Split all commands to separate namespace
pos = 0
while len(split_argv):
pos += 1
cmd, *args_raw = split_argv.pop(0)
assert cmd[0].isalpha(), 'Command must start with a letter.'
args_parsed = commands.choices[cmd].parse_args(args_raw, namespace=OrderNamespace())
setattr(args, f'{cmd}~{pos}', args_parsed)
return args
#
# Supported commands and options
#
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--print', action='store_true')
commands = parser.add_subparsers(title='Operation chain')
cmd1_parser = commands.add_parser('add', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
cmd1_parser.add_argument('add', help='Add this number.', type=float)
cmd1_parser.add_argument('-r', '--repeat', help='Repeat this operation N times.',
default=1, type=int)
cmd2_parser = commands.add_parser('mult', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
cmd2_parser.add_argument('mult', help='Multiply with this number.', type=float)
cmd2_parser.add_argument('-r', '--repeat', help='Repeat this operation N times.',
default=1, type=int)
args = parse_args_into_namespaces(parser, commands)
return args
#
# DEMO
#
args = init_args()
# print('Parsed arguments:')
# for cmd in args.command_order:
# namespace = getattr(args, cmd)
# for option_name in namespace.command_order:
# option_value = getattr(namespace, option_name)
# print((cmd, option_name, option_value))
print('Execution:')
result = 0
for cmd in args.command_order:
namespace = getattr(args, cmd)
cmd_name, cmd_position = cmd.split('~') if cmd.find('~') > -1 else (cmd, 0)
if cmd_name == 'globals':
pass
elif cmd_name == 'add':
for r in range(namespace.repeat):
if args.globals.print:
print(f'+ {namespace.add}')
result = result + namespace.add
elif cmd_name == 'mult':
for r in range(namespace.repeat):
if args.globals.print:
print(f'* {namespace.mult}')
result = result * namespace.mult
else:
raise NotImplementedError(f'Namespace `{cmd}` is not implemented.')
print(10*'-')
print(result)
$ python argstest.py --print add 1 -r 2 mult 5 add 3 mult -r 5 5
Execution:
+ 1.0
+ 1.0
* 5.0
+ 3.0
* 5.0
* 5.0
* 5.0
* 5.0
* 5.0
----------
40625.0