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