Python 命令行之旅:深入 click 之子命令篇
掃描二維碼
隨時隨地手機看文章
一、前言
在上兩篇文章中,我們介紹了 click中的”參數(shù)“和“選項”,本文將繼續(xù)深入了解
click,著重講解它的“命令”和”組“。
本系列文章默認使用 Python 3 作為解釋器進行講解。
若你仍在使用 Python 2,請注意兩者之間語法和庫的使用差異哦~
二、命令和組
Click中非常重要的特性就是任意嵌套命令行工具的概念,通過 Command[2] 和 Group[3] (實際上是 MultiCommand[4])來實現(xiàn)。
所謂命令組就是若干個命令(或叫子命令)的集合,也成為多命令。
2.1 回調(diào)調(diào)用
對于一個普通的命令來說,回調(diào)發(fā)生在命令被執(zhí)行的時候。如果這個程序的實現(xiàn)中只有命令,那么回調(diào)總是會被觸發(fā),就像我們在上一篇文章中舉出的所有示例一樣。不過像 --help這類選項則會阻止進入回調(diào)。
對于組和多個子命令來說,情況略有不同。回調(diào)通常發(fā)生在子命令被執(zhí)行的時候:
@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')
執(zhí)行效果如下:
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
在上面的示例中,我們將函數(shù) cli定義為一個組,把函數(shù) sync定義為這個組內(nèi)的子命令。當我們調(diào)用 tool.py --debug sync命令時,會依次觸發(fā) cli和 sync的處理邏輯(也就是命令的回調(diào))。
2.2 嵌套處理和上下文
從上面的例子可以看到,命令組 cli接收的參數(shù)和子命令 sync彼此獨立。但是有時我們希望在子命令中能獲取到命令組的參數(shù),這就可以用 Context[5] 來實現(xiàn)。
每當命令被調(diào)用時,click會創(chuàng)建新的上下文,并鏈接到父上下文。通常,我們是看不到上下文信息的。但我們可以通過 pass_context[6] 裝飾器來顯式讓 click傳遞上下文,此變量會作為第一個參數(shù)進行傳遞。
@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,兩個函數(shù)的第一個參數(shù)都是 ctx上下文
-
在命令組 cli中,給上下文的 obj變量(字典)賦值
-
在子命令 sync中通過 ctx.obj['DEBUG']獲得上一步的參數(shù)
-
通過這種方式完成了從命令組到子命令的參數(shù)傳遞
2.3 不使用命令來調(diào)用命令組
默認情況下,調(diào)用子命令的時候才會調(diào)用命令組。而有時你可能想直接調(diào)用命令組,通過指定 click.group的 invoke_without_command=True來實現(xiàn):
@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')
調(diào)用命令有:
$ tool
I was invoked without subcommand
$ tool sync
I am about to invoke sync
The subcommand
在上面的示例中,通過 ctx.invoked_subcommand來判斷是否由子命令觸發(fā),針對兩種情況打印日志。
2.4 自定義命令組/多命令
除了使用 click.group[7] 來定義命令組外,你還可以自定義命令組(也就是多命令),這樣你就可以延遲加載子命令,這會很有用。
自定義多命令需要實現(xiàn) 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') # 命令對應(yīng)的 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()
調(diào)用命令有:
$ 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 之后,實現(xiàn)鏈式命令組變得非常簡單,只需在 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')
調(diào)用命令則有:
$ setup.py sdist bdist_wheel
sdist called
bdist_wheel called
2.7 命令組/多命令管道
鏈式命令組中一個常見的場景就是實現(xiàn)管道,這樣在上一個命令處理好后,可將結(jié)果傳給下一個命令處理。
實現(xiàn)命令組管道的要點是讓每個命令返回一個處理函數(shù),然后編寫一個總的管道調(diào)度函數(shù)(并由 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,也就意味著可以不傳子命令來觸發(fā)命令組
-
定義了三個命令處理函數(shù),分別對應(yīng) uppercase、lowercase和 strip命令
-
在管道調(diào)度函數(shù) process_pipeline中,將輸入 input變成生成器,然后調(diào)用處理函數(shù)(實際輸入幾個命令,就有幾個處理函數(shù))進行處理
2.8 覆蓋默認值
默認情況下,參數(shù)的默認值是從通過裝飾器參數(shù) 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()
調(diào)用命令則有:
$ cli runserver
Serving on http://127.0.0.1:5000/
三、總結(jié)
本文首先介紹了命令的回調(diào)調(diào)用、上下文,再進一步介紹命令組的自定義、合并、鏈接、管道等功能,了解到了 click的強大。而命令組中更加高階的能力(如命令返回值[8])則可看官方文檔進一步了解。
我們通過介紹 click 的參數(shù)、選項和命令已經(jīng)能夠完全實現(xiàn)命令行程序的所有功能。而 click 還為我們提供了許多錦上添花的功能,比如實用工具、參數(shù)自動補全等,我們將在下節(jié)詳細介紹。