Per-occurrence rewrites
Rewrite one segment of a compound command without touching its siblings.
A whole-line rewrite is the wrong tool for a compound command: mapping the entire line to one new command either drops the cd and pytest or rewrites them along for the ride. rewrite_command_occurrences calls to once per parsed command and splices every rewrite back by byte span, so untouched segments, operators, redirects, and comments survive byte-for-byte. This example steers pip install to uv pip install wherever it appears in a line.
"""Rewrite `pip install` to `uv pip install`, one occurrence at a time."""
from __future__ import annotations
import shlex
from captain_hook import (
Allow,
BaseHookEvent,
Input,
Occurrence,
Rewrite,
rewrite_command_occurrences,
)
def pip_to_uv(evt: BaseHookEvent, occ: Occurrence) -> str | None:
cmd = occ.command
if occ.piped or cmd.redirects or cmd.env:
return None # only rewrite what the splice can reproduce in full
if cmd.executable != "pip" or not cmd.args or cmd.args[0] != "install":
return None
return shlex.join(["uv", "pip", *cmd.args])
rewrite_command_occurrences(
to=pip_to_uv,
note=lambda evt, pairs: f"Rewrote {len(pairs)} pip install(s) to uv pip: same resolver, faster installs.",
tests={
Input(command="pip install requests"): Rewrite(pattern="uv pip install requests"),
# Only the pip segment is rewritten; the cd and pytest survive byte-for-byte.
Input(command="cd api && pip install -r requirements.txt && pytest"): Rewrite(
pattern="cd api && uv pip install -r requirements.txt && pytest"
),
Input(command="pip install 'foo>=2'"): Rewrite(pattern="uv pip install 'foo>=2'"),
Input(command="pip download requests"): Allow(),
Input(command="python -m pip install requests"): Allow(),
Input(command="pip install foo | tee install.log"): Allow(),
},
)What it rewrites
pip install requests # → uv pip install requests
cd api && pip install -r requirements.txt && pytest # → cd api && uv pip install -r requirements.txt && pytest
pip install 'foo>=2' # quoting survives: → uv pip install 'foo>=2'What it leaves alone
pip download requests # not an install
python -m pip install requests # not a bare pip executable
pip install foo | tee install.log # piped — the guard declines rather than break the pipeThe rewrite / allow split mirrors the hook’s inline tests, so it stays true as the hook evolves.
Run it yourself
uvx capt-hook --hooks docs/examples test