複雜的Typer CLI 範例

展示一個更複雜的 Typer CLI 範例,結合多個指令、參數、選項、Enum 型別、巢狀子指令,讓你看到 Typer 在大型工具設計上的彈性。


🧩 範例:檔案管理 CLI

import typer
from pathlib import Path
from enum import Enum
app = typer.Typer(help="檔案管理工具")
class Mode(str, Enum):
    copy = "copy"
    move = "move"
    delete = "delete"
@app.command()
def process(
    input_file: Path = typer.Argument(..., help="輸入檔案路徑"),
    output_file: Path = typer.Argument(None, help="輸出檔案路徑 (copy/move 模式需要)"),
    mode: Mode = typer.Option(Mode.copy, "--mode", "-m", help="處理模式"),
    overwrite: bool = typer.Option(False, "--overwrite", "-o", help="是否覆蓋輸出檔案"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="顯示詳細訊息")
):
    """
    處理檔案:支援 copy/move/delete。
    """
    if verbose:
        typer.echo(f"Mode={mode}, Input={input_file}, Output={output_file}, Overwrite={overwrite}")
    if not input_file.exists():
        typer.echo("❌ Input file does not exist.")
        raise typer.Exit(code=1)
    if mode in [Mode.copy, Mode.move]:
        if output_file is None:
            typer.echo("❌ Output file required for copy/move.")
            raise typer.Exit(code=1)
        if output_file.exists() and not overwrite:
            typer.echo("❌ Output file already exists. Use --overwrite.")
            raise typer.Exit(code=1)
    if mode == Mode.copy:
        typer.echo(f"📂 Copying {input_file}{output_file}")
    elif mode == Mode.move:
        typer.echo(f"📂 Moving {input_file}{output_file}")
    elif mode == Mode.delete:
        typer.echo(f"🗑️ Deleting {input_file}")
# 子指令群組
user_app = typer.Typer(help="使用者管理")
app.add_typer(user_app, name="user")
@user_app.command()
def add(
    username: str = typer.Argument(..., help="使用者名稱"),
    admin: bool = typer.Option(False, "--admin", help="是否為管理員")
):
    typer.echo(f"✅ Added user {username}, admin={admin}")
@user_app.command()
def remove(
    username: str = typer.Argument(..., help="要刪除的使用者名稱")
):
    typer.echo(f"🗑️ Removed user {username}")
if __name__ == "__main__":
    app()

⚡ 執行方式

# 檔案處理
python main.py process input.txt output.txt --mode copy --verbose
python main.py process input.txt output.txt --mode move --overwrite
python main.py process input.txt --mode delete
# 使用者管理
python main.py user add Alice --admin
python main.py user remove Bob

🌟 特點

  • Enum 型別Mode 限制選項只能是 copy/move/delete
  • 巢狀子指令user add/remove 作為子指令群組。
  • 選項與參數混合:必填位置參數 + 可選旗標。
  • 自動產生文件python main.py --help 會顯示完整結構。

這樣的結構已經接近「專案級 CLI 工具」的設計。


📂 專案結構建議

更完整的 Typer 專案架構範例,我來展示一個「模組化 CLI 專案」的設計方式,適合大型自動化工具或團隊協作。

project/
│── main.py          # 入口點,註冊 Typer app
│── commands/
│   │── __init__.py  # 匯入子指令
│   │── files.py     # 檔案處理相關指令
│   │── users.py     # 使用者管理相關指令
│── utils/
│   │── helpers.py   # 共用工具函式
│── tests/
│   │── test_cli.py  # CLI 測試

📝 main.py

import typer
from commands import files, users
app = typer.Typer(help="專案級 CLI 工具")
# 加入子指令群組
app.add_typer(files.app, name="files")
app.add_typer(users.app, name="users")
if __name__ == "__main__":
    app()

📦 commands/files.py

import typer
from pathlib import Path
app = typer.Typer(help="檔案相關操作")
@app.command()
def copy(
    src: Path = typer.Argument(..., help="來源檔案"),
    dst: Path = typer.Argument(..., help="目標檔案"),
    overwrite: bool = typer.Option(False, "--overwrite", "-o", help="是否覆蓋")
):
    if dst.exists() and not overwrite:
        typer.echo("❌ 目標檔案已存在,請使用 --overwrite")
        raise typer.Exit(code=1)
    typer.echo(f"📂 Copy {src}{dst}")

👥 commands/users.py

import typer
app = typer.Typer(help="使用者管理")
@app.command()
def add(
    username: str = typer.Argument(..., help="使用者名稱"),
    admin: bool = typer.Option(False, "--admin", help="是否為管理員")
):
    typer.echo(f"✅ 新增使用者 {username}, admin={admin}")
@app.command()
def remove(username: str):
    typer.echo(f"🗑️ 移除使用者 {username}")

🧪 測試範例 (tests/test_cli.py)

from typer.testing import CliRunner
from main import app
runner = CliRunner()
def test_add_user():
    result = runner.invoke(app, ["users", "add", "Alice", "--admin"])
    assert result.exit_code == 0
    assert "新增使用者 Alice" in result.stdout

🌟 優點

  • 模組化:每個子指令獨立檔案,方便維護。
  • 可測試:用 CliRunner 撰寫 pytest 測試。
  • 可擴充:新增新指令只要在 commands/ 裡加檔案,再在 main.py 註冊即可。

