做者:HelloGitHub-Prodesire
python
HelloGitHub 的《讲解开源项目》系列,项目地址:https://github.com/HelloGitHub-Team/Articlegit
在上两篇文章中,咱们介绍了 click
中的”参数“和“选项”,本文将继续深刻了解 click
,着重讲解它的“命令”和”组“。github
本系列文章默认使用 Python 3 做为解释器进行讲解。 若你仍在使用 Python 2,请注意二者之间语法和库的使用差别哦~
Click
中很是重要的特性就是任意嵌套命令行工具的概念,经过 Command 和 Group (其实是 MultiCommand)来实现。编程
所谓命令组就是若干个命令(或叫子命令)的集合,也成为多命令。api
对于一个普通的命令来讲,回调发生在命令被执行的时候。若是这个程序的实现中只有命令,那么回调老是会被触发,就像咱们在上一篇文章中举出的全部示例同样。不过像 --help
这类选项则会阻止进入回调。bash
对于组和多个子命令来讲,状况略有不一样。回调一般发生在子命令被执行的时候:app
@click.group() @click.option('--debug/--no-debug', default=False) def cli(debug): click.echo('Debug mode is %s' % ('on' if debug else 'off')) @cli.command() # @cli, not @click! def sync(): click.echo('Syncing')
执行效果以下:函数
Usage: tool.py [OPTIONS] COMMAND [ARGS]... Options: --debug / --no-debug --help Show this message and exit. Commands: sync $ tool.py --debug sync Debug mode is on Syncing
在上面的示例中,咱们将函数 cli
定义为一个组,把函数 sync
定义为这个组内的子命令。当咱们调用 tool.py --debug sync
命令时,会依次触发 cli
和 sync
的处理逻辑(也就是命令的回调)。工具
从上面的例子能够看到,命令组 cli
接收的参数和子命令 sync
彼此独立。可是有时咱们但愿在子命令中能获取到命令组的参数,这就能够用 Context 来实现。this
每当命令被调用时,click
会建立新的上下文,并连接到父上下文。一般,咱们是看不到上下文信息的。但咱们能够经过 pass_context 装饰器来显式让 click
传递上下文,此变量会做为第一个参数进行传递。
@click.group() @click.option('--debug/--no-debug', default=False) @click.pass_context def cli(ctx, debug): # 确保 ctx.obj 存在而且是个 dict。 (以防 `cli()` 指定 obj 为其余类型 ctx.ensure_object(dict) ctx.obj['DEBUG'] = debug @cli.command() @click.pass_context def sync(ctx): click.echo('Debug is %s' % (ctx.obj['DEBUG'] and 'on' or 'off')) if __name__ == '__main__': cli(obj={})
在上面的示例中:
cli
和子命令 sync
指定装饰器 click.pass_context
,两个函数的第一个参数都是 ctx
上下文cli
中,给上下文的 obj
变量(字典)赋值sync
中经过 ctx.obj['DEBUG']
得到上一步的参数默认状况下,调用子命令的时候才会调用命令组。而有时你可能想直接调用命令组,经过指定 click.group
的 invoke_without_command=True
来实现:
@click.group(invoke_without_command=True) @click.pass_context def cli(ctx): if ctx.invoked_subcommand is None: click.echo('I was invoked without subcommand') else: click.echo('I am about to invoke %s' % ctx.invoked_subcommand) @cli.command() def sync(): click.echo('The subcommand')
调用命令有:
$ tool I was invoked without subcommand $ tool sync I am about to invoke sync The subcommand
在上面的示例中,经过 ctx.invoked_subcommand
来判断是否由子命令触发,针对两种状况打印日志。
除了使用 click.group 来定义命令组外,你还能够自定义命令组(也就是多命令),这样你就能够延迟加载子命令,这会颇有用。
自定义多命令须要实现 list_commands
和 get_command
方法:
import click import os plugin_folder = os.path.join(os.path.dirname(__file__), 'commands') class MyCLI(click.MultiCommand): def list_commands(self, ctx): rv = [] # 命令名称列表 for filename in os.listdir(plugin_folder): if filename.endswith('.py'): rv.append(filename[:-3]) rv.sort() return rv def get_command(self, ctx, name): ns = {} fn = os.path.join(plugin_folder, name + '.py') # 命令对应的 Python 文件 with open(fn) as f: code = compile(f.read(), fn, 'exec') eval(code, ns, ns) return ns['cli'] cli = MyCLI(help='This tool\'s subcommands are loaded from a ' 'plugin folder dynamically.') # 等价方式是经过 click.command 装饰器,指定 cls=MyCLI # @click.command(cls=MyCLI) # def cli(): # pass if __name__ == '__main__': cli()
当有多个命令组,每一个命令组中有一些命令,你想把全部的命令合并在一个集合中时,click.CommandCollection
就派上了用场:
@click.group() def cli1(): pass @cli1.command() def cmd1(): """Command on cli1""" @click.group() def cli2(): pass @cli2.command() def cmd2(): """Command on cli2""" cli = click.CommandCollection(sources=[cli1, cli2]) if __name__ == '__main__': cli()
调用命令有:
$ cli --help Usage: cli [OPTIONS] COMMAND [ARGS]... Options: --help Show this message and exit. Commands: cmd1 Command on cli1 cmd2 Command on cli2
从上面的示例能够看出,cmd1
和 cmd2
分别属于 cli1
和 cli2
,经过 click.CommandCollection
能够将这些子命令合并在一块儿,将其能力提供个同一个命令程序。
Tips:若是多个命令组中定义了一样的子命令,那么取第一个命令组中的子命令。
有时单级子命令可能知足不了你的需求,你甚至但愿能有多级子命令。典型地,setuptools
包中就支持多级/链式子命令: setup.py sdist bdist_wheel upload
。在 click 3.0 以后,实现链式命令组变得很是简单,只需在 click.group
中指定 chain=True
:
@click.group(chain=True) def cli(): pass @cli.command('sdist') def sdist(): click.echo('sdist called') @cli.command('bdist_wheel') def bdist_wheel(): click.echo('bdist_wheel called')
调用命令则有:
$ setup.py sdist bdist_wheel sdist called bdist_wheel called
链式命令组中一个常见的场景就是实现管道,这样在上一个命令处理好后,可将结果传给下一个命令处理。
实现命令组管道的要点是让每一个命令返回一个处理函数,而后编写一个总的管道调度函数(并由 MultiCommand.resultcallback()
装饰):
@click.group(chain=True, invoke_without_command=True) @click.option('-i', '--input', type=click.File('r')) def cli(input): pass @cli.resultcallback() def process_pipeline(processors, input): iterator = (x.rstrip('\r\n') for x in input) for processor in processors: iterator = processor(iterator) for item in iterator: click.echo(item) @cli.command('uppercase') def make_uppercase(): def processor(iterator): for line in iterator: yield line.upper() return processor @cli.command('lowercase') def make_lowercase(): def processor(iterator): for line in iterator: yield line.lower() return processor @cli.command('strip') def make_strip(): def processor(iterator): for line in iterator: yield line.strip() return processor
在上面的示例中:
cli
定义为了链式命令组,而且指定 invoke_without_command=True,也就意味着能够不传子命令来触发命令组uppercase
、lowercase
和 strip
命令process_pipeline
中,将输入 input
变成生成器,而后调用处理函数(实际输入几个命令,就有几个处理函数)进行处理默认状况下,参数的默认值是从经过装饰器参数 default
定义。咱们还能够经过 Context.default_map
上下文字典来覆盖默认值:
@click.group() def cli(): pass @cli.command() @click.option('--port', default=8000) def runserver(port): click.echo('Serving on http://127.0.0.1:%d/' % port) if __name__ == '__main__': cli(default_map={ 'runserver': { 'port': 5000 } })
在上面的示例中,经过在 cli
中指定 default_map
变可覆盖命令(一级键)的选项(二级键)默认值(二级键的值)。
咱们还能够在 click.group
中指定 context_settings
来达到一样的目的:
CONTEXT_SETTINGS = dict( default_map={'runserver': {'port': 5000}} ) @click.group(context_settings=CONTEXT_SETTINGS) def cli(): pass @cli.command() @click.option('--port', default=8000) def runserver(port): click.echo('Serving on http://127.0.0.1:%d/' % port) if __name__ == '__main__': cli()
调用命令则有:
$ cli runserver Serving on http://127.0.0.1:5000/
本文首先介绍了命令的回调调用、上下文,再进一步介绍命令组的自定义、合并、连接、管道等功能,了解到了 click
的强大。而命令组中更加高阶的能力(如命令返回值)则可看官方文档进一步了解。
咱们经过介绍 click
的参数、选项和命令已经可以彻底实现命令行程序的全部功能。而 click
还为咱们提供了许多锦上添花的功能,好比实用工具、参数自动补全等,咱们将在下节详细介绍。
『讲解开源项目系列』——让对开源项目感兴趣的人再也不畏惧、让开源项目的发起者再也不孤单。跟着咱们的文章,你会发现编程的乐趣、使用和发现参与开源项目如此简单。欢迎留言联系咱们、加入咱们,让更多人爱上开源、贡献开源~