做者: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
還爲咱們提供了許多錦上添花的功能,好比實用工具、參數自動補全等,咱們將在下節詳細介紹。
『講解開源項目系列』——讓對開源項目感興趣的人再也不畏懼、讓開源項目的發起者再也不孤單。跟着咱們的文章,你會發現編程的樂趣、使用和發現參與開源項目如此簡單。歡迎留言聯繫咱們、加入咱們,讓更多人愛上開源、貢獻開源~