python命令行操作:Click包

0 前言

在Python開發和測試過程中主要有兩種模式可以選擇:腳本模式、命令行模式。在代碼的開發和調試過程中使用腳本模式還是很方便的,尤其接觸pycharm、eclipse這類強大的IDE,或者配合vs code這種綜合的文本編輯器。但是如果使用純文本編輯器,或者是在做整個系統開發時,與其他模型進行接口調用時,命令行模式就顯得非常重要了。目前有很多熱門的命令行工具,本文會一一介紹一下,並且重點介紹一下最近發現的一款命令行神器Click。
這款包配合setuptools的end_points參數可以,在命令行直接執行自定義的命令,而不需要使用python 腳本.py 參數這樣的格式。

1 Click簡介

Click 是 Flask 的團隊 pallets 開發的優秀開源項目,它爲命令行工具的開發封裝了大量方法,使開發者只需要專注於功能實現。恰好我最近在開發的一個小工具需要在命令行環境下操作,就寫個學習筆記。

2 Hello World

國際慣例,先來一段 “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()

執行結果爲:
在這裏插入圖片描述
除此之外,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.

3 函數變CLI

3.1 command

從上面的 “Hello World” 演示中可以看出,Click 是通過裝飾器來把一個函數方法裝飾成命令行接口的,這個裝飾器方法就是 @click.command()。

import click

@click.command()
def hello():
    click.echo('Hello World!')

@click.command() 裝飾器把 hello() 方法變成了 Command 對象,當它被調用時,就會執行該實例內的行爲。而 --help 參數就是 Command 對象內置的參數。

3.2 group

不同的 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)
cli()

執行結果爲:
在這裏插入圖片描述
@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')
cli()

執行結果爲:
在這裏插入圖片描述

4 命令行參數介紹

命令行的參數功能是不可或缺的,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.option() 和 @click.argument(),兩者有些許區別,使用場景也有所不同。
總體而言,argument() 裝飾器比 option() 功能簡單些。
option支持下面的特性:

  • 自動提示缺失的輸入;
  • option 參數可以從環境變量中獲取,argument 參數則不行;
  • option 參數在 help 輸出中有完整的文檔,argument 則沒有;

而 argument 參數可以接受可變個數的參數值,而 option 參數只能接收固定個數的參數值(默認是 1 個)。
Click 可以設置不同的參數類型,簡單類型如 click.STRING,click.INT,click.FLOAT,click.BOOL。
命令行的參數名由 “-short_name” 和 “–long_name” 聲明,如果參數名既沒有以 “-” 開頭,也沒有以 “–” 開頭,那麼這邊變量名會成爲被裝飾方法的內部變量,而非方法參數。

4.1 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 選項有顯示。
既然支持 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()

如果希望命令行程序能在我們錯誤輸入或漏掉輸入的情況下,友好的提示用戶,就需要用到 Click 的 prompt 功能,看代碼:

# prompt
@click.command()
@click.option('--name', prompt=True)
def hello(name):
    click.echo('Hello %s!' % name)

如果在執行 hello 時沒有提供 –name 參數,控制檯會提示用戶輸入該參數。也可以自定義控制檯的提示輸出,把 prompt 改爲自定義內容即可。如第一部分的hello world中的prompt設置爲Your name。
對於類似賬戶密碼等參數的輸入,就要進行隱藏顯示。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!')

if __name__ == '__main__':
    hello()

運行結果:
在這裏插入圖片描述
對於類似刪除數據庫表這樣的危險操作,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()

4.2 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) #-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
import click
@click.command()
@click.argument('input', type=click.File('rb'))
@click.argument('output', type=click.File('wb'))
def inout(input, output):

    chunk = input.readline()

    output.write(chunk)

if __name__ == '__main__':
    inout()

運行程序,將文本寫進文件
在這裏插入圖片描述
在這裏插入圖片描述
經過試驗option好像不支持這種文件對象參數。

如果參數值只是想做爲文件名而已呢,很簡單,將 type 指定爲 click.Path():

@click.command()
@click.argument('f', type=click.Path(exists=True))
def touch(f):
    click.echo(click.format_filename(f))

5 打包跨平臺可執行程序

通過 Click 編寫了簡單的命令行方法後,還需要把 .py 文件轉換成可以在控制檯裏運行的命令行程序。最簡單的辦法就是在文件末尾加上如下代碼:

if __name__ == '__main__':
    command_name()

Click 支持使用 setuptools 來更好的實現命令行程序打包,把源碼文件打包成系統中的可執行程序,並且不限平臺。一般我們會在源碼根目錄下創建 setup.py 腳本,先看一段簡單的打包代碼:
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 命令的導入路徑。
我們使用上面的打包程序,對下面的代碼進行打包:

# hello.py
import click

@click.group()
def cli():
    pass

@cli.command()
def initdb():
    click.echo('Initialized the database')

@cli.command()
def dropdb():
    click.echo('Dropped the database')
cli()

打包命令:
在這裏插入圖片描述
然後進行安裝和運行:
在這裏插入圖片描述
有關python打包和運行的詳細內容看考博客:python程序的打包和安裝

原文鏈接:Python Click 學習筆記

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章