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 pipe

The 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

See also