带有常用选项的 Python 多命令 CLI
Posted
技术标签:
【中文标题】带有常用选项的 Python 多命令 CLI【英文标题】:Python multi-command CLI with common options 【发布时间】:2020-04-10 22:48:56 【问题描述】:我正在为我的 Python 应用程序添加 CLI。 CLI 应该允许一次运行多个命令。命令应该有通用选项和个人选项。
示例:
$ python mycliapp.py --common-option1 value1 --common-option2 value2 cmd1 --cmd1-option cmd2 --cmd2-option somevalue cmd3
该示例具有所有命令使用的两个常用选项,并且每个命令可以具有或不具有仅由该命令使用的选项。
我考虑过 Python Click。它具有丰富的功能,但它不允许(至少我没有找到)在没有一些主命令的情况下使用常用选项。
上面的例子在点击后会如下所示:
$ python mycliapp.py maincmd --common-option1 value1 --common-option2 value2 cmd1 --cmd1-option cmd2 --cmd2-option somevalue cmd3
另外,考虑 Python Argparse。看起来它可以做我需要的事情,并且我已经设法编写了一个代码,该代码适用于常用选项和单个命令,但无法管理使用多个命令。 这个页面Python argparse - Add argument to multiple subparsers 有很好的例子,但似乎command2 应该是command1 的子命令。这有点不同,因为我需要命令可以按任何顺序执行。
【问题讨论】:
【参考方案1】:Click 绝对支持这种语法。一个简单的示例如下所示:
import click
@click.group(chain=True)
@click.option('--common-option1')
@click.option('--common-option2')
def main(common_option1, common_option2):
pass
@main.command()
@click.option('--cmd1-option', is_flag=True)
def cmd1(cmd1_option):
pass
@main.command()
@click.option('--cmd2-option')
def cmd2(cmd2_option):
pass
@main.command()
def cmd3():
pass
if __name__ == '__main__':
main()
假设以上内容在mycliapp.py
,我们看到常见的帮助输出:
$ python example.py --help
Usage: example.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--common-option1 TEXT
--common-option2 TEXT
--help Show this message and exit.
Commands:
cmd1
cmd2
cmd3
对于cmd1
:
$ python mycliapp.py cmd1 --help
Usage: mycliapp.py cmd1 [OPTIONS]
Options:
--cmd1-option
--help Show this message and exit.
对于cmd2
:
$ python mycliapp.py cmd2 --help
Usage: mycliapp.py cmd2 [OPTIONS]
Options:
--cmd2-option TEXT
--help Show this message and exit.
等等
有了这个,我们可以从你的问题中运行命令行:
python mycliapp.py --common-option1 value1 --common-option2 value2 \
cmd1 --cmd1-option \
cmd2 --cmd2-option somevalue \
cmd3
更新 1
这是一个使用in the documentation建议的回调模型实现管道的示例:
import click
@click.group(chain=True)
@click.option('--common-option1')
@click.option('--common-option2')
@click.pass_context
def main(ctx, common_option1, common_option2):
ctx.obj =
'common_option1': common_option1,
'common_option2': common_option2,
@main.resultcallback()
def process_pipeline(processors, common_option1, common_option2):
print('common_option1 is', common_option1)
for func in processors:
res = func()
if not res:
raise click.ClickException('Failed processing!')
@main.command()
@click.option('--cmd1-option', is_flag=True)
def cmd1(cmd1_option):
def process():
print('This is cmd1')
return cmd1_option
return process
@main.command()
@click.option('--cmd2-option')
def cmd2(cmd2_option):
def process():
print('This is cmd2')
return cmd2_option != 'fail'
return process
@main.command()
@click.pass_context
def cmd3(ctx):
def process():
print('This is cmd3 (common option 1 is: common_option1'.format(**ctx.obj))
return True
return process
if __name__ == '__main__':
main()
每个命令都返回一个布尔值,指示它是否成功。失败的命令将中止管道处理。例如,这里 cmd1
失败,所以 cmd2
永远不会执行:
$ python mycliapp.py cmd1 cmd2
This is cmd1
Error: Failed processing!
但如果我们让cmd1
开心,它就会起作用:
$ python mycliapp.py cmd1 --cmd1-option cmd2
This is cmd1
This is cmd2
同样,比较一下:
$ python mycliapp.py cmd1 --cmd1-option cmd2 --cmd2-option fail cmd3
This is cmd1
This is cmd2
Error: Failed processing!
有了这个:
$ python mycliapp.py cmd1 --cmd1-option cmd2 cmd3
This is cmd1
This is cmd2
This is cmd3
当然你不需要按顺序调用:
$ python mycliapp.py cmd2 cmd1 --cmd1-option
This is cmd2
This is cmd1
【讨论】:
这个可以和管道一起使用,所以我知道之前命令的结果(click.palletsprojects.com/en/7.x/commands/…)?我没有设法将它们组合起来,如本例所示github.com/pallets/click/blob/master/examples/imagepipe/…。 您绝对可以将其应用于管道的结构。这几乎就是click.palletsprojects.com/en/7.x/commands/… 中显示的内容。如果您遇到问题,可以打开一个显示您的代码的新问题,我们可以弄清楚发生了什么。 问题是必须有生成器和处理器。 Generator 与 Processor 共享通用选项(据我所知)。但就我而言,没有生成器(没有主命令)。 但是在这个例子中有一个主命令……它是程序的入口点,也是定义常用选项的地方。如果您使用处理器模型,那么您将在其中附加结果回调。如果您想更新您的问题以显示您尝试实现的特定行为,我们可以尝试修改此示例以匹配。 使用pass_context
装饰器并将您的常用选项存储在 ctx.obj
上。请参阅 main
和 cmd3
的更新代码来演示这一点。【参考方案2】:
您可以在没有main command
的情况下使用argparse
来做到这一点。
# maincmd just to tie between arguments and subparsers
parser = argparse.ArgumentParser(prog='maincmd')
parser.add_argument('--common-option1', type=str, required=False)
parser.add_argument('--common-option2', type=str, required=False)
main_subparsers = parser.add_subparsers(title='sub_main', dest='sub_cmd')
parser_cmd1 = main_subparsers.add_parser('cmd1', help='help cmd1')
parser_cmd1.add_argument('--cmd1-option', type=str, required=False)
cmd1_subparsers = parser_cmd1.add_subparsers(title='sub_cmd1', dest='sub_cmd1')
parser_cmd2 = cmd1_subparsers.add_parser('cmd2', help='help cmd2')
options = parser.parse_args(sys.argv[1:])
print(vars(options))
让我们检查一下:
python test.py --common-option1 value1 --common-option2 value2
#'common_option1': 'value1', 'common_option2': 'value2', 'sub_cmd': None
python test.py --common-option1 value1 --common-option2 value2 cmd1
# 'common_option1': 'value1', 'common_option2': 'value2', 'sub_cmd': 'cmd1', 'cmd1_option': None, 'sub_cmd1': None
python test.py --common-option1 value1 --common-option2 value2 cmd1 --cmd1-option cmd1-val
# 'common_option1': 'value1', 'common_option2': 'value2', 'sub_cmd': 'cmd1', 'cmd1_option': 'cmd1-val', 'sub_cmd1': None
python test.py --common-option1 value1 --common-option2 value2 cmd1 --cmd1-option cmd1-val cmd2
# 'common_option1': 'value1', 'common_option2': 'value2', 'sub_cmd': 'cmd1', 'cmd1_option': 'cmd1-val', 'sub_cmd1': 'cmd2'
仅供参考。我曾与Click
和argparse
一起工作。 argparse
在我看来更具可扩展性和功能性。
希望这会有所帮助。
【讨论】:
谢谢!当 cmd2 放在 cmd1 旁边时,此方法有效。当命令的顺序不同时(cmd2 在 cmd1 之前),它说maincmd: error: argument sub_cmd: invalid choice: 'cmd2' (choose from 'cmd1')
@Elephant 谢谢。你说的对。这是因为cmd1
是cmd2
的子解析器。但您也可以将cmd2
添加到主解析器main_subparsers.add_parser('cmd2', help='help cmd2')
。在这种情况下,您可以在 cmd1
之后调用 cmd2
并且不使用 cmd1
。以上是关于带有常用选项的 Python 多命令 CLI的主要内容,如果未能解决你的问题,请参考以下文章