Python 作為一個成熟的腳本語言(scripting langage),已經發展相當多方便地模組、套件供開發者們使用,得益於其方便性與易用性, Python 也十分適合用來撰寫命令列(command line)工具,幫忙解決日常工作或生活上瑣事的自動化。

本文將介紹 Click 模組,讓開發者們能夠更方便地製作命令列工具。

學會 Click 後就能將各種製作指令的瑣事交給它 ,將能夠更專注於主要功能的開發!

本文環境

$ pip install click

Hello, Click

首先看看如何用 Click 製作第 1 個指令 ( hello.py ):

# -*- coding: utf-8 -*-
import click


@click.command()
@click.option('-t', '--to', 'to', help='To who')
def greeting(to):
    '''Say hello to someone'''
    print(f'Hello, {to or "stranger"}!')


if __name__ == '__main__':
    greeting()

上述指令的執行結果:

$ python hello.py
Hello, stranger!

如果加上 --help 就會發現漂亮的命令列說明,連參數說明也有:

$ python hello.py --help
Usage: hello.py [OPTIONS]

  Say hello to someone

Options:
  -t, --to TEXT  To who
  --help         Show this message and exit.

接著加上 -t 參數試試:

$ python hello.py -t Michael
Hello, Michael!

雖然內建的 argparse 模組已提供製作命令列工具會需要的功能,但使用上還是有些許不方便的地方,並不像 Click 可以直接用裝飾子(decorator)的方式製作命令列工具,或是像 Click 提供不少 options 可供使用,總的來說, Click 方便性無話可說,一定是大拇指的啦!

Flask 目前也已採用 Click ,作為其製作命令(command)的功能 (詳見 文件 ) 需要使用的模組,因此在穩定性方面應有一定程度的保證。

必要參數

第 1 個範例可以清楚看到透過 @click.option 能夠添加命令列的參數,然而 option 預設並不是必要的參數,如要改為必要的參數,可以在 @click.option 代入 required=True :

# -*- coding: utf-8 -*-
import click


@click.command()
@click.option('-t', '--to', 'to', help='To who', required=True)
def greeting(to):
    '''Say hello to someone'''
    print(f'Hello, {to or "stranger"}!')


if __name__ == '__main__':
    greeting()

如此一來,指令的說明也會出現 [required] 的備註,如果執行時未指定必要參數,也會提示必要參數必須指定:

$ python hello.py --help
Usage: hello.py [OPTIONS]

  Say hello to someone

Options:
  -t, --to TEXT  To who  [required]
  --help         Show this message and exit.
$
$ python hello.py
Usage: hello.py [OPTIONS]
Try "hello.py --help" for help.

Error: Missing option "-t" / "--to".

參數預設值

在上述範例中用 f-string 在未指定 -t / --to 參數時用 stranger 作為預設值:

{to or "stranger"}

此時同樣可以透過 Click 設定預設值,只要代入 default=<預設值>show_default=True (將預設值顯示在 help 說明中)即可:

# -*- coding: utf-8 -*-
import click


@click.command()
@click.option('-t', '--to', 'to', help='To who', default='stranger', show_default=True)
def greeting(to):
    '''Say hello to someone'''
    print(f'Hello, {to}')


if __name__ == '__main__':
    greeting()

執行 --help 時就會發現說明中特別標示預設值是什麼:

$ python hello.py --help
Usage: hello.py [OPTIONS]

  Say hello to someone

Options:
  -t, --to TEXT  To who  [default: stranger]
  --help         Show this message and exit.

驗證參數

驗證參數是命令列指令開發不可或缺的一環,例如檢查參數是否為字串、整數、日期等,目前 Click 提供多種型別的檢查:

  • str
  • int
  • float
  • bool
  • click.uuid
  • click.File
  • click.Path
  • click.Choice
  • click.IntRange
  • click.FloatRange
  • click.DateTime

各種型別的說明詳見 Parameter Types

以下示範如何使用這些型別,只要額外代入 type=<type> 參數即可:

# -*- coding: utf-8 -*-
import click


@click.command()
@click.option('-t', '--to', 'to', help='To who', default='stranger', show_default=True)
@click.option('-r', '--repeat', 'repeat', help='Repeat n times', type=int, required=True)
def greeting(to, repeat):
    '''Say hello to someone'''
    for __ in range(0, repeat):
        print(f'Hello, {to}')


if __name__ == '__main__':
    greeting()

執行 --help 時,指令說明也同時顯示接受何種型別的參數:

python hello2.py --help
Usage: hello2.py [OPTIONS]

  Say hello to someone

Options:
  -t, --to TEXT         To who  [default: stranger]
  -r, --repeat INTEGER  Repeat n times  [required]
  --help                Show this message and exit.

如果故意使用錯誤型別,也會無法通過檢查:

python hello.py -r abc
Usage: hello.py [OPTIONS]
Try "hello.py --help" for help.

