快轉到主要內容
  1. 教學文章/

Python argparse 實戰:CLI 參數解析、旗標設計與 subcommands 完全攻略

·9 分鐘· loading · loading · ·
Python Argparse Cli Command-Line Automation Developer-Tools
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 42: 本文

featured

一、前言
#

嗨,我是拍拍君 🐍

很多 Python 腳本剛開始都很單純,先寫一支 python job.py 能跑就好。可是只要腳本開始被重複使用,需求很快就會長出來,像 --input--output--verbose--dry-run--limit,甚至後來還會想拆成 fetchreportexport 這些子命令。這時如果你還在用 sys.argv 手工切字串,通常很快就會進入混亂模式。

argparse 是 Python 標準庫提供的 CLI 參數解析工具。它的特色不是花俏,而是穩、夠用、零依賴。你可以用它處理型別轉換、預設值、必填規則、help 訊息、子命令分派,而且幾乎所有 Python 環境都有它。

這篇文章會從最小範例一路帶到比較實戰的設計。讀完你會知道:

  • positional arguments 和 optional arguments 怎麼分
  • typedefaultchoicesnargs 怎麼用
  • store_truecount 這些旗標設計為什麼實用
  • subparsers 怎麼做出像樣的多命令工具
  • help 訊息和 parser 結構怎麼寫比較耐維護

如果你之後想往更有框架感的方向走,也可以延伸讀 Python Click 教學:打造漂亮 CLI 工具Python Typer 入門:用型別註解打造現代 CLI。但先把 argparse 練熟,真的很值得。

二、安裝
#

2.1 argparse 是標準庫
#

先講一個很實際的優點,argparse 不用安裝。只要你有 Python 3,就可以直接用。

python --version
# Python 3.11.9
import argparse

如果你是用 uv 管環境,也一樣不需要額外安裝套件:

uv init demo-argparse
cd demo-argparse
uv run python -c "import argparse; print('ok')"

這讓 argparse 特別適合內部工具、研究流程腳本、部署助手、資料清理工具,因為你不用擔心對方環境少裝某個第三方 CLI framework。

2.2 第一個 parser
#

先看一個最小可用範例:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("name")
args = parser.parse_args()

print(f"Hello, {args.name}!")

執行:

python hello.py 拍拍君
# Hello, 拍拍君!

如果忘了給參數:

python hello.py

你會看到:

usage: hello.py [-h] name
hello.py: error: the following arguments are required: name

這裡已經能看出 argparse 的價值。你不用自己手刻 usage,也不用再寫一堆「如果長度不夠就印錯誤」的樣板。

2.3 兩種核心參數
#

先記住一件事,CLI 的參數通常分成兩類:

  1. positional arguments,靠位置判斷,例如 name
  2. optional arguments,靠旗標判斷,例如 --verbose

這個分類很重要。因為一支工具該不該用 positional,常常影響使用體驗。必要且順序自然的值適合 positional。比較像設定開關、輸出格式、限制數量的,通常適合 optional。

三、基本參數設計
#

3.1 positional arguments 與型別驗證
#

先來做一個最簡單的加法器:

import argparse

parser = argparse.ArgumentParser(description="兩個整數相加")
parser.add_argument("x", type=int, help="第一個整數")
parser.add_argument("y", type=int, help="第二個整數")
args = parser.parse_args()

print(args.x + args.y)

正常執行:

python add.py 3 5
# 8

錯誤輸入:

python add.py 3 apple
usage: add.py [-h] x y
add.py: error: argument y: invalid int value: 'apple'

這裡的 type=int 很值得養成習慣。它不只轉型,也順便把錯誤訊息統一處理掉。CLI 在入口就做驗證,後面會省很多麻煩。

3.2 optional arguments、預設值與合法值
#

接著加一個可選的輸出風格:

import argparse

parser = argparse.ArgumentParser(description="兩個整數相加")
parser.add_argument("x", type=int)
parser.add_argument("y", type=int)
parser.add_argument("--style", default="plain", choices=["plain", "pretty"])
args = parser.parse_args()

result = args.x + args.y
print(f"結果是:{args.x} + {args.y} = {result}" if args.style == "pretty" else result)
python add.py 3 5
python add.py 3 5 --style pretty

defaultchoices 幾乎是 argparse 最常用的兩個設計。前者讓工具在沒給參數時也能合理工作,後者則把規則直接鎖在 parser 裡,不必等執行到後面才發現值不合法。

3.3 短旗標與長旗標
#

高頻參數通常會同時提供短版和長版:

parser.add_argument("-o", "--output", default="result.txt", help="輸出檔名")

這樣使用者可以自由選擇:

python tool.py input.txt -o out.txt
python tool.py input.txt --output out.txt

短旗標適合日常高頻操作,長旗標適合可讀性。拍手君自己的偏好是,只要某個參數很常打,就值得給短旗標。

3.4 布林旗標:store_true
#

很多 CLI 會出現這些開關型參數:--verbose--dry-run--force。這種情況最常見的寫法是 store_true

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("source")
parser.add_argument("target")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()

