无法让argparse读取带破折号的引号字符串?

85

有没有办法使argparse将两个引号之间的内容识别为单个参数?它似乎会继续看到破折号并认为这是一个新选项的开始。

我有类似以下的东西:

mainparser = argparse.ArgumentParser()
subparsers = mainparser.add_subparsers(dest='subcommand')
parser = subparsers.add_parser('queue')
parser.add_argument('-env', '--extraEnvVars', type=str,
                        help='String of extra arguments to be passed to model.')
...other arguments added to parser...

但是当我运行:

python Application.py queue -env "-s WHATEVER -e COOL STUFF"

它给了我:

Application.py queue: error: argument -env/--extraEnvVars: expected one argument

如果我省略第一个破折号,它完全正常工作,但关键是我必须能够传入带有破折号的字符串。我尝试使用 \ 转义,这使它成功了,但在参数字符串中添加了 \ 。是否有人知道如何解决这个问题?无论 -s 是否是解析器中的参数,都会出现这种情况。

编辑:我正在使用Python 2.7。

编辑2:

python Application.py -env " -env"

功能完美,但是

python Application.py -env "-env"

不是这样的。

EDIT3: 看起来这实际上是一个正在被讨论的 bug:http://www.gossamer-threads.com/lists/python/bugs/89529http://python.6.x6.nabble.com/issue9334-argparse-does-not-accept-options-taking-arguments-beginning-with-dash-regression-from-optp-td578790.html。这只发生在2.7中,而不是在optparse中。

EDIT4: 目前的bug报告为:http://bugs.python.org/issue9334


你使用的是哪个版本的Python? - William
我正在使用Python 2.7。 - sfendell
这在我的Python 2.7上运行良好。你有任何其他参数配置吗? - Martijn Pieters
是的,有一些。此外,“-e”是我的程序中一个子解析器的参数。我会发布更完整的代码片段以使其更清晰。 - sfendell
嗯...我很确定。我的其他选项都正常工作,只要带引号的字符串不以破折号开头,extraEnvVars就能按预期工作。例如,python Application.py queue -env " -env" 就可以正常工作。 - sfendell
顺便提一下,在这里“引用”是完全无意义的——在argparse(或Python解释器)有机会判断它们是否存在之前,引号已经被shell消耗掉了。 - Charles Duffy
7个回答

92

更新的答案:

在调用时可以加上等号:

python Application.py -env="-env"
我曾经也遇到过你要做的事情有些困难,但是 argparse 中有一个内置的解决方法,即 parse_known_args 方法。使用该方法可以让你没有定义的所有参数通过解析器,但这意味着你会将它们用于子进程。缺点是无法报告错误的参数,并且您必须确保您的选项与子进程的选项之间没有冲突。
另一个选择可能是强制用户使用加号而不是减号:
python Application.py -e "+s WHATEVER +e COOL STUFF"

在后处理之前,将“+”更改为“-”,然后再传递给您的子进程。


1
我不认为parse_known_args对我有帮助。我根本不想读取引号中的参数;我希望将带引号的字符串作为单个对象传递给-env。我考虑过后处理路线,如果我在这里得不到更好的答案,我可能会这样做,但它感觉很hacky,并且意味着字符串中的+字符会变成-。我真的希望能够传递包含任何字符的字符串。 - sfendell
1
我明白你的问题... 如果你想要读取多个没有引号的字符串,那么使用 nargs='+' 告诉 -env 读取一个或多个字符串。 - SethMMorton
但我也希望其中一些字符串中有破折号,甚至可能与我的子解析器中的参数名称相同。类似python Application.py queue -env“-env blah”应该可以工作。 - sfendell
1
很抱歉,我已经没有更多的想法了。我尝试过做同样的事情,但最终选择重新实现选项以通过子进程传递,因为我无法让你尝试的东西起作用。祝你好运!希望有人能提出一个好的建议,我们可以尝试一下。 - SethMMorton
8
尝试使用等号:python Application.py -env="-env",将其改为python Application.py --env="-env" - SethMMorton
@SethMMorton python Application.py -env="-env" 对我来说完美无缺,谢谢!你应该考虑将它发布为一个新答案,这样它就不会被埋没在这里了。 - dshepherd

27

这个问题在http://bugs.python.org/issue9334中有深入讨论。大部分活动发生在2011年。我去年添加了一个补丁,但是argparse的积压补丁很多。

问题在于像'--env'"-s WHATEVER -e COOL STUFF"这样的字符串,在跟随一个需要参数的选项后可能存在歧义。