Error: Invalid value for "-r" / "--repeat": abc is not a valid integer

客製型別(Custom Types)

Click 也能夠透過繼承 click.ParamType 類別達到客製化型別的目的,只要實作 convert 方法即可,例如以下實作奇數型別,讓 --repeat 參數只接受奇數:

# -*- coding: utf-8 -*-
import click


class OddIntType(click.ParamType):
    name = 'odd_int'

    def convert(self, value, param, ctx):
        try:
            n = int(value)
            if n % 2 == 0:
               raise ValueError
            return n
        except ValueError:
            self.fail('%s is not a valid odd integer' % value, param, ctx)


ODD_INT = OddIntType()


@click.command()
@click.option('-t', '--to', 'to', help='To who', default='stranger', show_default=True)
@click.option('-r', '--repeat', 'repeat', help='Repeat n times', type=ODD_INT, required=True)
def greeting(to, repeat):
    '''Say hello to someone'''
    for __ in range(0, repeat):
        print(f'Hello, {to}')


if __name__ == '__main__':
    greeting()

如果 --repeat 故意代入偶數就會無法通過 OddIntType 的檢查:

$ python hello.py -r 2
Usage: hello.py [OPTIONS]
Try "hello.py --help" for help.

Error: Invalid value for "-r" / "--repeat": 2 is not a valid odd integer

多值參數(Multi Value Options)

有時候一個參數可能需要多個值,例如計算總和就是這種情境。 Click 模組也提供此種多值參數的用法。

需要注意的是 Click 目前提供 @click.option@click.argument 2 種製作參數的方式,然而 @click.option 所能接受的多值參數是有限個數的,例如:

# -*- coding: utf-8 -*-
import click


@click.command()
@click.option('-s', '--sum', 'numbers', nargs=2, required=True, type=float)
def do_sum(numbers):
    print(f'sum: {sum(numbers)}')


if __name__ == '__main__':
    do_sum()

上述範例利用 nargs=2 指定 --sum 必須 2 個值,如果少指定一個就會出現錯誤提示:

$ python do_sum.py -s 1
Error: -s option requires 2 arguments

如果超過指定的 2 個參數也同樣會出現錯誤:

python do_sum.py -s 1 2 4
Usage: di_sum.py [OPTIONS]
Try "do_sum.py --help" for help.

Error: Got unexpected extra argument (4)

如果想要擁有不定參數(Variadic Arguments)的用法,就只能使用 @click.argument

# -*- coding: utf-8 -*-
import click


@click.command()
@click.argument('numbers', nargs=-1, required=True, type=float)
def do_sum(numbers):
    print(f'sum: {sum(numbers)}')


if __name__ == '__main__':
    do_sum()

上述指定 nargs=-1 就會讓 numbers 支援不定參數:

$ python do_sum.py 1 2 3 4
sum: 10.0

在此趁機說明 @click.argument 是 positional 形式的參數,會依照參數位置自動對應到不同的參數:

# -*- coding: utf-8 -*-
import click


@click.command()
@click.argument('a', required=True)
@click.argument('b', required=True)
def p(a, b):
    print(f'a: {a}')
    print(f'b: {b}')


if __name__ == '__main__':
    p()

因此上述的範例不需要額外指定 -a , -b ,而是透過參數位置自動對應到 a 與 b ,參數的位置可以用 --help 查看:

$ python pritnab.py --help
Usage: pritnab.py.py [OPTIONS] A B

Options:
  --help  Show this message and exit.

直接執行以下指令,就會發現 1, 2 分別會被自動對應到 a 與 b :

$ python pritnab.py 1 2
a: 1
b: 2

這就是 argument 與 option 不同的地方。

Prompting

有時候命令列指令還需要詢問使用者的相關資訊, Click 也提供相當方便的 prompt 參數供我們使用,同時 prompt 也支援隱藏密碼輸入的功能,例如以下範例同時詢問使用者名稱與密碼:

# -*- coding: utf-8 -*-
import click


@click.command()
@click.option('--name', prompt='Your name please', required=True)
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True)
def register(name, password):
    click.echo('Hello %s!' % name)

執行結果:

$ python register.py
Your name please: Michael
Password:
Repeat for confirmation:
Hello Michael!

如果要隱藏密碼類型的輸入,只需要將代入 prompt=True, hide_input=True@click.option 中即可。

如果需要再次確認,則額外再代入 confirmation_prompt=True 參數至 @click.option 中即可達成目的,相當簡單!

結語

Click 提供許多製作命令列指令時所需要使用的功能,更提供自動化製作指令說明的功能,同時也整合 Setuptools 讓我們能輕鬆地將指令安裝在系統之中,有效增加開發指令的效率,讓開發者更能專注在核心功能的開發。而 Click 文件中也有許多本文未涵蓋的內容,有興趣的話可以前往好好翻閱,應能夠有不少收穫!

Happy Coding!

References

https://click.palletsprojects.com/en/7.x/