if args.dry_run:
    print(f"[DRY RUN] would copy {args.source} -> {args.target}")
else:
    print(f"copy {args.source} -> {args.target}")

這種設計很直覺,有給旗標就是 True,沒給就是 False。對會影響副作用的操作,像刪檔、同步、部署,--dry-run 幾乎是必備好習慣。

3.5 countnargs
#

有時候你不只想知道有沒有開 verbose,而是想知道開到幾級。這時候可以用 count

parser.add_argument("-v", "--verbose", action="count", default=0)
python job.py
python job.py -v
python job.py -vv

另外,如果你要一次收多個值,可以用 nargs

parser.add_argument("keywords", nargs="+", help="一個以上關鍵字")
python grep_demo.py error warning critical

常見設定有幾種:

  • nargs="+",至少一個
  • nargs="*",零個以上
  • nargs=2,固定兩個
  • nargs="?",零或一個

這類設計會讓你的 CLI 比單純硬拆 sys.argv[1:] 乾淨非常多。

3.6 參數名稱和 dest
#

如果你定義了這種旗標:

parser.add_argument("--max-items", type=int, default=10)

在程式裡取得的欄位名稱會自動變成:

args.max_items

因為破折號會轉成底線。如果你想自己指定名稱,也可以加 dest

parser.add_argument("--max-items", dest="limit", type=int, default=10)

這樣最後拿到的是 args.limit。這個小細節很常用,特別是當 CLI 命名想維持 Unix 風格,但 Python 端又想保留好讀的變數名稱時。

四、讓 help 訊息真的好用
#

4.1 descriptionhelp 不要省
#

CLI 的第一層 UX 其實就是 help。最基本的兩件事,是替 parser 加 description,替每個 argument 加 help

parser = argparse.ArgumentParser(description="把 chatPTT 對話紀錄整理成報表")
parser.add_argument("input", help="輸入的 JSON 檔案")
parser.add_argument("--format", choices=["md", "html"], default="md", help="輸出格式")
parser.add_argument("--limit", type=int, default=20, help="最多保留幾筆資料")

help 不用寫成小說,但至少要讓使用者知道這個參數是幹嘛的、有哪些值、預設是什麼。未來的你也會感謝現在的你。

4.2 formatter_class
#

如果你想保留 description 內的換行,可以用 RawTextHelpFormatter。如果你想讓預設值直接顯示在 help 中,可以用 ArgumentDefaultsHelpFormatter

import argparse

parser = argparse.ArgumentParser(
    description="""整理聊天資料並輸出報表

常見流程:
1. 先抓資料
2. 再清理資料
3. 最後輸出 markdown
""",
    formatter_class=argparse.RawTextHelpFormatter,
)
parser = argparse.ArgumentParser(
    formatter_class=argparse.ArgumentDefaultsHelpFormatter
)

這些看起來像小功能,但對參數多的工具很有感。help 一旦清楚,工具就會像真的產品,而不是只給原作者自己用的腳本。

4.3 互斥旗標與自訂型別
#

有些參數邏輯上不能同時存在,例如 --quiet--verbose。這時可以用互斥群組:

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("--quiet", action="store_true")
group.add_argument("--verbose", action="store_true")

另外,如果你需要更細的驗證,可以把函式當成 type

from pathlib import Path
import argparse


def existing_file(value: str) -> Path:
    path = Path(value)
    if not path.is_file():
        raise argparse.ArgumentTypeError(f"找不到檔案: {value}")
    return path


parser = argparse.ArgumentParser()
parser.add_argument("input", type=existing_file)

這兩種技巧都很務實,因為它們讓規則留在 parser,而不是散落在程式各處。

五、subcommands 實戰
#

當你的工具只有一件事要做時,單一 parser 很夠用。可是只要功能開始分化,像是 fetchsummarizeexport,最好就改用子命令,讓每個動作有自己的參數和 help。

5.1 完整範例
#

下面是一個簡化版的聊天資料工具:

import argparse
from pathlib import Path


def handle_fetch(args):
    print(f"fetch from {args.source}, limit={args.limit}")


def handle_summarize(args):
    print(f"summarize {args.input} with style={args.style}")


def handle_export(args):
    print(f"export {args.input} -> {args.output} ({args.format})")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="chatptt", description="處理 chatPTT 對話資料的小工具")
    subparsers = parser.add_subparsers(dest="command", required=True)

    fetch_parser = subparsers.add_parser("fetch", help="抓取原始資料")
    fetch_parser.add_argument("source", choices=["slack", "discord", "imessage"])
    fetch_parser.add_argument("--limit", type=int, default=100)
    fetch_parser.set_defaults(func=handle_fetch)

    summarize_parser = subparsers.add_parser("summarize", help="摘要對話內容")
    summarize_parser.add_argument("input", type=Path)
    summarize_parser.add_argument("--style", choices=["brief", "detailed"], default="brief")
    summarize_parser.set_defaults(func=handle_summarize)

    export_parser = subparsers.add_parser("export", help="匯出整理結果")
    export_parser.add_argument("input", type=Path)
    export_parser.add_argument("-o", "--output", type=Path, required=True)
    export_parser.add_argument("--format", choices=["md", "html"], default="md")
    export_parser.set_defaults(func=handle_export)

    return parser