這樣的架構非常適合你在團隊裡推廣,因為它結合了 可維護性可測試性

Typer 常見設計模式速查表

太好了 🎉
這裡我幫你整理一份 Typer 常見設計模式速查表,讓你在設計 CLI 工具時可以快速套用。


🗂 Typer 常見設計模式速查表

1️⃣ 必填參數 (Argument)

@app.command()
def greet(name: str = typer.Argument(..., help="使用者名稱")):
    typer.echo(f"Hello {name}")
  • ... 表示必填
  • 自動產生位置參數

2️⃣ 選項 (Option)

@app.command()
def repeat(
    text: str,
    times: int = typer.Option(1, "--times", "-t", help="重複次數")
):
    for _ in range(times):
        typer.echo(text)
  • --times / -t 可選
  • 有預設值

3️⃣ 布林旗標

@app.command()
def build(debug: bool = typer.Option(False, "--debug", "-d", help="顯示除錯訊息")):
    if debug:
        typer.echo("🔍 Debug mode enabled")
  • 布林旗標不需要值,只要加上 --debug 即可

4️⃣ Enum 型別

from enum import Enum
class Mode(str, Enum):
    fast = "fast"
    slow = "slow"
@app.command()
def run(mode: Mode = typer.Option(Mode.fast, "--mode", help="執行模式")):
    typer.echo(f"Running in {mode} mode")
  • 限制選項只能是 Enum 定義的值

5️⃣ 子指令群組

files_app = typer.Typer(help="檔案操作")
app.add_typer(files_app, name="files")
@files_app.command()
def copy(src: str, dst: str):
    typer.echo(f"Copy {src}{dst}")
  • python main.py files copy a.txt b.txt

6️⃣ 環境變數讀取

import os
@app.command()
def config(
    api_key: str = typer.Option(os.getenv("API_KEY"), "--api-key", help="API 金鑰")
):
    typer.echo(f"Using API key: {api_key}")
  • 可用 os.getenv() 讀取環境變數作為預設值

7️⃣ 測試 CLI

from typer.testing import CliRunner
runner = CliRunner()
def test_greet():
    result = runner.invoke(app, ["greet", "Alice"])
    assert result.exit_code == 0
    assert "Hello Alice" in result.stdout
  • 使用 CliRunner 搭配 pytest 測試 CLI

🌟 小結

  • Argument → 必填位置參數
  • Option → 可選參數/旗標
  • Enum → 限制選項值
  • 子指令群組 → 模組化 CLI
  • 環境變數 → 與系統整合
  • 測試 → 保證 CLI 穩定性

流程圖/決策樹在設計 CLI 時能快速判斷「該用 Argument 還是 Option」

Typer 設計模式速查表轉成一個 流程圖 / 決策樹,讓你在設計 CLI 工具時能快速判斷該用 Argument 還是 Option,以及何時用 Enum 或旗標。

🗂 Typer CLI 決策樹

開始設計 CLI 指令
        │
        ├── 這個參數是否必填?
        │       │
        │       ├── 是 → 使用 Argument
        │       │       └── 需要型別限制嗎?
        │       │              ├── 是 → 使用 Enum
        │       │              └── 否 → 一般 Argument
        │       │
        │       └── 否 → 使用 Option
        │               └── 這是布林旗標嗎?
        │                      ├── 是 → Option(bool),只需加上 --flag
        │                      └── 否 → Option(型別, 預設值)
        │
        ├── 是否需要子指令群組?
        │       ├── 是 → 建立 Typer 子 app (app.add_typer)
        │       └── 否 → 保持在主 app
        │
        ├── 是否需要環境變數整合?
        │       ├── 是 → Option(os.getenv("VAR"))
        │       └── 否 → 一般 Option/Argument
        │
        └── 是否需要測試?
                ├── 是 → 使用 CliRunner 撰寫 pytest
                └── 否 → 可直接執行

🌟 使用範例對照

  • 必填參數typer.Argument(...)
  • 選項typer.Option(default, "--flag")
  • 布林旗標typer.Option(False, "--debug")
  • Enum 限制typer.Option(Mode.fast, "--mode")
  • 子指令群組app.add_typer(sub_app, name="sub")
  • 環境變數typer.Option(os.getenv("API_KEY"))

這樣你在設計 CLI 工具時,只要沿著這個決策樹走,就能快速判斷該用哪種 Typer 語法。

Also See

File7FileLast Modified
如何在uv 的開發環境下使用Spyder工具如何在uv 的開發環境下使用Spyder工具1:25 AM - December 07, 2025
Python 開發工具前五名詳細比較Python 開發工具前五名詳細比較1:25 AM - December 07, 2025
python 讀取某一個檔案日期,然後將檔案日期再設定回原本檔案python 讀取某一個檔案日期,然後將檔案日期再設定回原本檔案1:25 AM - December 07, 2025
Python 標準函式庫依照功能分類Python 標準函式庫依照功能分類1:25 AM - December 07, 2025
python 中,移除 list 中的某一個元素python 中,移除 list 中的某一個元素1:25 AM - December 07, 2025
python list 在最前面插入資料python list 在最前面插入資料1:25 AM - December 07, 2025
PyCharm 2025 的試用機制與試用過期後的差異PyCharm 2025 的試用機制與試用過期後的差異1:25 AM - December 07, 2025