原文鏈接:Python Click 學習筆記
Click 是 Flask 的團隊 pallets 開發的優秀開源項目,它為命令行工具的開發封裝了大量方法,使開發者只需要專注於功能實現。恰好我最近在開發的一個小工具需要在命令行環境下操作,就寫個學習筆記。
國際慣例,先來一段 “Hello World” 程序(假定已經安裝了 Click 包)。
# hello.py import click @click.command() @click.option('--count', default=1, help='Number of greetings.') @click.option('--name', prompt='Your name', help='The person to greet.') def hello(count, name): """Simple program that greets NAME for a total of COUNT times.""" for x in range(count): click.echo('Hello %s!' % name) if __name__ == '__main__': hello()
執行 python hello.py --count=3
,不難猜到控制台的輸出結果。除此之外,Click 還悄悄地做了其他的工作,比如幫助選項:
$ python hello.py --help Usage: hello.py [OPTIONS] Simple program that greets NAME for a total of COUNT times. Options: --count INTEGER Number of greetings. --name TEXT The person to greet. --help Show this message and exit.
函數秒變 CLI
從上面的 “Hello World” 演示中可以看出,Click 是通過裝飾器來把一個函數方法裝飾成命令行接口的,這個裝飾器方法就是 @click.command()
。
import click @click.command() def hello(): click.echo('Hello World!')
@click.command()
裝飾器把 hello()
方法變成了 Command
對象,當它被調用時,就會執行該實例內的行為。而 --help
參數就是 Command
對象內置的參數。
不同的 Command
實例可以關聯到 group
中。group
下綁定的命令就成為了它的子命令,參考下面的代碼:
@click.group() def cli(): pass @click.command() def initdb(): click.echo('Initialized the database') @click.command() def dropdb(): click.echo('Dropped the database') cli.add_command(initdb) cli.add_command(dropdb)
@click.group
裝飾器把方法裝飾為可以擁有多個子命令的 Group
對象。由 Group.add_command()
方法把 Command
對象關聯到 Group
對象。
也可以直接用 @Group.command
裝飾方法,會自動把方法關聯到該 Group
對象下。
@click.group() def cli(): pass @cli.command() def initdb(): click.echo('Initialized the database') @cli.command() def dropdb(): click.echo('Dropped the database')
命令行的參數是不可或缺的,Click 支持對 command
方法添加自定義的參數,由 option()
和 argument()
裝飾器實現。
@click.command() @click.option('--count', default=1, help='number of greetings') @click.argument('name') def hello(count, name): for x in range(count): click.echo('Hello %s!' % name)
打包跨平台可執行程序
通過 Click 編寫了簡單的命令行方法后,還需要把 .py
文件轉換成可以在控制台里運行的命令行程序。最簡單的辦法就是在文件末尾加上如下代碼:
if __name__ == '__main__': command()
Click 支持使用 setuptools
來更好的實現命令行程序打包,把源碼文件打包成系統中的可執行程序,並且不限平台。一般我們會在源碼根目錄下創建 setup.py
腳本,先看一段簡單的打包代碼:
from setuptools import setup setup( name='hello', version='0.1', py_modules=['hello'], install_requires=[ 'Click', ], entry_points=''' [console_scripts] hello=hello:cli ''', )
留意 entry_points
字段,在 console_scripts
下,每一行都是一個控制台腳本,等號左邊的的是腳本的名稱,右邊的是 Click 命令的導入路徑。
詳解命令行參數
上面提到了自定義命令行參數的兩個裝飾器:@click.option()
和 @click.argument()
,兩者有些許區別,使用場景也有所不同。
總體而言,argument()
裝飾器比 option()
功能簡單些,后者支持下面的特性:
- 自動提示缺失的輸入;
- option 參數可以從環境變量中獲取,argument 參數則不行;
- option 參數在 help 輸出中有完整的文檔,argument 則沒有;
而 argument 參數可以接受可變個數的參數值,而 option 參數只能接收固定個數的參數值(默認是 1 個)。
Click 可以設置不同的參數類型,簡單類型如 click.STRING
,click.INT
,click.FLOAT
,click.BOOL
。
命令行的參數名由 “-short_name” 和 “–long_name” 聲明,如果參數名既沒有以 “-“ 開頭,也沒有以 “–” 開頭,那么這邊變量名會成為被裝飾方法的內部變量,而非方法參數。
Option 參數
option 最基礎的用法就是簡單值變量,option 接收一個變量值,下面是一段示例代碼:
@click.command() @click.option('--n', default=1) def dots(n): click.echo('.' * n)
如果在命令行后面跟隨參數 --n=2
就會輸出兩個點,如果傳參數,默認輸出一個點。上面的代碼中,參數類型沒有顯示給出,但解釋器會認為是 INT
型,因為默認值 1 是 int 值。
有些時候需要傳入多個值,可以理解為一個 list,option 只支持固定長度的參數值,即設置后必須傳入,個數由 nargs
確定。
@click.command() @click.option('--pos', nargs=2, type=float) def findme(pos): click.echo('%s / %s' % pos)
findme --pos 2.0 3.0
輸出結果就是 2.0 / 3.0
既然可以傳入 list,那么 tuple 呢?Click 也是支持的:
@click.command() @click.option('--item', type=(unicode, int)) def putitem(item): click.echo('name=%s id=%d' % item)
這樣就傳入了一個 tuple 變量,putitem --item peter 1338
得到的輸出就是 name=peter id=1338
上面沒有設置 nargs,因為 nargs 會自動取 tuple 的長度值。因此上面的代碼實際上等同於:
@click.command() @click.option('--item', nargs=2, type=click.Tuple([unicode, int])) def putitem(item): click.echo('name=%s id=%d' % item)
option 還支持同一個參數多次使用,類似 git commit -m aa -m bb
中 -m
參數就傳入了 2 次。option 通過 multiple
標識位來支持這一特性:
@click.command() @click.option('--message', '-m', multiple=True) def commit(message): click.echo('\n'.join(message))
有時候,命令行參數是固定的幾個值,這時就可以用到 Click.choice
類型來限定傳參的潛在值:
# choice @click.command() @click.option('--hash-type', type=click.Choice(['md5', 'sha1'])) def digest(hash_type): click.echo(hash_type)
當上面的命令行程序參數 --hash-type
不是 md5 或 sha1,就會輸出錯誤提示,並且在 --help
提示中也會對 choice 選項有顯示。
如果希望命令行程序能在我們錯誤輸入或漏掉輸入的情況下,友好的提示用戶,就需要用到 Click 的 prompt 功能,看代碼:
# prompt @click.command() @click.option('--name', prompt=True) def hello(name): click.echo('Hello %s!' % name)
如果在執行 hello 時沒有提供 –name 參數,控制台會提示用戶輸入該參數。也可以自定義控制台的提示輸出,把 prompt
改為自定義內容即可。
對於類似賬戶密碼等參數的輸入,就要進行隱藏顯示。option
的 hide_input
和 confirmation_promt
標識就是用來控制密碼參數的輸入:
# password @click.command() @click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True) def encrypt(password): click.echo('Encrypting password to %s' % password.encode('rot13'))
Click 把上面的操作進一步封裝成裝飾器 click.password_option()
,因此上面的代碼也可以簡化成:
# password @click.command() @click.password_option() def encrypt(password): click.echo('Encrypting password to %s' % password.encode('rot13'))
有的參數會改變命令行程序的執行,比如 node
是進入 Node 控制台,而 node --verion
是輸出 node 的版本號。Click 提供 eager 標識對參數名進行標記,攔截既定的命令行執行流程,而是調用一個回調方法,執行后直接退出。下面模擬 click.version_option()
的功能,實現 --version
參數名輸出版本號:
# eager def print_version(ctx, param, value): if not value or ctx.resilient_parsing: return click.echo('Version 1.0') ctx.exit() @click.command() @click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True) def hello(): click.echo('Hello World!')
對於類似刪除數據庫表這樣的危險操作,Click 支持彈出確認提示,--yes
標識位置為 True 時會讓用戶再次確認:
# yes parameters def abort_if_false(ctx, param, value): if not value: ctx.abort() @click.command() @click.option('--yes', is_flag=True, callback=abort_if_false, expose_value=False, prompt='Are you sure you want to drop the db?') def dropdb(): click.echo('Dropped all tables!')
測試運行下:
$ dropdb Are you sure you want to drop the db? [y/N]: n Aborted! $ dropdb --yes Dropped all tables!
同樣的,Click 對次進行了封裝,click.confirmation_option()
裝飾器實現了上述功能:
@click.command() @click.confirmation_option(prompt='Are you sure you want to drop the db?') def dropdb(): click.echo('Dropped all tables!')
前面只講了默認的參數前綴 --
和 -
,Click 允許開發者自定義參數前綴(雖然嚴重不推薦)。
# other prefix @click.command() @click.option('+w/-w') def chmod(w): click.echo('writable=%s' % w) if __name__ == '__main__': chmod()
如果想要用 /
作為前綴,而且要像上面一樣采用布爾標識,會產生沖突,因為布爾標識也是用 /
,這種情況下可以用 ;
代替布爾標識的 /
:
@click.command() @click.option('/debug;/no-debug') def log(debug): click.echo('debug=%s' % debug) if __name__ == '__main__': log()
既然支持 Choice,不難聯想到 Range,先看代碼:
# range @click.command() @click.option('--count', type=click.IntRange(0, 20, clamp=True)) @click.option('--digit', type=click.IntRange(0, 10)) def repeat(count, digit): click.echo(str(digit) * count) if __name__ == '__main__': repeat()
Argument 參數
Argument 的作用類似 Option,但沒有 Option 那么全面的功能。
和 Option 一樣,Argument 最基礎的應用就是傳遞一個簡單變量值:
@click.command() @click.argument('filename') def touch(filename): click.echo(filename)
命令行后跟的參數值被賦值給參數名 filename
。
另一個用的比較廣泛的是可變參數,也是由 nargs
來確定參數個數,變量值會以 tuple 的形式傳入函數:
@click.command() @click.argument('src', nargs=-1) @click.argument('dst', nargs=1) def copy(src, dst): for fn in src: click.echo('move %s to folder %s' % (fn, dst))
運行程序:
$ copy foo.txt bar.txt my_folder
move foo.txt to folder my_folder
move bar.txt to folder my_folder
Click 支持通過文件名參數對文件進行操作,click.File()
裝飾器就是處理這種操作的,尤其是在類 Unix 系統下,它支持以 -
符號作為標准輸入/輸出。
# File @click.command() @click.argument('input', type=click.File('rb')) @click.argument('output', type=click.File('wb')) def inout(input, output): while True: chunk = input.read(1024) if not chunk: break output.write(chunk)
運行程序,先將文本寫進文件,再讀取
如果參數值只是想做為文件名而已呢,很簡單,將 type
指定為 click.Path()
:
@click.command() @click.argument('f', type=click.Path(exists=True)) def touch(f): click.echo(click.format_filename(f))
命令組
$ touch hello.txt hello.txt $ touch missing.txt Usage: touch [OPTIONS] F Error: Invalid value for "f": Path "missing.txt" does not exist.
仿 Hexo 示例程序
# hexo.py