optparse做一个简单的从左到右的解析。第一个--env是一个带一个参数的选项标志,因此它会消耗下一个,无论它看起来像什么。另一方面,argparse会两次循环通过这些字符串。首先,它将它们分类为'O'或'A'(选项标志或参数)。在第二个循环中,它使用类似于正则表达式的模式匹配来处理可变的nargs值。在这种情况下,看起来我们有OO,即两个标志而没有参数。

在使用argparse时的解决方案是确保参数字符串不会被误认为是选项标志。这里(和Bug问题中)显示的可能性包括:

--env="--env"  # clearly defines the argument.

--env " --env"  # other non - character
--env "--env "  # space after

--env "--env one two"  # but not '--env "-env one two"'
'--env'本身看起来像一个标志(即使用引号括起来,参见sys.argv),但是当跟随其他字符串时它不是。但是"-env one two"有问题,因为它可以被解析为['-e','nv one two'],一个`'-e'`标志后面跟着一个字符串(甚至更多的选项)。 --nargs=argparse.PARSER也可以用于强制argparse将所有后续字符串视为参数。但是它们仅在参数列表末尾起作用。
在issue9334中提出了一个补丁来添加args_default_to_positional=True模式。在这种模式下,只有当解析器能够清楚地将字符串与定义的参数匹配时,才将字符串分类为选项标志。因此,在“--env --one”中,“--one”将被分类为参数。但是“--env --env”中的第二个“--env”仍然会被分类为选项标志。
进一步扩展相关案例,请参阅:Using argparse with argument values that begin with a dash ("-").
parser = argparse.ArgumentParser(prog="PROG")
parser.add_argument("-f", "--force", default=False, action="store_true")
parser.add_argument("-e", "--extra")
args = parser.parse_args()
print(args)

生产

1513:~/mypy$ python3 stack16174992.py --extra "--foo one"
Namespace(extra='--foo one', force=False)
1513:~/mypy$ python3 stack16174992.py --extra "-foo one"
usage: PROG [-h] [-f] [-e EXTRA]
PROG: error: argument -e/--extra: expected one argument
1513:~/mypy$ python3 stack16174992.py --extra "-bar one"
Namespace(extra='-bar one', force=False)
1514:~/mypy$ python3 stack16174992.py -fe one
Namespace(extra='one', force=True)

"-foo one" 的情况失败了,因为 -foo 被解释为 -f 标志加上未指定的额外内容。这个行为与允许 -fe 被解释为 ['-f', '-e'] 相同。

如果我将 nargs 改为 REMAINDER(而不是 PARSER),则 -e 后面的所有内容都会被解释为该标志的参数:

parser.add_argument("-e", "--extra", nargs=argparse.REMAINDER)

所有情况都可行。请注意,值是一个列表。不需要引号:

1518:~/mypy$ python3 stack16174992.py --extra "--foo one"
Namespace(extra=['--foo one'], force=False)
1519:~/mypy$ python3 stack16174992.py --extra "-foo one"
Namespace(extra=['-foo one'], force=False)
1519:~/mypy$ python3 stack16174992.py --extra "-bar one"
Namespace(extra=['-bar one'], force=False)
1519:~/mypy$ python3 stack16174992.py -fe one
Namespace(extra=['one'], force=True)
1520:~/mypy$ python3 stack16174992.py --extra --foo one
Namespace(extra=['--foo', 'one'], force=False)
1521:~/mypy$ python3 stack16174992.py --extra -foo one
Namespace(extra=['-foo', 'one'], force=False)

argparse.REMAINDER类似于'*',但它会获取接下来的所有内容,不管它看起来是否像一个标志。而argparse.PARSER更像是'+', 它期望首先传递一个位置参数。这是由subparsers使用的nargs

对于REMAINDER的使用已经在文档中有所记录,可以参考https://docs.python.org/zh-cn/3/library/argparse.html#nargs


3
非常感谢:nargs=argparse.PARSER 对我非常有帮助。 - guettli
我不太确定发生了什么变化,但现在 python Application.py queue -env "-s WHATEVER -e COOL STUFF" 能用了。queue -env "-foo" 仍然会抛出错误,因为独立的 '-foo'(或 '--foo')仍被解释为标志。显然,标志后面的空格是有影响的。 - hpaulj

