如何使用 argparse 处理 CLI 子命令

Posted

技术标签:

【中文标题】如何使用 argparse 处理 CLI 子命令【英文标题】:How to handle CLI subcommands with argparse 【发布时间】:2019-02-08 19:18:46 【问题描述】:

我需要实现程序接受子命令的命令行界面。

例如,如果程序名为“foo”,则 CLI 如下所示

foo cmd1 <cmd1-options>
foo cmd2
foo cmd3 <cmd3-options>

cmd1cmd3 必须与它们的至少一个选项一起使用,并且三个 cmd* 参数始终是互斥的。

我正在尝试在 argparse 中使用子解析器,但目前没有成功。问题在于cmd2,它没有参数:

如果我尝试添加不带参数的子解析器条目,parse_args 返回的命名空间将不包含任何信息告诉我选择了此选项(请参见下面的示例)。 如果我尝试将 cmd2 作为参数添加到 parser(而不是子解析器),那么 argparse 将期望 cmd2 参数后跟任何子解析器参数。

有没有一种简单的方法可以通过argparse 实现这一目标?用例应该很常见……

以下是我迄今为止所尝试的更接近我需要的内容:

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='Functions')
parser_1 = subparsers.add_parser('cmd1', help='...')
parser_1.add_argument('cmd1_option1', type=str, help='...')

parser_2 = subparsers.add_parser(cmd2, help='...')

parser_3 = subparsers.add_parser('cmd3', help='...')
parser_3.add_argument('cmd3_options', type=int, help='...')

args = parser.parse_args()

【问题讨论】:

【参考方案1】:

首先,子解析器从不插入到命名空间中。在您发布的示例中,如果您尝试将脚本运行为:

$python3 test_args.py cmd1 1
Namespace(cmd1_option1='1')

test_args.py 包含您提供的代码(import argparse 开头,print(args) 结尾)。

请注意,没有提及 cmd1 仅提及它的论点。 这是设计使然

正如 cmets 中所指出的,您可以将传递 dest 参数的信息添加到 add_subparsers 调用。

处理这些情况的通常方法是使用子解析器的set_defaults 方法:

import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='Functions')
parser_1 = subparsers.add_parser('cmd1', help='...')
parser_1.add_argument('cmd1_option1', type=str, help='...')
parser_1.set_defaults(parser1=True)

parser_2 = subparsers.add_parser('cmd2', help='...')
parser_2.set_defaults(parser2=True)

parser_3 = subparsers.add_parser('cmd3', help='...')
parser_3.add_argument('cmd3_options', type=int, help='...')
parser_3.set_defaults(parser_3=True)

args = parser.parse_args()
print(args)

结果:

$python3 test_args.py cmd1 1
Namespace(cmd1_option1='1', parser1=True)
$python3 test_args.py cmd2
Namespace(parser2=True)

通常,不同的子解析器在大多数情况下会以完全不同的方式处理参数。通常的模式是使用不同的函数来运行不同的命令并使用set_defaults 设置func 属性。当您解析参数时,您只需调用该可调用对象:

subparsers = parser.add_subparsers()
parser_1 = subparsers.add_parser(...)
parser_1.set_defaults(func=do_command_one)

parser_k = subparsers.add_parser(...)
parser_k.set_defaults(func=do_command_k)

args = parser.parse_args()
if args.func:
    args.func(args)

【讨论】:

我对“从未插入”有异议。只需给“add_subparsers”一个dest @hpaulj 你是对的。但是,我个人从未使用过它。正如我所说,为不同的子解析器提供不同的处理功能需要更多的命令,因此使用set_defaults 是一个更好的解决方案。仅适用于您希望使用dest 参数的最简单的情况。【参考方案2】:

如果给add_subparsers 命令提供dest,则可以将子解析器标识添加到主Namespace

来自文档:

但是,如果需要检查被调用的子解析器的名称,add_subparsers() 调用的 dest 关键字参数将起作用:

>>> parser = argparse.ArgumentParser()
>>> subparsers = parser.add_subparsers(dest='subparser_name')
>>> subparser1 = subparsers.add_parser('1')
>>> subparser1.add_argument('-x')
>>> subparser2 = subparsers.add_parser('2')
>>> subparser2.add_argument('y')
>>> parser.parse_args(['2', 'frobble'])
Namespace(subparser_name='2', y='frobble')

默认情况下,destargparse.SUPPRESS,这会阻止 subparsers 将名称添加到 namespace

【讨论】:

以上是关于如何使用 argparse 处理 CLI 子命令的主要内容,如果未能解决你的问题,请参考以下文章

默认子命令,或不使用 argparse 处理子命令

如何使用 python argparse 解析多个嵌套的子命令?

如何有一个特定的子命令需要带有 argparse 的标志?

如何使用 python argparse 解决命令行问题?

将 argparse 别名解析回原始命令

在子解析器 args 之后添加*** argparse 参数