This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-9p/tools/file_edit

133 lines
4.4 KiB
Python

#!/usr/bin/env python3
# description: Replace text in a file; old_string not found is the guard
# Args: <file_path> <old_string> <new_string> [replace_all=true|1|yes]
import sys
import os
import difflib
def format_diff(path, old, new):
old_lines = old.splitlines(keepends=True)
new_lines = new.splitlines(keepends=True)
diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
if not diff:
return None
return f"```diff\n{diff}```"
def find_exact(content, find):
return find if find in content else None
def find_ws_normalized(content, find):
nf = ' '.join(find.split())
find_lines = find.split('\n')
n = len(find_lines)
lines = content.split('\n')
for i in range(len(lines) - n + 1):
block = '\n'.join(lines[i:i+n])
if ' '.join(block.split()) == nf:
return block
return None
def find_indent_flexible(content, find):
def deindent(text):
ls = text.split('\n')
min_indent = float('inf')
for l in ls:
stripped = l.lstrip(' \t')
if stripped:
min_indent = min(min_indent, len(l) - len(stripped))
if min_indent == float('inf'):
return text
return '\n'.join(l[min_indent:] if len(l) > min_indent else l.lstrip(' \t') for l in ls)
nf = deindent(find)
find_lines = find.split('\n')
n = len(find_lines)
lines = content.split('\n')
for i in range(len(lines) - n + 1):
block = '\n'.join(lines[i:i+n])
if deindent(block) == nf:
return block
return None
def find_trimmed_boundary(content, find):
trimmed = find.strip()
if trimmed == find:
return None
if trimmed in content:
return trimmed
find_lines = find.split('\n')
n = len(find_lines)
lines = content.split('\n')
for i in range(len(lines) - n + 1):
block = '\n'.join(lines[i:i+n])
if block.strip() == trimmed:
return block
return None
def robust_replace(content, old_string, new_string, replace_all):
match = (find_exact(content, old_string) or
find_ws_normalized(content, old_string) or
find_indent_flexible(content, old_string) or
find_trimmed_boundary(content, old_string))
if match is None:
return None, "old_string not found in file", False
if replace_all:
return content.replace(match, new_string), None, False
n = content.count(match)
if n > 1:
return None, f"old_string is not unique ({n} occurrences); use replace_all=true or provide more context", True
return content.replace(match, new_string, 1), None, False
if len(sys.argv) < 4:
print("usage: file_edit <file_path> <old_string> <new_string> [replace_all=true|1|yes]")
sys.exit(1)
file_path = sys.argv[1]
old_string = sys.argv[2]
new_string = sys.argv[3]
_ra = sys.argv[4].lower() if len(sys.argv) > 4 else ''
replace_all = _ra.split('=')[-1] in ('true', '1', 'yes')
if not os.path.isabs(file_path):
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: path must be absolute, got: {file_path}")
sys.exit(1)
if old_string == new_string:
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
print("error: old_string and new_string must be different")
sys.exit(1)
if not os.path.exists(file_path):
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: file not found: {file_path}")
sys.exit(1)
try:
with open(file_path, 'r', encoding='utf-8') as f:
original = f.read()
except UnicodeDecodeError:
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: file is not valid UTF-8: {file_path}")
sys.exit(1)
new_content, err, retryable = robust_replace(original, old_string, new_string, replace_all)
if err:
print(f"STATUS=error STATE_CHANGED=false RETRYABLE={'true' if retryable else 'false'}")
print(f"error: {err}")
sys.exit(1)
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
except OSError as e:
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
print(f"error: {e}")
sys.exit(1)
diff = format_diff(file_path, original, new_content)
if diff is None:
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
print("(no changes)")
else:
print("STATUS=done STATE_CHANGED=true")
print(diff)