带有常用选项的 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 上。请参阅 maincmd3 的更新代码来演示这一点。【参考方案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'

仅供参考。我曾与Clickargparse 一起工作。 argparse 在我看来更具可扩展性和功能性。

希望这会有所帮助。

【讨论】:

谢谢!当 cmd2 放在 cmd1 旁边时,此方法有效。当命令的顺序不同时(cmd2 在 cmd1 之前),它说maincmd: error: argument sub_cmd: invalid choice: 'cmd2' (choose from 'cmd1') @Elephant 谢谢。你说的对。这是因为cmd1cmd2 的子解析器。但您也可以将cmd2 添加到主解析器main_subparsers.add_parser('cmd2', help='help cmd2')。在这种情况下,您可以在 cmd1 之后调用 cmd2 并且不使用 cmd1

以上是关于带有常用选项的 Python 多命令 CLI的主要内容,如果未能解决你的问题,请参考以下文章

Python基础之Linux基础:远程管理常用命令

Python 选项解析器:带有可选参数的布尔标志

Linux常用命令——chattrlsattr

Linux常用命令——head

linux常用命令集(软件包操作命令,共2个)

如何在驻留在不同路径的包上运行带有mod选项“-m”的Python3?