import subprocess
import sys
from pathlib import Path
TEMPLATE_VERSION = 1
FORBIDDEN = (set(range(0x00, 0x20)) - {0x09, 0x0A}) | {0x7F}
NAMES = {0x07: "BEL", 0x08: "BACKSPACE", 0x09: "TAB", 0x0D: "CARRIAGE RETURN",
0x1B: "ESC", 0x7F: "DEL"}
def tracked_files(root: Path) -> list[str]:
out = subprocess.run(
["git", "-C", str(root), "ls-files", "-z"], capture_output=True, check=True
).stdout
return [p.decode() for p in out.split(b"\0") if p]
def is_binary(data: bytes) -> bool:
return b"\0" in data[:8192]
def scan(data: bytes) -> list[tuple[int, int, bytes]]:
hits = []
for lineno, line in enumerate(data.split(b"\n"), 1):
for b in sorted({c for c in line if c in FORBIDDEN}):
hits.append((lineno, b, line))
return hits
def describe(byte: int) -> str:
return f"0x{byte:02x} ({NAMES[byte]})" if byte in NAMES else f"0x{byte:02x}"
def check_tree(root: Path) -> list[str]:
problems = []
for rel in tracked_files(root):
path = root / rel
try:
data = path.read_bytes()
except (FileNotFoundError, IsADirectoryError):
continue if is_binary(data):
continue
for lineno, byte, line in scan(data):
excerpt = line.decode("utf-8", "replace")[:70]
problems.append(f"{rel}:{lineno}: {describe(byte)} in: {excerpt!r}")
return problems
def _self_test() -> int:
failures: list[str] = []
def check(name: str, cond: bool, detail: str = "") -> None:
if not cond:
failures.append(f"{name}: {detail}")
check("plain text passes", scan(b"hello\tworld\nsecond line\n") == [],
"clean text was flagged")
bs = chr(92)
check("backspace caught", any(b == 0x08 for _, b, _ in scan(b"Git's `usr\x08in` on PATH")),
"the justfile-common.just defect passed")
check("BEL caught", any(b == 0x07 for _, b, _ in scan(b"`\\?\x07cpi#pnp0c0a#0#`")),
"the win_setupapi.rs rustdoc defect passed")
check("repaired text passes", scan(f"Git's `usr{bs}bin` on PATH".encode()) == [],
"the CORRECT text was flagged -- the guard would block its own fix")
check("CR caught", any(b == 0x0D for _, b, _ in scan(b"line one\r\nline two\r\n")),
"a CRLF file passed")
check("LF passes", scan(b"line one\nline two\n") == [], "an LF file was flagged")
no_cr = b"a\nb\nc\nd\n"
check("CR count is not the line count", len([1 for _, b, _ in scan(no_cr) if b == 0x0D]) == 0,
"counted lines instead of carriage returns")
hits = scan(b"clean\nalso clean\nbad\x1bhere\n")
check("line number reported", hits and hits[0][0] == 3, f"got {hits}")
check("binary sniffed", is_binary(b"\x89PNG\r\n\x1a\n\0\0\0"), "a PNG was treated as text")
check("text not sniffed as binary", not is_binary(b"ordinary text\n"), "text called binary")
live = check_tree(Path(__file__).resolve().parent.parent)
check("live tree clean", not live, "; ".join(live))
if failures:
for f in failures:
print(f" FAIL {f}", file=sys.stderr)
print(f"text_check.py self-test FAILED ({len(failures)})", file=sys.stderr)
return 1
print(f"text_check.py self-test passed (template v{TEMPLATE_VERSION})")
return 0
def main(argv: list[str]) -> int:
if "--self-test" in argv:
return _self_test()
root = Path(__file__).resolve().parent.parent
problems = check_tree(root)
if problems:
for p in problems:
print(f" {p}", file=sys.stderr)
print(
f"\ntext_check: {len(problems)} forbidden byte(s) in tracked text.\n"
" A control byte is usually a backslash that collapsed in transport "
"(~/AGENTS.md sec.14).\n"
" A CARRIAGE RETURN is worktree drift that git normalises out of its own view: "
"`git diff` shows\n"
" nothing, and one `git add` makes `git status` clean while every CR stays on "
"disk. Confirm with\n"
" `git ls-files --eol <file>` (look for `i/lf w/crlf`) and repair the worktree "
"copy in place.",
file=sys.stderr,
)
return 1
print("text: no control characters or carriage returns in tracked files")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))