pathlib 是 Python 3.4+ 引入的現代路徑處理模組,提供物件導向的 API 來處理檔案系統路徑。在 Hook 系統中,幾乎每個檔案都使用 pathlib

為什麼使用 pathlib?

傳統 os.path 方式

 1import os
 2
 3# 組合路徑
 4config_path = os.path.join(project_root, ".claude", "config.json")
 5
 6# 取得父目錄
 7parent = os.path.dirname(file_path)
 8
 9# 取得檔名
10filename = os.path.basename(file_path)
11
12# 檢查存在
13if os.path.exists(config_path):
14    ...

現代 pathlib 方式

 1from pathlib import Path
 2
 3# 組合路徑
 4config_path = project_root / ".claude" / "config.json"
 5
 6# 取得父目錄
 7parent = file_path.parent
 8
 9# 取得檔名
10filename = file_path.name
11
12# 檢查存在
13if config_path.exists():
14    ...

基本操作

建立 Path 物件

 1from pathlib import Path
 2
 3# 從字串建立
 4p = Path("/home/user/project")
 5
 6# 當前目錄
 7cwd = Path.cwd()
 8
 9# 使用者目錄
10home = Path.home()
11
12# 從 __file__ 建立
13current_file = Path(__file__)

路徑組合

使用 / 運算子組合路徑(非常直觀):

1from pathlib import Path
2
3project = Path("/home/user/project")
4config = project / ".claude" / "config.json"
5
6# 等同於
7config = project.joinpath(".claude", "config.json")

取得路徑部分

1p = Path("/home/user/project/file.txt")
2
3p.name        # "file.txt"
4p.stem        # "file"(不含副檔名)
5p.suffix      # ".txt"
6p.parent      # Path("/home/user/project")
7p.parents[0]  # Path("/home/user/project")
8p.parents[1]  # Path("/home/user")
9p.parts       # ('/', 'home', 'user', 'project', 'file.txt')

實際範例:Hook 系統

日誌目錄建立

來自 .claude/lib/hook_logging.py

 1from pathlib import Path
 2
 3def setup_hook_logging(hook_name: str) -> logging.Logger:
 4    # 建立日誌目錄
 5    project_root = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
 6    log_dir = Path(project_root) / ".claude" / "hook-logs" / hook_name
 7
 8    # mkdir 的 parents=True 會建立所有不存在的父目錄
 9    # exist_ok=True 表示如果目錄已存在不會報錯
10    log_dir.mkdir(parents=True, exist_ok=True)
11
12    # 日誌檔案路徑
13    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
14    log_file = log_dir / f"{hook_name}-{timestamp}.log"
15
16    # ...

設定檔案搜尋

來自 .claude/lib/config_loader.py

 1def load_config(config_name: str) -> dict:
 2    config_dir = get_config_dir()
 3
 4    # 優先嘗試不同副檔名
 5    yaml_path = config_dir / f"{config_name}.yaml"
 6    yml_path = config_dir / f"{config_name}.yml"
 7    json_path = config_dir / f"{config_name}.json"
 8
 9    if yaml_path.exists():
10        return _load_yaml_file(yaml_path)
11    elif yml_path.exists():
12        return _load_yaml_file(yml_path)
13    elif json_path.exists():
14        return _load_json_file(json_path)
15
16    raise FileNotFoundError(f"Configuration not found: {config_name}")

檔案操作

讀取檔案

1from pathlib import Path
2
3p = Path("config.json")
4
5# 讀取文字
6content = p.read_text(encoding="utf-8")
7
8# 讀取位元組
9data = p.read_bytes()

寫入檔案

1from pathlib import Path
2
3p = Path("output.txt")
4
5# 寫入文字
6p.write_text("Hello, World!", encoding="utf-8")
7
8# 寫入位元組
9p.write_bytes(b"binary data")

檢查檔案類型

1p = Path("/some/path")
2
3p.exists()      # 是否存在
4p.is_file()     # 是否為檔案
5p.is_dir()      # 是否為目錄
6p.is_symlink()  # 是否為符號連結

目錄操作

建立目錄

1from pathlib import Path
2
3p = Path("new_dir/sub_dir")
4
5# 建立目錄(包含父目錄)
6p.mkdir(parents=True, exist_ok=True)

列出目錄內容

 1from pathlib import Path
 2
 3p = Path(".")
 4
 5# 列出所有項目
 6for item in p.iterdir():
 7    print(item)
 8
 9# 使用 glob 模式
10for py_file in p.glob("*.py"):
11    print(py_file)
12
13# 遞迴搜尋
14for md_file in p.rglob("*.md"):
15    print(md_file)

實際範例:驗證所有 Hook

來自 .claude/lib/hook_validator.py

 1def validate_all_hooks(self, hooks_dir: Optional[str] = None):
 2    if hooks_dir is None:
 3        hooks_dir = str(self.project_root / ".claude" / "hooks")
 4
 5    hooks_dir = self._resolve_path(hooks_dir)
 6
 7    # 找出所有 .py 檔案
 8    results = []
 9    for hook_file in sorted(hooks_dir.glob("*.py")):
10        if hook_file.name.startswith("_"):
11            continue  # 跳過 __init__.py 等
12        results.append(self.validate_hook(str(hook_file)))
13
14    return results

路徑解析

相對路徑與絕對路徑

1from pathlib import Path
2
3p = Path("./relative/path")
4
5# 轉換為絕對路徑
6absolute = p.resolve()
7
8# 相對於某個目錄
9relative = p.relative_to(Path.cwd())

實際範例:解析路徑

1def _resolve_path(self, path: str) -> Path:
2    """解析路徑為絕對路徑"""
3    p = Path(path)
4    if p.is_absolute():
5        return p
6    return self.project_root / p

常用模式

計算腳本所在目錄

1# 在 .claude/hooks/my_hook.py 中
2from pathlib import Path
3
4# 取得 lib 目錄
5lib_path = Path(__file__).parent.parent / "lib"
6# __file__ = .claude/hooks/my_hook.py
7# parent = .claude/hooks/
8# parent.parent = .claude/
9# / "lib" = .claude/lib/

確保檔案副檔名

1def ensure_extension(path: Path, ext: str) -> Path:
2    """確保檔案有指定的副檔名"""
3    if path.suffix != ext:
4        return path.with_suffix(ext)
5    return path
6
7# 使用
8p = Path("config")
9p = ensure_extension(p, ".json")  # Path("config.json")

安全的檔案讀取

1def safe_read_file(path: Path) -> Optional[str]:
2    """安全讀取檔案,不存在時返回 None"""
3    if not path.exists():
4        return None
5    try:
6        return path.read_text(encoding="utf-8")
7    except Exception:
8        return None

思考題

  1. Path("/a/b") / "c"Path("/a/b").joinpath("c") 有什麼區別?
  2. 為什麼 mkdir(parents=True, exist_ok=True) 是常見的組合?
  3. glob("**/*.py")rglob("*.py") 有什麼區別?

實作練習

  1. 寫一個函式,找出目錄中所有超過 1MB 的檔案
  2. 寫一個函式,將所有 .txt 檔案重命名為 .md
  3. 實作一個函式,計算目錄中所有 Python 檔案的總行數

下一章:json - 序列化