butler/tools/command_canonicalize.py
Rama main · réplica de lectura del repositorio original.
"""Canonicalize shell commands for stable approval fingerprints (Codex subset)."""
from __future__ import annotations
import re
import shlex
from typing import Sequence
_BASH_NAMES = frozenset({"bash", "sh", "zsh", "/bin/bash", "/bin/sh", "/usr/bin/bash"})
_LC_FLAGS = frozenset({"-lc", "-c", "/c"})
def _split_command(command: str) -> list[str]:
text = (command or "").strip()
if not text:
return []
try:
return shlex.split(text, posix=True)
except ValueError:
return text.split()
def canonicalize_command_for_approval(command: str) -> str:
"""
Normalize wrapper paths so `/bin/bash -lc 'git status'` and `bash -lc git status`
share the same approval fingerprint string.
"""
argv = _split_command(command)
if not argv:
return ""
prog = argv[0].lower()
base = prog.rsplit("/", 1)[-1]
if base in _BASH_NAMES and len(argv) >= 2:
flag = argv[1]
if flag in _LC_FLAGS and len(argv) >= 3:
script = " ".join(argv[2:])
mode = flag
return f"__butler_shell__:{mode}:{_normalize_script(script)}"
if flag in _LC_FLAGS and len(argv) == 2:
return f"__butler_shell__:{flag}:"
return " ".join(_normalize_token(t) for t in argv)
def _normalize_token(token: str) -> str:
t = token.strip()
if t.startswith("/") and "/" in t[1:]:
return t.rsplit("/", 1)[-1]
return t
def _normalize_script(script: str) -> str:
body = script.strip()
if (body.startswith("'") and body.endswith("'")) or (
body.startswith('"') and body.endswith('"')
):
body = body[1:-1].strip()
body = re.sub(r"\s+", " ", body)
return body
def canonical_argv_list(argv: Sequence[str]) -> str:
if not argv:
return ""
return canonicalize_command_for_approval(" ".join(shlex.quote(str(a)) for a in argv))
__all__ = ["canonical_argv_list", "canonicalize_command_for_approval"]