Python argparse 位置参数与子命令
我正在使用argparse这个库,想要把子命令和位置参数结合起来,但遇到了一个问题。
下面这段代码运行得很好:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser.add_argument('positional')
subparsers.add_parser('subpositional')
parser.parse_args('subpositional positional'.split())
这段代码把参数解析成了Namespace(positional='positional')
,但是当我把位置参数改成nargs='?'的时候:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser.add_argument('positional', nargs='?')
subparsers.add_parser('subpositional')
parser.parse_args('subpositional positional'.split())
就出现了错误:
usage: [-h] {subpositional} ... [positional]
: error: unrecognized arguments: positional
这是为什么呢?
4 个回答
我觉得问题在于,当调用 add_subparsers
时,会在原来的解析器中添加一个新参数,用来传递子解析器的名称。
比如,使用下面的代码:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser.add_argument('positional')
subparsers.add_parser('subpositional')
parser.parse_args()
你会得到以下的帮助信息:
usage: test.py [-h] {subpositional} ... positional
positional arguments:
{subpositional}
positional
optional arguments:
-h, --help show this help message and exit
注意到 subpositional
出现在 positional
之前。我认为你想要的是把位置参数放在子解析器名称之前。因此,可能你需要在子解析器之前添加这个参数:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('positional')
subparsers = parser.add_subparsers()
subparsers.add_parser('subpositional')
parser.parse_args()
使用这段代码得到的帮助信息是:
usage: test.py [-h] positional {subpositional} ...
positional arguments:
positional
{subpositional}
optional arguments:
-h, --help show this help message and exit
这样,你首先传递主解析器的参数,然后是子解析器的名称,最后是子解析器的参数(如果有的话)。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('positional', nargs='?')
subparsers = parser.add_subparsers()
subparsers.add_parser('subpositional')
print(parser.parse_args(['positional', 'subpositional']))
# -> Namespace(positional='positional')
print(parser.parse_args(['subpositional']))
# -> Namespace(positional=None)
parser.print_usage()
# -> usage: bpython [-h] [positional] {subpositional} ...
一般来说,命令前面的参数(左边的)属于主程序,而后面的参数(右边的)属于具体的命令。因此,positional
参数应该放在命令 subpositional
之前。比如说,git
和 twistd
这些程序就是这样用的。
另外,如果有一个参数是 narg=?
,那它可能应该是一个选项(--opt=value
),而不是一个位置参数。
一开始我和jcollado的想法一样,但后来发现,如果后面的(顶层)位置参数有特定的 nargs
(比如 nargs
= None
或 nargs
= 整数),那么它就会按预期工作。当 nargs
是 '?'
或 '*'
时,它就会出问题,有时 nargs
是 '+'
也会出问题。所以,我查看了代码,想搞清楚到底发生了什么。
问题的关键在于参数是如何被分开的。为了弄清楚每个参数该怎么处理,调用 parse_args
会把参数总结成一个像 'AA'
的字符串,在你的例子中是('A'
代表位置参数,'O'
代表可选参数),然后根据你通过 .add_argument
和 .add_subparsers
方法添加的操作,生成一个正则表达式模式来匹配这个总结字符串。
在你的例子中,参数字符串最终是 'AA'
。变化的是要匹配的模式(你可以在 argparse.py
的 _get_nargs_pattern
下看到可能的模式)。对于 subpositional
,最终的模式是 '(-*A[-AO]*)'
,这意味着 允许一个参数后面跟任意数量的选项或参数。对于 positional
,则取决于传给 nargs
的值:
None
=>'(-*A-*)'
- 3 =>
'(-*A-*A-*A-*)'
(每个预期参数一个'-*A'
) '?'
=>'(-*A?-*)'
'*'
=>'(-*[A-]*)'
'+'
=>'(-*A[A-]*)'
这些模式会被拼接,对于 nargs=None
(你的工作示例),最终得到 '(-*A[-AO]*)(-*A-*)'
,这匹配到两个组 ['A', 'A']
。这样,subpositional
只会解析 subpositional
(这是你想要的),而 positional
会匹配它的操作。
但是对于 nargs='?'
,你最终得到的是 '(-*A[-AO]*)(-*A?-*)'
。第二组完全由可选模式组成,而 *
是贪婪的,这意味着第一组会把字符串中的所有内容都匹配上,最终识别到两个组 ['AA', '']
。这意味着 subpositional
得到了两个参数,当然会出问题。
有趣的是,nargs='+'
的模式是 '(-*A[-AO]*)(-*A[A-]*)'
,这在 你只传一个参数时 是有效的。比如说 subpositional a
,因为你在第二组中至少需要一个位置参数。再说一次,由于第一组是贪婪的,传入 subpositional a b c d
会得到 ['AAAA', 'A']
,这显然不是你想要的。
简而言之:一团糟。我想这应该算是一个bug,但不确定如果把模式改成非贪婪的会有什么影响……