21
您可以通过在空格后开始参数 python tst.py -e ' -e blah' 作为一个非常简单的解决方法。如果您喜欢,只需使用 lstrip() 函数将选项还原即可。
或者,如果第一个 "子参数" 不是原始函数的有效参数,那么您根本不需要做任何事情。也就是说,python tst.py -e '-s hi -e blah' 无法正常工作的唯一原因是 -stst.py 的有效选项。
此外,已弃用的 optparse 模块可以无任何问题地使用。

3
我认为这不会发生,因为-s是子解析器的有效选项。我尝试了以下代码:python Application.py queue -e "-notarealoption"并得到了相同的错误。我更喜欢使用lstrip而不是像SethMMorton建议的用replace将+替换为-,但似乎应该有一种方法来引用一个字符串,使得argparse不会替换/修改/读取其中任何内容。 - sfendell
1
真的吗?我只是根据我现在的短暂测试做出了这个假设。我编写了一个脚本,它接受一个参数“-a”,并简单地将其发送为“-a '-b hello'”,它可以正常工作。但我想我正在使用不同版本的Python。 - William
1
我编辑了我的原始问题。显然这是argparse中已知的一个错误,而且版本大于2.7 :(. 最终我在调用parser.parse_args()之前修改了sys.argv,在-env选项的开头添加了一个虚拟字符,并在之后将其删除。这很不专业,但我终于得到了我想要的结果。 - sfendell
我成功地使用了一个以空格开头的带引号的字符串。因此,我的解析器在参数多值输入“-10:a 10:b”上失败了,但对于“' -10:a' 10:b”却起作用了。 - J.B. Brown
我简直不敢相信这个包在处理一个简单的破折号作为参数时如此糟糕。Django管理仍在使用它,所以我必须使用它。如果不行,我可以使用Click。 - swdev
@William:-a '-b hello' 是可以工作的;但是尝试 -a '-bhello',无论你是否有 -b-bhello 选项,它都会出错。看起来如果你的引用值中有空格,argparse 可以很好地处理它,即使它以破折号开头。如果没有,就不行了。 - fireattack

1
类似的问题。我通过将空格替换为“\”来解决这个问题。例如:

python Application.py "cmd -option"
替换为
python Application.py "cmd\ -option"
不确定是否适用于你的问题。

1
我已将一个脚本从optparse移植到argparse,其中某些参数的值可能以负数开头。我遇到了这个问题,因为该脚本在许多地方被使用时没有使用'='符号将负值与标志连接起来。在阅读此处和http://bugs.python.org/issue9334中的讨论后,我知道参数只接受一个值,并且接受后续参数(即缺少的值)作为值是没有风险的。顺便说一下,我的解决方案是预处理参数并将有问题的参数与'='连接起来,然后再传递给parse_args()函数:
def preprocess_negative_args(argv, flags=None):
    if flags is None:
        flags = ['--time', '--mtime']
    result = []
    i = 0
    while i < len(argv):
        arg = argv[i]
        if arg in flags and i+1 < len(argv) and argv[i+1].startswith('-'):
            arg = arg + "=" + argv[i+1]
            i += 1
        result.append(arg)
        i += 1
    return result

这种方法至少不需要任何用户更改,它只修改明确需要允许负值的参数。
>>> import argparse
>>> parser = argparse.ArgumentParser("prog")
>>> parser.add_argument("--time")
>>> parser.parse_args(preprocess_negative_args("--time -1d,2".split()))
Namespace(time='-1d,2')

告诉argparse应明确允许带有前导破折号的参数更方便,但这种方法似乎是一个合理的妥协。


0
paser.add_argument("--argument_name", default=None, nargs=argparse.REMAINDER)

注意:参数值必须仅在双引号内传递,单引号无效。 python_file.py --argument_name "--abc=10 -a=1 -b=2 cdef"

0
为了避免处理 argparse 时即使查看 '-' 也不是你想要的标志,你可以在 argparse 读取之前编辑 sys.argv。只需保存您不想看到的参数,放置一个填充参数,然后在 argparse 处理 sys.argv 后将填充替换为原始参数。我刚刚为自己的代码做了这件事。它不太好看,但它有效且容易。如果您的标志不总是按相同顺序出现,还可以使用 for 循环来迭代 sys.argv。
parser.add_argument('-n', '--input', nargs='*')
spot_saver = ''
if sys.argv[1] == '-n':             #'-n' can be any flag you use
    if sys.argv[2][0] == '-':       #This checks the first character of the element
        spot_saver = sys.argv[2] 
        sys.argv[2] = "fillerText" 
args = parser.parse_args()
if args.input[0] == 'fillerText':
    args.input[0] = spot_saver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接