【问题标题】:Python multi-command CLI with common options带有常用选项的 Python 多命令 CLI
【发布时间】: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 的子命令。这有点不同,因为我需要命令可以按任何顺序执行。

【问题讨论】:

    标签: python command-line-interface argparse python-click


    【解决方案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
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-04
    • 2016-12-12
    • 2019-10-11
    • 1970-01-01
    • 2014-05-28
    • 1970-01-01
    相关资源
    最近更新 更多