Python 命令行之旅:深入 click 之子命令篇
作者:HelloGitHub-Prodesire
HelloGitHub 的《讲解开源项目》系列,项目地址:https://github.com/HelloGitHub-Team/Article
一、前言
在上两篇文章中,我们介绍了 click
中的”参数“和“选项”,本文将继续深入了解 click
,着重讲解它的“命令”和”组“。
本系列文章默认使用 Python 3 作为解释器进行讲解。
若你仍在使用 Python 2,请注意两者之间语法和库的使用差异哦~
二、命令和组
Click
中非常重要的特性就是任意嵌套命令行工具的概念,通过 Command 和 Group (实际上是 MultiCommand)来实现。
所谓命令组就是若干个命令(或叫子命令)的集合,也成为多命令。
2.1 回调调用
对于一个普通的命令来说,回调发生在命令被执行的时候。如果这个程序的实现中只有命令,那么回调总是会被触发,就像我们在上一篇文章中举出的所有示例一样。不过像 --help
这类选项则会阻止进入回调。
对于组和多个子命令来说,情况略有不同。回调通常发生在子命令被执行的时候:
@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
的处理逻辑(也就是命令的回调)。
2.2 嵌套处理和上下文
从上面的例子可以看到,命令组 cli
接收的参数和子命令 sync
彼此独立。但是有时我们希望在子命令中能获取到命令组的参数,这就可以用 Context 来实现。
每当命令被调用时,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']
获得上一步的参数 - 通过这种方式完成了从命令组到子命令的参数传递
2.3 不使用命令来调用命令组
默认情况下,调用子命令的时候才会调用命令组。而有时你可能想直接调用命令组,通过指定 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
来判断是否由子命令触发,针对两种情况打印日志。
2.4 自定义命令组/多命令
除了使用 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()
2.5 合并命令组/多命令
当有多个命令组,每个命令组中有一些命令,你想把所有的命令合并在一个集合中时,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:如果多个命令组中定义了同样的子命令,那么取第一个命令组中的子命令。
2.6 链式命令组/多命令
有时单级子命令可能满足不了你的需求,你甚至希望能有多级子命令。典型地,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
2.7 命令组/多命令管道
链式命令组中一个常见的场景就是实现管道,这样在上一个命令处理好后,可将结果传给下一个命令处理。
实现命令组管道的要点是让每个命令返回一个处理函数,然后编写一个总的管道调度函数(并由 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
变成生成器,然后调用处理函数(实际输入几个命令,就有几个处理函数)进行处理
2.8 覆盖默认值
默认情况下,参数的默认值是从通过装饰器参数 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
还为我们提供了许多锦上添花的功能,比如实用工具、参数自动补全等,我们将在下节详细介绍。
『讲解开源项目系列』——让对开源项目感兴趣的人不再畏惧、让开源项目的发起者不再孤单。跟着我们的文章,你会发现编程的乐趣、使用和发现参与开源项目如此简单。欢迎留言联系我们、加入我们,让更多人爱上开源、贡献开源~
Python 命令行之旅:深入 click 之子命令篇的更多相关文章
- Python 命令行之旅 —— 初探 argparse
『讲解开源项目系列』启动--让对开源项目感兴趣的人不再畏惧.让开源项目的发起者不再孤单.跟着我们的文章,你会发现编程的乐趣.使用和发现参与开源项目如此简单.欢迎联系我们给我们投稿,让更多人爱上开源.贡 ...
- Linux命令行上执行操作,不退回命令行的解决方法
问题描述: 如果你现在登录Centos执行了某个操作,但是操作一直占用命令行,命令行显示的也都是这个命令相关的操作,我想做其它事情 ,该怎么办呢 ? 解决方法: 根据<Linux命令行与Shel ...
- 批处理命令行CMD启动停止重启IIS的命令
原文:批处理命令行CMD启动停止重启IIS的命令 启动IIS: net start iisadmin (IIS的整个服务) net start w3svc (WWW网页WEB服务) ...
- 用 nodejs 写一个命令行工具 :创建 react 组件的命令行工具
用 nodejs 写一个命令行工具 :创建 react 组件的命令行工具 前言 上周,同事抱怨说 react 怎么不能像 angular 那样,使用命令行工具来生成一个组件.对呀,平时工作时,想要创建 ...
- Python 命令行之旅:深入 click 之参数篇
作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...
- Python 命令行之旅:深入 click 之选项篇
作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...
- Python 命令行之旅:深入 click 之增强功能
作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...
- Python 命令行之旅:使用 click 实现 git 命令
作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...
- Python 命令行之旅:使用 docopt 实现 git 命令
作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...
随机推荐
- 算法问题实战策略 QUADTREE
地址 https://algospot.com/judge/problem/read/QUADTREE 将压缩字符串还原后翻转再次压缩的朴素做法 在数据量庞大的情况下是不可取的 所以需要在压缩的情况下 ...
- 用Docker搭建一个支持https的nginx代理服务
用Docker搭建一个支持https的nginx代理服务 说明:本文所提的服务只是作者平常测试使用,可能含有未知bug或不成熟的解决方案,仅供参考,请不要用于正式环境,当然,使用过程中有任何问题欢迎提 ...
- Mybaits 源码解析 (三)----- Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
上一篇我们讲解到mapperElement方法用来解析mapper,我们这篇文章具体来看看mapper.xml的解析过程 mappers配置方式 mappers 标签下有许多 mapper 标签,每一 ...
- Eureka error "java.net.UnknownHostException:
spring cloud 中zuul智能路由,本地部署没有问题,部署到服务器就报com.netflix.zuul.exception.ZuulException: Forwarding error 项 ...
- zepto源码分析·整体架构
代码数量 1.2.0版本代码量为1650行,去掉注释大概1500左右 代码模块 默认版本只包括核心模块,事件模块,ajax模块,form模块和ie模块,其它模块需要自行拓展加入,其中form模块只包含 ...
- 我发现了Unity3D的2D Light Renderer, 随后就把它抄了过来
. 前几个月,偶然在群里看到有人讨论Unity3D光照,于是我又萌生了一个新的目标----把它抄过来! . 众所周知,3D渲染的整个流水线都跟光照密不可分,相关的技术更是数不甚数,而2D游戏的光照通常 ...
- SpringBoot整合FastJson(七)
一.Maven依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson&l ...
- 聊一聊Java中的各种运算符
计算机之所以叫“计算机”,其最基本用途之一就是运算,对应刚刚接触Java的小伙伴而言,熟悉并掌握Java中的各种运算符及其在表达式中的运算优先级是十分必要的. 算术运算 算术运算主要用来处理数学中的加 ...
- android studio ndk 环境配置成eclipse模式
gradle.properties: android.useDeprecatedNdk=true build.gradle: android { ... sourceSets.main { jni.s ...
- 网络安全-主动信息收集篇第二章-二层网络扫描之scapy
scapy是python第三方库文件,可以使用python进行调用也单独进行使用. 非常强大可以用于抓包.分析.创建.修改.注入网络流量. 使用scapy 详细使用方式可以查看github:https ...