一、前言 #
寫 Python 腳本的時候,你一定碰過這種情境:腳本寫好了,但每次要改參數就得去改程式碼。於是你開始用 argparse 加上命令列參數⋯⋯然後發現自己花在 parser 設定上的時間,比寫邏輯還多 😅
如果你曾經受夠了 argparse 的冗長設定,今天拍拍君要介紹的 click 絕對會讓你眼前一亮。click 用 Python 裝飾器(decorator)的方式定義 CLI,語法直覺、功能強大,而且是 Flask 團隊(Pallets Projects)出品的 — 品質有保障!
💡 之前我們介紹過 Typer,它其實就是建構在 click 之上的!學會 click,你不只多了一個工具,更能理解 Typer 的底層運作。
二、安裝 #
click 已經是很多套件的依賴(包括 Flask),你的環境裡搞不好已經有了:
# 用 pip 安裝
pip install click
# 或用 uv(推薦!)
uv pip install click
# 確認版本
python -c "import click; print(click.__version__)"
目前最新穩定版是 8.1.x,支援 Python 3.7+。
三、Hello Click — 第一個 CLI #
先來看看 click 版的 Hello World:
# hello.py
import click
@click.command()
@click.option("--name", "-n", default="拍拍君", help="你的名字")
@click.option("--count", "-c", default=1, help="打招呼幾次")
def hello(name, count):
"""一個簡單的打招呼程式 🐍"""
for _ in range(count):
click.echo(f"哈囉,{name}!")
if __name__ == "__main__":
hello()
執行看看:
$ python hello.py --name 拍拍醬 --count 3
哈囉,拍拍醬!
哈囉,拍拍醬!
哈囉,拍拍醬!
$ python hello.py --help
Usage: hello.py [OPTIONS]
一個簡單的打招呼程式 🐍
Options:
-n, --name TEXT 你的名字
-c, --count INTEGER 打招呼幾次
--help Show this message and exit.
跟 argparse 比一下:
# argparse 版本 — 感受一下差異
import argparse
parser = argparse.ArgumentParser(description="一個簡單的打招呼程式")
parser.add_argument("--name", "-n", default="拍拍君", help="你的名字")
parser.add_argument("--count", "-c", type=int, default=1, help="打招呼幾次")
args = parser.parse_args()
for _ in range(args.count):
print(f"哈囉,{args.name}!")
click 版本不只更簡潔,型別轉換也是自動的(從 default 推斷),而且 help 文字直接寫在 docstring 裡。
四、參數類型:Option vs Argument #
click 把命令列參數分成兩種:
Option(選項)— 用 -- 開頭
#
@click.command()
@click.option("--verbose", "-v", is_flag=True, help="顯示詳細資訊")
@click.option("--output", "-o", type=click.Path(), help="輸出檔案路徑")
@click.option("--format", "fmt", type=click.Choice(["json", "csv", "table"]),
default="table", help="輸出格式")
def process(verbose, output, fmt):
"""處理資料並輸出"""
if verbose:
click.echo("🔍 詳細模式已開啟")
click.echo(f"輸出格式:{fmt}")
if output:
click.echo(f"輸出到:{output}")
注意幾個重點:
is_flag=True— 布林開關,不需要值type=click.Path()— 檔案路徑型別,可以加exists=True驗證type=click.Choice([...])— 限定選項,輸入錯誤會自動提示- 第二個參數
"fmt"— 當 option 名跟 Python 關鍵字衝突時,用這個當變數名
Argument(引數)— 位置參數 #
@click.command()
@click.argument("src", type=click.Path(exists=True))
@click.argument("dst", type=click.Path())
def copy_file(src, dst):
"""複製檔案從 SRC 到 DST"""
click.echo(f"📋 複製 {src} → {dst}")
$ python copy.py myfile.txt backup.txt
📋 複製 myfile.txt → backup.txt
🔑 Option vs Argument 的選擇原則:必填的用 Argument,可選的用 Option。Argument 不會出現在
--help的 Options 區,但會顯示在 Usage 行。
五、進階 Option 技巧 #
5.1 密碼輸入(隱藏回顯) #
@click.command()
@click.option("--password", prompt=True, hide_input=True,
confirmation_prompt=True, help="設定密碼")
def set_password(password):
"""設定新密碼"""
click.echo(f"✅ 密碼已設定(長度:{len(password)})")
$ python set_pw.py
Password: ****
Repeat for confirmation: ****
✅ 密碼已設定(長度:8)
或者更簡潔地用 @click.password_option():
@click.command()
@click.password_option()
def set_password(password):
click.echo(f"✅ 密碼已設定(長度:{len(password)})")
5.2 環境變數綁定 #
@click.command()
@click.option("--api-key", envvar="PYPY_API_KEY", help="API 金鑰")
@click.option("--debug", envvar="PYPY_DEBUG", is_flag=True, help="除錯模式")
def connect(api_key, debug):
"""連接到拍拍服務"""
if debug:
click.echo(f"🔑 API Key: {api_key[:4]}****")
click.echo("✅ 已連接!")
$ export PYPY_API_KEY="sk-1234567890"
$ python connect.py
✅ 已連接!
5.3 多值選項 #
@click.command()
@click.option("--include", "-i", multiple=True, help="要包含的檔案模式")
def build(include):
"""建置專案"""
for pattern in include:
click.echo(f"📦 包含:{pattern}")
$ python build.py -i "*.py" -i "*.md" -i "*.toml"
📦 包含:*.py
📦 包含:*.md
📦 包含:*.toml
5.4 數值範圍驗證 #
@click.command()
@click.option("--port", type=click.IntRange(1024, 65535), default=8080,
help="伺服器埠號(1024-65535)")
@click.option("--workers", type=click.IntRange(min=1), default=4,
help="工作行程數")
def serve(port, workers):
"""啟動伺服器"""
click.echo(f"🚀 啟動伺服器 port={port}, workers={workers}")
$ python serve.py --port 80
Error: Invalid value for '--port': 80 is not in the range 1024<=x<=65535.
六、子命令群組(Group) #
這是 click 最強大的功能之一!你可以把多個命令組織成群組,就像 git 有 git commit、git push 一樣:
# chatptt.py — 拍拍君的聊天管理工具
import click
@click.group()
@click.version_option(version="1.0.0")
def cli():
"""chatPTT — 拍拍君的聊天管理工具 🐍"""
pass
@cli.command()
@click.argument("room_name")
@click.option("--private", is_flag=True, help="建立私人聊天室")
def create(room_name, private):
"""建立新聊天室"""
room_type = "🔒 私人" if private else "🌐 公開"
click.echo(f"✅ 已建立{room_type}聊天室:{room_name}")
@cli.command()
@click.option("--active", is_flag=True, help="只顯示活躍的聊天室")
def list_rooms(active):
"""列出所有聊天室"""
rooms = ["general", "random", "python-help", "拍拍大廳"]
if active:
rooms = rooms[:2] # 假設只有前兩個活躍
for room in rooms:
click.echo(f" 💬 {room}")
@cli.command()
@click.argument("room_name")
@click.argument("message")
@click.option("--as-user", default="拍拍君", help="以誰的身份發送")
def send(room_name, message, as_user):
"""發送訊息到聊天室"""
click.echo(f"[{as_user}] → #{room_name}: {message}")
if __name__ == "__main__":
cli()
使用方式:
$ python chatptt.py --version
chatPTT, version 1.0.0
$ python chatptt.py --help
Usage: chatptt.py [OPTIONS] COMMAND [ARGS]...
chatPTT — 拍拍君的聊天管理工具 🐍
Options:
--version Show the version and exit.
--help Show this message and exit.
Commands:
create 建立新聊天室
list-rooms 列出所有聊天室
send 發送訊息到聊天室
$ python chatptt.py create 讀書會 --private
✅ 已建立🔒 私人聊天室:讀書會
$ python chatptt.py send general "大家好!"
[拍拍君] → #general: 大家好!
巢狀子命令 #
群組可以巢狀!適合更複雜的 CLI:
@cli.group()
def admin():
"""管理員功能"""
pass
@admin.command()
@click.argument("username")
def ban(username):
"""封鎖使用者"""
click.echo(f"🚫 已封鎖:{username}")
@admin.command()
@click.argument("username")
def unban(username):
"""解除封鎖"""
click.echo(f"✅ 已解封:{username}")
$ python chatptt.py admin ban trollUser123
🚫 已封鎖:trollUser123
七、Context 共享與回呼 #
當你需要在群組和子命令之間共享設定時,click 的 Context 就派上用場了:
@click.group()
@click.option("--config", type=click.Path(exists=True), help="設定檔路徑")
@click.option("--verbose", "-v", is_flag=True, help="詳細輸出")
@click.pass_context
def cli(ctx, config, verbose):
"""我的 CLI 工具"""
# 把設定存在 context 裡
ctx.ensure_object(dict)
ctx.obj["verbose"] = verbose
ctx.obj["config"] = config
@cli.command()
@click.argument("name")
@click.pass_context
def greet(ctx, name):
"""打招呼"""
if ctx.obj["verbose"]:
click.echo(f"(config: {ctx.obj['config']})")
click.echo(f"哈囉,{name}!")
$ python app.py --verbose greet 拍拍醬
(config: None)
哈囉,拍拍醬!
八、輸出美化與互動 #
8.1 彩色輸出 #
@click.command()
def status():
"""顯示系統狀態"""
click.secho("✅ 資料庫:正常", fg="green")
click.secho("⚠️ 快取:使用率 87%", fg="yellow", bold=True)
click.secho("❌ 郵件服務:離線", fg="red")
click.secho("ℹ️ 版本:v2.1.0", fg="blue", underline=True)
8.2 進度條 #
import time
@click.command()
@click.argument("files", nargs=-1, type=click.Path(exists=True))
def upload(files):
"""上傳檔案"""
with click.progressbar(files, label="上傳中") as bar:
for f in bar:
time.sleep(0.5) # 模擬上傳
click.echo("✅ 全部上傳完成!")
$ python upload.py file1.txt file2.txt file3.txt
上傳中 [####################################] 100%
✅ 全部上傳完成!
8.3 互動式確認與提示 #
@click.command()
@click.argument("path", type=click.Path(exists=True))
@click.option("--force", is_flag=True, help="跳過確認")
def delete(path, force):
"""刪除檔案"""
if not force:
click.confirm(f"⚠️ 確定要刪除 {path} 嗎?", abort=True)
click.echo(f"🗑️ 已刪除:{path}")
$ python delete.py important.txt
⚠️ 確定要刪除 important.txt 嗎? [y/N]: n
Aborted!
九、實戰範例:迷你任務管理器 #
把前面學的全部串起來,做一個完整的 CLI 小工具:
# tasks.py — 拍拍君的任務管理器
import json
import click
from pathlib import Path
TASKS_FILE = Path.home() / ".pypy_tasks.json"
def load_tasks():
if TASKS_FILE.exists():
return json.loads(TASKS_FILE.read_text())
return []
def save_tasks(tasks):
TASKS_FILE.write_text(json.dumps(tasks, ensure_ascii=False, indent=2))
@click.group()
@click.version_option(version="0.1.0")
def cli():
"""🐍 拍拍任務管理器 — 用命令列掌控你的 TODO"""
pass
@cli.command()
@click.argument("title")
@click.option("--priority", "-p",
type=click.Choice(["low", "medium", "high"]),
default="medium", help="優先級")
def add(title, priority):
"""新增任務"""
tasks = load_tasks()
task = {
"id": len(tasks) + 1,
"title": title,
"priority": priority,
"done": False,
}
tasks.append(task)
save_tasks(tasks)
emoji = {"low": "🟢", "medium": "🟡", "high": "🔴"}
click.secho(f"✅ 已新增:{emoji[priority]} {title}", fg="green")
@cli.command("list")
@click.option("--all", "show_all", is_flag=True, help="顯示已完成的任務")
@click.option("--priority", "-p",
type=click.Choice(["low", "medium", "high"]),
help="依優先級篩選")
def list_tasks(show_all, priority):
"""列出任務"""
tasks = load_tasks()
if not show_all:
tasks = [t for t in tasks if not t["done"]]
if priority:
tasks = [t for t in tasks if t["priority"] == priority]
if not tasks:
click.echo("📭 沒有任務!去加一些吧~")
return
emoji = {"low": "🟢", "medium": "🟡", "high": "🔴"}
for t in tasks:
status = "✅" if t["done"] else "⬜"
click.echo(f" {status} [{t['id']}] {emoji[t['priority']]} {t['title']}")
@cli.command()
@click.argument("task_id", type=int)
def done(task_id):
"""標記任務完成"""
tasks = load_tasks()
for t in tasks:
if t["id"] == task_id:
t["done"] = True
save_tasks(tasks)
click.secho(f"🎉 完成:{t['title']}", fg="green")
return
click.secho(f"❌ 找不到 ID {task_id} 的任務", fg="red")
@cli.command()
@click.argument("task_id", type=int)
@click.option("--force", is_flag=True, help="跳過確認")
def remove(task_id, force):
"""刪除任務"""
tasks = load_tasks()
task = next((t for t in tasks if t["id"] == task_id), None)
if not task:
click.secho(f"❌ 找不到 ID {task_id} 的任務", fg="red")
return
if not force:
click.confirm(f"確定要刪除「{task['title']}」嗎?", abort=True)
tasks = [t for t in tasks if t["id"] != task_id]
save_tasks(tasks)
click.secho(f"🗑️ 已刪除:{task['title']}", fg="yellow")
if __name__ == "__main__":
cli()
試跑:
$ python tasks.py add "寫 click 教學文" -p high
✅ 已新增:🔴 寫 click 教學文
$ python tasks.py add "買咖啡" -p low
✅ 已新增:🟢 買咖啡
$ python tasks.py list
⬜ [1] 🔴 寫 click 教學文
⬜ [2] 🟢 買咖啡
$ python tasks.py done 1
🎉 完成:寫 click 教學文
$ python tasks.py list
⬜ [2] 🟢 買咖啡
$ python tasks.py list --all
✅ [1] 🔴 寫 click 教學文
⬜ [2] 🟢 買咖啡
十、click vs argparse vs Typer 比較 #
| 特性 | argparse | click | Typer |
|---|---|---|---|
| 內建 | ✅ 標準庫 | ❌ 需安裝 | ❌ 需安裝 |
| 語法風格 | 命令式 | 裝飾器 | 型別提示 |
| 子命令 | 手動設定 | @group |
自動 |
| 自動補全 | ❌ | 外掛支援 | ✅ 內建 |
| 彩色輸出 | ❌ | ✅ secho |
✅(用 Rich) |
| 型別驗證 | 手動 | 半自動 | 全自動 |
| 底層 | — | Werkzeug | click |
| 學習曲線 | 中 | 低 | 最低 |
🤔 怎麼選?
- 不想裝額外套件 →
argparse- 要彈性、要完全控制 →
click- 追求最少程式碼、現代 Python → Typer(底層就是 click)
結語 #
click 是 Python CLI 生態系的中流砥柱 — 它夠成熟、夠穩定、文件完整,而且設計理念「可組合」(composable)讓你的 CLI 工具可以從簡單腳本一路長成複雜應用。
今天我們學了:
- 🎯 用
@click.command()和@click.option()快速定義 CLI - 📋 Option vs Argument 的差異與使用時機
- 🔧 進階功能:密碼、環境變數、多值、範圍驗證
- 🗂️ 子命令群組讓 CLI 結構化
- 🎨 彩色輸出、進度條、互動提示
- 🛠️ 實戰任務管理器完整範例
如果你之前用過 Typer,現在回頭看 click 會有「啊,原來 Typer 底下是這樣做的」的恍然大悟感。兩個都學起來,以後不管什麼場景都能搞定!
Happy clicking! 🖱️