如何基于单个选项在Python中的click CLI中定义控制流,并将其余的传递给另一个命令?

如何基于单个选项在Python中的click CLI中定义控制流,并将其余的传递给另一个命令?,python,command-line-interface,python-click,Python,Command Line Interface,Python Click,这个问题与Python包有关,并且与基于传递给CLI的参数的控制流有关 我正试图构建一个主CLI,使其位于repo的顶部目录,它将从一个中心位置控制许多不同的操作模式。调用此命令时,可能需要n选项,其中第一个选项将确定要调用哪个模块,然后将n-1参数传递给另一个模块。但我希望每个模块的命令和选项都在其各自的模块中定义,而不是在主CLI控制器中定义,因为我试图保持主控制器的简单性,并将每个模块很好地抽象出来 以下是我想要的一个简单示例: import click @click.command(

这个问题与Python包有关,并且与基于传递给CLI的参数的控制流有关

我正试图构建一个主CLI,使其位于repo的顶部目录,它将从一个中心位置控制许多不同的操作模式。调用此命令时,可能需要
n
选项,其中第一个选项将确定要调用哪个模块,然后将
n-1
参数传递给另一个模块。但我希望每个模块的命令和选项都在其各自的模块中定义,而不是在主CLI控制器中定义,因为我试图保持主控制器的简单性,并将每个模块很好地抽象出来

以下是我想要的一个简单示例:

import click


@click.command()
@click.option('--foo-arg', default='asdf')
def foo(foo_arg):
    click.echo(f"Hello from foo with arg {foo_arg}")


@click.command()
@click.option('--bar-arg', default='zxcv')
def bar(bar_arg):
    click.echo(f"Hello from bar with arg {bar_arg}")


@click.command()
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
    if mode == "foo":
        foo()

    if mode == "bar":
        bar()


if __name__ == '__main__':
    cli()
在本例中,
foo()
bar()
应被视为嵌入到repo中的模块,并且可能还需要大量CLI选项,我不想用这些选项重载main
CLI.py
。出于上下文原因,我觉得
foo()
的CLI选项应该位于
foo
中。以下是我希望它如何工作

例1:

python -m cli --mode foo --foo-arg asdf
应该产生

Hello from foo with arg asdf
Hello from bar with arg zxcv
例2:

python -m cli --mode bar --bar-arg zxcv
应该产生

Hello from foo with arg asdf
Hello from bar with arg zxcv
例3:

python -m cli --mode foo --bar-arg qwer
应该失败,因为
foo()
没有
--bar arg
选项

免责声明:我知道我可以将
foo
bar
注册为单独的命令(通过
python-m cli foo--foo arg asd调用,即使用
foo
而不是
--foo
)。但是,由于超出此问题范围的原因,我需要由
--mode
选项标识符指定
foo
bar
。这是与我的应用程序交互的工具的一个限制,不幸的是,这超出了我的控制范围


是否有一种方法可以解析arg并基于arg的子集使控制流成为可能,然后将剩余的arg传递给后续模块,而不是将每个模块的选项定义为
def cli()

上的装饰器?

您可以使用回调进行验证:

另外,使用多链接命令,这样每个参数都将传递给它所需的命令


使用选项调用子命令可以通过使用
click.multi命令和自定义
parse_args()
方法实现,如:

自定义类 使用自定义类 要使用自定义类,请调用
mode\u opts\u cmds()
函数创建自定义类,然后使用
cls
参数将该类传递给
click.group()
装饰器

@click.group(cls=mode_opts_cmds('mode'))
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
    """My wonderful cli"""
这是怎么回事? 这是因为click是一个设计良好的OO框架。
@click.group()
decorator通常实例化
click.group
对象,但允许使用
cls
参数过度处理此行为。因此,在我们自己的类中继承
click.Group
并超越所需的方法是一件相对容易的事情

在本例中,我们跳过了
单击.Group.parse_args()
,这样在解析命令行时,我们只需删除
--mode
,然后命令行就可以像普通子命令一样进行解析

使其成为一个库函数 如果自定义类将驻留在库中,则需要传入名称空间,而不是使用库文件中的默认
globals()
。然后,需要使用以下内容调用自定义类创建者方法:

@click.group(cls=mode_opts_cmds('mode', namespace=globals()))
测试代码 测试结果
我认为问题在于我需要由选项而不是命令来驱动逻辑。这意味着在
python cli.py
之外,后面的所有内容都必须采用
--选项名option\u值
格式。我知道这是一个有点奇怪的要求,但我正在编写的应用程序是由一个外部控制器调用的,该控制器只能使用选项名称和值的字典(而不是命令名称)来形成命令。我可以让上面的示例使用命令,例如
python cli.py foo--foo arg asdf
,但不是
python cli.py--mode foo--foo arg asdf
(这是必需的)。我看到的唯一问题是当我尝试将
mode\u opts\u cmds()
移动到单独的utils模块中并导入它时。在这种情况下,我得到了
KeyError:'foo'
。你有什么建议吗?我将把
foo
bar
放在
cli()
旁边的同一个文件中。我移动了自定义类
def mode\u opts\u cmds(mode\u opt\u name)
。。。在
中单击utils.py
并在
cli.py
顶部单击utils导入模式选择cmds
。这是当它失败并产生
KeyError:'foo'
错误时。很抱歉,您是正确的,我使用了
globals()
,它将特定于类定义所在的文件。检查更新以获得处理此问题的一种方法。是否可以将其修改为以任意顺序接受参数,如
--foo arg asdf--mode foo
?我问这个问题的原因是,与我的应用程序交互的外部服务按字母顺序对CL参数进行排序,这是我无法控制的。@BenD,当然可以。需要做的不是简单地
args.remove(self.mode\u opt\u name)
,而是跟踪
---mode
在列表中的位置,然后将以下arg移动到列表的前面。
Click Version: 7.0
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> --mode foo --foo-arg asdf
Hello from foo with arg asdf
-----------
> --mode bar --bar-arg zxcv
Hello from bar with arg zxcv
-----------
> --mode foo --bar-arg qwer
Usage: test.py foo [OPTIONS]
Try "test.py foo --help" for help.

Error: no such option: --bar-arg
-----------
> --mode foo --help
Usage: test.py foo [OPTIONS]

  the foo command is ok

Options:
  --foo-arg TEXT
  --help          Show this message and exit.
-----------
> 
Usage: test.py [OPTIONS] COMMAND [ARGS]...

  My wonderful cli command

Options:
  --mode [foo|bar]
  --help            Show this message and exit.

Commands:
  bar  bar is my favorite
  foo  the foo command is ok