def main() -> None:
    parser = build_parser()
    args = parser.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()

這段最重要的地方有三個。第一,add_subparsers() 會建立子命令系統。第二,每個子命令都能有自己的 arguments。第三,set_defaults(func=...) 可以把 handler 綁到命令上,避免最後變成很長的 if/elif

5.2 使用方式與好處
#

python chatptt.py fetch slack --limit 30
python chatptt.py summarize logs/chat.json --style detailed
python chatptt.py export output/summary.json -o report.md --format md

這種結構對使用者很友善,因為操作語意清楚。對維護者也很友善,因為不同動作的參數不會全部擠在同一層。當工具慢慢變大時,這個差異會非常明顯。

5.3 子命令 help、Path 與拆分結構
#

子命令也可以有自己的說明:

fetch_parser = subparsers.add_parser(
    "fetch",
    help="抓取原始資料",
    description="從指定資料來源抓取原始聊天紀錄",
)

如果你的工具常處理路徑,直接搭配 Path 通常很舒服:

from pathlib import Path
parser.add_argument("input", type=Path)

另外,拍拍君很推薦把 CLI 結構拆成三層:

  1. build_parser() 負責定義 arguments
  2. handle_xxx(args) 負責執行邏輯
  3. main() 負責接線

這樣一來,parser 和商業邏輯不會糊成一坨,測試也更容易寫。

六、進階小技巧與設計原則
#

6.1 BooleanOptionalAction
#

如果某個布林選項需要正反兩種寫法,例如快取預設開啟,但你有時想關掉,可以用 BooleanOptionalAction

import argparse

parser = argparse.ArgumentParser()
parser.add_argument(
    "--cache",
    action=argparse.BooleanOptionalAction,
    default=True,
    help="是否啟用快取",
)

這樣使用者就能寫:

python app.py --cache
python app.py --no-cache

這比只提供 store_true 更完整,尤其在預設值是 True 的情況下特別自然。

6.2 parse_known_args()
#

有些工具本身只是 wrapper,吃掉一部分參數後,把剩下的交給其他程式。這時可以用:

args, remaining = parser.parse_known_args()

這對包裝 pytestuvicorn 或其他 CLI 很方便。不過如果你不是在做 wrapper,拍手君通常不會優先用它,因為它也可能讓 CLI 規則變得比較模糊。

6.3 設計 CLI 的四個小原則
#

拍拍君自己設計 CLI 時,通常會守四個原則:

  • 名稱要直白,少用模糊的 modetype
  • 預設值要合理,讓常見情境少打一點字
  • help 要站在使用者角度寫,不要只寫內部術語
  • 不同動作就拆成 subcommands,不要硬塞成一堆旗標

這些原則很普通,但真的有用。CLI 工具好不好用,往往不是看你用了多炫的框架,而是看參數和 help 設計有沒有清楚。

6.4 什麼時候該升級到 Click 或 Typer?
#

拍拍君的建議很簡單:

  • 小到中型工具,argparse 通常夠用
  • 想用 decorator 和型別註解驅動,去看 Typer
  • 想要更成熟的 CLI framework 和延伸生態,去看 Click

argparse 的優勢從來不是華麗,而是標準庫、零依賴、到處都能跑。很多時候,這反而是最務實的選擇。

結語
#

argparse 很樸素,但它真的能把一支零散的 Python script 變成像樣的工具。只要你熟悉 add_argument()typedefaultchoicesstore_truenargssubparsers 這些核心概念,就已經足夠寫出大部分日常需要的 CLI。

如果你最近正想把一些研究腳本、自動化任務或內部小工具整理得更正式,我很推薦從 argparse 開始。先把參數介面、help 訊息、預設值、子命令結構整理乾淨,工具立刻就會從「能跑」升級成「能用」。之後如果需求真的變大,再升級到 Click 或 Typer 也完全不遲。

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 42: 本文

相關文章

Python subprocess:外部命令執行與管道串接完全指南
·8 分鐘· loading · loading
Python Subprocess Shell Automation Cli
Python click:比 argparse 更優雅的 CLI 框架
·10 分鐘· loading · loading
Python Click Cli 命令列 開發工具
用 Typer 打造專業 CLI 工具:Python 命令列框架教學
·10 分鐘· loading · loading
Python Typer Cli 開發工具
讓你的終端機華麗變身:Rich 套件教學
·2 分鐘· loading · loading
Python Rich Cli
Rust CLI 實戰:用 clap 打造命令列工具(Python Typer 對照版)
·5 分鐘· loading · loading
Rust Cli Clap Typer Python
Python Profiling:cProfile + line_profiler 效能分析完全指南
·8 分鐘· loading · loading
Python Profiling CProfile Line_profiler Performance Optimization