1. 程式人生 > 程式設計 >Python3 Click模組的使用方法詳解

Python3 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',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',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,value):
  if not value:
    ctx.abort()
@click.command()
@click.option('--yes',callback=abort_if_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',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)

執行程式,先將文字寫進檔案,再讀取

$ inout - hello.txt
hello
^D
$ inout hello.txt -
hello

如果引數值只是想做為檔名而已呢,很簡單,將 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.

更多關於Python3 Click模組的使用方法請檢視下面的相關連結