我在argparse中为我的python项目实现了一个CustomAction。 CustomAction用于在命令行上指定任意数量的name=value
对样式参数,即nargs='*'
。
class NameValueAction(argparse.Action):
""" CustomAction for argparse to be able to process name,value \
pairs specified as command line arguments. Specified as
$ python runner.py --env=target_env --props name1=value1 name2=value2 module/
"""
def __call__(self, parser, namespace, values, option_string=None):
for value in values:
n, v = value.split('=')
setattr(namespace, n, v)
问题是无法阻止__call__
处理命令行上的module/
参数。如何在不消耗__call__
参数的情况下正确结束module/
方法并允许它由runner.py处理?
PS:我已经尝试退出最后一个不是name=value
的论点,但是这不起作用,因为模块已经被消耗了,我不知道如何重新使用它堆栈。
答案 0 :(得分:2)
我会尝试您最新的自定义操作:
In [34]: parser=argparse.ArgumentParser()
In [35]: parser.add_argument('--env')
In [36]: parser.add_argument('--props',nargs='*',action=NameValueAction)
Out[36]: NameValueAction(option_strings=['--props'], dest='props', nargs='*', const=None, default=None, type=None, choices=None, help=None, metavar=None)
unrecognized arguments
出现parse_args
错误。您的操作正确定义为未知:
In [37]: args=parser.parse_args('--env=target_env --props name1=value1 name2=value2 module/'.split())
usage: ipython2.7 [-h] [--env ENV] [--props [PROPS [PROPS ...]]]
ipython2.7: error: unrecognized arguments: module/
...
使用parse_known_args,我可以看到没有错误消息的args和extras:
In [38]: parser.parse_known_args('--env=target_env --props name1=value1 name2=value2 module/'.split())
Out[38]:
(Namespace(env='target_env', name1='value1', name2='value2', props=None),
['module/'])
因此--props
之后的所有字符串都作为values
传递给该Action。它将值分配给命名空间,然后返回。 parse_known_args
从名称空间中取出unrecognized
值,并将它们放在extras
列表中。
现在我将添加一个位置,希望它将采用module/
字符串:
In [39]: parser.add_argument('foo')
In [40]: parser.parse_known_args('--env=target_env --props name1=value1 name2=value2 module/'.split())
usage: ipython2.7 [-h] [--env ENV] [--props [PROPS [PROPS ...]]] foo
ipython2.7: error: too few arguments
...
糟糕,即使使用parse_known_args
,也会出现其他错误。问题在于'模块/'仍然被--props
给予,foo
没有留下任何内容。 --props
有一个*
的标志,这意味着它会获得所有符合条件的参数(无-
)。放置'模块/'在命名空间中unknown
没有帮助。解析器不会重新评估此列表中的字符串。
我可以使用' - '表示后面的所有字符串都是位置。现在--props
未收到或处理'模块\'。相反,下次处理位置时,foo
会消耗它。
In [41]: parser.parse_known_args('--env=target_env --props name1=value1 name2=value2 -- module/'.split())
Out[41]:
(Namespace(env='target_env', foo='module/', name1='value1', name2='value2', props=None),
[])
另一个可选项,例如' - env'可以用来标记“道具”的结尾。参数:
In [42]: parser.parse_known_args('--props name1=value1 name2=value2 --env=target_env module/'.split())
Out[42]:
(Namespace(env='target_env', foo='module/', name1='value1', name2='value2', props=None),
[])
请注意,progs=None
出现在命名空间中。这是因为解析器在解析开始时将所有Action默认值加载到Namespace中。您可以使用default=argparse.SUPPRESS
来阻止这种情况。
请参阅此错误/问题,了解如何将参数分配给' *'可选,以及如何为后续定位保留一些:
http://bugs.python.org/issue9338
argparse optionals with nargs='?', '*' or '+' can't be followed by positionals
https://stackoverflow.com/a/33405960/901925是另一个最近的SO问题,涉及定期的位置,然后是两个'?' positionals。
正如我在评论中指出的那样,argparse
与optparse
不同。我相信optparse
每个Action(或等效的)消耗尽可能多的字符串,剩下的就是后续的Action。在argparse
个别操作中,无法访问主列表(arg_strings
)。它是决定Action获取多少字符串的解析器。
argparse.py
文件中的更多详细信息。它是parse_args相关部分的摘要。
_parse_known_args(self, arg_strings, namespace):
# arg_strings - master list of strings from sys.argv
start_index = 0
while start_index<amax:
# step through arg_strings processing postionals and optionals
consume_positionals()
start_index = next_option_string_index
start_index = consume_optional(start_index)
consume_optional(start_index): # function local to _parse_known_args
...
start = start_index + 1
arg_count = <fn of available arguments and nargs>
stop = start + arg_count
args = arg_strings[start:stop]
<action = CustomAction.__call__>
take_action(action, args, option_string)
return stop
take_action(action, argument_strings, ...): # another local function
# argument_strings is a slice of arg_strings
argument_values = self._get_values(action, argument_strings)
# _get_values passes strings through the action.type function
action(self, namespace, argument_values, option_string)
# no return
最终结果是,您的CustomAction.__call__
获取了values
列表,这些列表来自主arg_strings
列表的一部分。它无法访问arg_strings
,也无法访问该切片的start
和stop
。因此,它无法改变字符串对自身或任何后续操作的分配。
另一个想法是将您无法解析的值放入self.dest
。
class NameValueAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
extras = []
for value in values:
try:
n, v = value.split('=')
setattr(namespace, n, v)
except ValueError:
extras.append(value)
if len(extras):
setattr(namespace, self.dest, extras)
然后解析(没有foo
位置)会产生:
In [56]: parser.parse_args('--props name1=value1 p1 name2=value2 module/'.split())
Out[56]: Namespace(env=None, name1='value1', name2='value2', props=['p1', 'module/'])
args.props
现在包含['p1','module/']
,--props
得到的字符串,但无法解析为n=v
对。根据需要解析后可以对它们进行重新定位。
答案 1 :(得分:1)
没有办法*阻止&#39;模块/&#39;被消费,因为没有关联name or flags
来表示它是一个单独的参数而不被--props
消费。
我认为您已将--props
设置为:
parser.add_argument('--props', nargs='*', action=NameValueAction)
这样会消耗尽可能多的args。您需要提供-m
或--module
选项才能让argparse存储&#39;模块/&#39;分开。
否则,您可以将模块作为位置arg parser.add_argument('module')
并在命令行上的--props
之前指定它:
parser.add_argument('--env')
parser.add_argument('--props', nargs='*', action=NameValueAction)
parser.add_argument('module')
""" Usage:
$ python runner.py --env=target_env module/ --props name1=value1 name2=value2
or
$ python runner.py module/ --env=target_env --props name1=value1 name2=value2
"""
该过程为:
>>> parser.parse_args('--env=target_env module/ --props name1=value1 name2=value2'.split())
Namespace(env='target_env', module='module/', name1='value1', name2='value2', props=None)
顺便说一句,使用您现有的代码并且没有上面建议的更改,您只需在命令行中指定module=module
,它就会像name=value
对一样处理:
>>> parser.parse_args('--env=target_env --props name1=value1 name2=value2 module=module/'.split())
Namespace(env='target_env', module='module/', name1='value1', name2='value2', props=None)
*如果确实无法将其作为单独的arg使用,那么您必须在NameValueAction中处理它。我将您的__call__
修改为:
def __call__(self, parser, namespace, values, option_string=None):
for value in values:
try:
n, v = value.split('=')
setattr(namespace, n, v) # better to put this in the else clause actually
except ValueError: # "need more than 1 value to unpack"
# raised when there's no '=' sign
setattr(namespace, 'module', value)
>>> parser.parse_args('--env=target_env --props name1=value1 name2=value2 MOARmodules/'.split())
Namespace(env='target_env', module='MOARmodules/', name1='value1', name2='value2', props=None)
当然,其缺点是剩下的行动有多复杂。上面实现的行为与action=store
类似,只会将其应用于'module'
。
您也可以尝试尝试将值附加到sys.argv
,但考虑到在您执行此操作时正在使用这些值,可能会产生意想不到的副作用,类似于您不应该这样做的原因。在迭代时插入/删除列表。
答案 2 :(得分:1)
在@ aneroid的线索中查看NameValueAction
内的处理后,我通读了argparse
模块以找到可行的方法。 Actions
在argparse
中执行命令行解析。 Action
下的argparse
在程序的命令行的一部分上被触发。 argparse
维护一个由用户定义的默认Actions
(例如:store, store_true, const
等)和CustomAction
对象的列表。然后将这些循环并在命令行的一部分上顺序处理以查找匹配并构建与每个Namespace
对应的Action
。在每次迭代中,argparse.Action
可能会发现命令行的某些部分与Action
处理的任何内容都不匹配并返回它们(在_UNRECOGNIZED_ARGS_ATTR
字段中,该字段由属性{{1}标识} '_unrecognized_args'
)回到来电者
来自argparse.py#parse_known_args(..):
Namespace
如上所示,如果找到任何无法识别的参数,它们将返回try:
namespace, args = self._parse_known_args(args, namespace)
if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
return namespace, args
except ArgumentError:
err = _sys.exc_info()[1]
self.error(str(err))
中的调用者。 args
类可以利用它来让它们由后面的任何其他NameValueAction
或项目的(Actions
)模块进行处理。因此,班级发生了变化:
runner.py
因此cmd行的工作原理如下:
class NameValueAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
for value in values:
try:
n, v = value.split('=')
setattr(namespace, n, v)
except ValueError:
# when input has ended without an option, probably at module name
setattr(namespace, '_unrecognized_args', values[values.index(value):])
如果在$ python runner.py --env=target_env --props name1=value1 name2=value2 module/
之后指定了其他选项,--props
将停止处理当前argparse
并向前迭代。所以以下内容也可以使用
Action
答案 3 :(得分:1)
(回答是因为我还需要事先从列表中“吃掉”一些未知的参数,下面的解决方案相当通用。)
正如上面提到的@hpaulj,如果没有子类化ArgumentParser
,使用位置参数将无法工作,因为解析器只是将所有内容传递给Action
,但是如果你只想要解析选项并获得非选项参数返回列表(即将它们传递给不同的解析器),以下工作(至少在Python 3.4上):
#!/usr/bin/env python3
import argparse
import itertools
class EatUnknown(argparse.Action):
def __init__(self, option_strings, dest, nargs=None, *args, **kwargs):
nargs = argparse.REMAINDER
super().__init__(option_strings, dest, nargs, *args, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
def all_opt_strings(parser):
nested = (x.option_strings for x in parser._actions
if x.option_strings)
return itertools.chain.from_iterable(nested)
all_opts = list(all_opt_strings(parser))
eaten = []
while len(values) > 0:
if values[0] in all_opts:
break
eaten.append(values.pop(0))
setattr(namespace, self.dest, eaten)
_, extras = parser._parse_known_args(values, namespace)
try:
getattr(namespace, argparse._UNRECOGNIZED_ARGS_ATTR).extend(extras)
except AttributeError:
setattr(namespace, argparse._UNRECOGNIZED_ARGS_ATTR, extras)
parser = argparse.ArgumentParser()
parser.add_argument("--foo", action="append")
parser.add_argument('--eatme', action=EatUnknown)
parser.add_argument('--eater', action=EatUnknown)
print(parser.parse_known_args())
可生产
$ ./argparse_eater.py --foo 1 AAA --eater 2 --unk-opt 3 --foo 4 BBB --eatme 5 --another-unk --foo 6 CCC
(Namespace(eater=['2', '--unk-opt', '3'], eatme=['5', '--another-unk'], foo=['1', '4', '6']), ['AAA', 'CCC', 'BBB'])
此示例“吃掉”任何非选项以及未知选项参数(其中nargs='*'
无法使用,证明示例正确),但不是allow_abbrev
兼容。
这个想法是使用一个简单的递归,这显然适用于代码是可重入的。可能不是最好的想法,但使用_unrecognized_args
并不是更好。
鉴于OP,这适用于--props
的多次出现。