diff --git a/search_and_replace.py b/search_and_replace.py new file mode 100755 index 0000000..e3596a9 --- /dev/null +++ b/search_and_replace.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 + +import argparse +from pathlib import Path +import sys + + +def find_files(directory: Path, recursive: bool): + if recursive: + yield from (p for p in directory.rglob("*") if p.is_file()) + else: + yield from (p for p in directory.iterdir() if p.is_file()) + +def process_file(file_path: Path, search: str, replace: str): + try: + content = file_path.read_text(encoding="utf-8") + except (UnicodeDecodeError, OSError): + return None + + if search not in content: + return None + + changed_lines = [] + + for lineno, line in enumerate(content.splitlines(), start=1): + if search in line: + changed_lines.append({ + "line": lineno, + "old": line, + "new": line.replace(search, replace), + }) + + return { + "path": file_path, + "count": content.count(search), + "new_content": content.replace(search, replace), + "changed_lines": changed_lines, + } + + +def main(): + parser = argparse.ArgumentParser( + description="Search and replace text in files with confirmation." + ) + parser.add_argument( + "directory", + help="Directory to search" + ) + parser.add_argument( + "search", + help="String to search for" + ) + parser.add_argument( + "replace", + help="Replacement string" + ) + parser.add_argument( + "-r", + "--recursive", + action="store_true", + help="Search recursively" + ) + parser.add_argument( + "-y", + "--yes", + action="store_true", + help="Skip confirmation" + ) + + args = parser.parse_args() + + directory = Path(args.directory) + + if not directory.is_dir(): + print(f"Error: '{directory}' is not a directory.", file=sys.stderr) + sys.exit(1) + + changes = [] + + for file_path in find_files(directory, args.recursive): + result = process_file(file_path, args.search, args.replace) + if result: + changes.append(result) + + if not changes: + print("No matches found.") + return + + print("\nPlanned changes:\n") + total_replacements = 0 + + for change in changes: + print("=" * 80) + print(f"File: {change['path']}") + print(f"Replacements: {change['count']}") + print() + + for line_change in change["changed_lines"]: + print(f"Line {line_change['line']}:") + print(f" - {line_change['old']}") + print(f" + {line_change['new']}") + print() + + total_replacements += change["count"] + + if not args.yes: + try: + response = input("\nApply these changes? [y/N]: ").strip().lower() + except KeyboardInterrupt: + print("Aborted") + sys.exit(1) + + if response.lower() not in ("y", "yes"): + print("Aborted.") + return + + for change in changes: + change["path"].write_text( + change["new_content"], + encoding="utf-8" + ) + + print( + f"Completed. Modified {len(changes)} file(s), " + f"{total_replacements} replacement(s)." + ) + + +if __name__ == "__main__": + main() diff --git a/zshrc b/zshrc index 13dd83c..a769745 100644 --- a/zshrc +++ b/zshrc @@ -263,3 +263,5 @@ cat() { command cat "$file" done } + +alias sr="~/.config/search_and_replace.py"