Python argparse 位置参数与子命令

11 投票
4 回答
7822 浏览
提问于 2025-04-17 09:12

我正在使用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 个回答

5

我觉得问题在于,当调用 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

这样,你首先传递主解析器的参数,然后是子解析器的名称,最后是子解析器的参数(如果有的话)。

6
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 之前。比如说,gittwistd 这些程序就是这样用的。

另外,如果有一个参数是 narg=?,那它可能应该是一个选项(--opt=value),而不是一个位置参数。

9

一开始我和jcollado的想法一样,但后来发现,如果后面的(顶层)位置参数有特定的 nargs(比如 nargs = Nonenargs = 整数),那么它就会按预期工作。当 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,但不确定如果把模式改成非贪婪的会有什么影响……

撰写回答