涉及的示例代碼和歷史文章,已同步更新到 HelloGitHub-Team 倉庫python
在上兩篇文章中,咱們介紹了 click
中的」參數「和「選項」,本文將繼續深刻了解 click
,着重講解它的「命令」和」組「。git
本系列文章默認使用 Python 3 做爲解釋器進行講解。
若你仍在使用 Python 2,請注意二者之間語法和庫的使用差別哦~
複製代碼
Click
中很是重要的特性就是任意嵌套命令行工具的概念,經過 Command 和 Group (其實是 MultiCommand)來實現。github
所謂命令組就是若干個命令(或叫子命令)的集合,也成爲多命令。編程
對於一個普通的命令來講,回調發生在命令被執行的時候。若是這個程序的實現中只有命令,那麼回調老是會被觸發,就像咱們在上一篇文章中舉出的全部示例同樣。不過像 --help
這類選項則會阻止進入回調。api
對於組和多個子命令來講,狀況略有不一樣。回調一般發生在子命令被執行的時候:bash
@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')
複製代碼
執行效果以下:app
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 來實現。工具
每當命令被調用時,click
會建立新的上下文,並連接到父上下文。一般,咱們是看不到上下文信息的。但咱們能夠經過 pass_context 裝飾器來顯式讓 click
傳遞上下文,此變量會做爲第一個參數進行傳遞。this
@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
還爲咱們提供了許多錦上添花的功能,好比實用工具、參數自動補全等,咱們將在下節詳細介紹。
『講解開源項目系列』——讓對開源項目感興趣的人再也不畏懼、讓開源項目的發起者再也不孤單。跟着咱們的文章,你會發現編程的樂趣、使用和發現參與開源項目如此簡單。歡迎留言聯繫咱們、加入咱們,讓更多人愛上開源、貢獻開源~