163 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
import os
import argparse
def replace_content(path, old_string, new_string, preview, verbose):
"""
Replace occurrences of old_string with new_string inside the file at path.
"""
try:
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
if old_string in content:
new_content = content.replace(old_string, new_string)
print_verbose(f"Replacing content in: {path}", verbose)
if not preview:
with open(path, 'w', encoding='utf-8') as fw:
fw.write(new_content)
except UnicodeDecodeError:
print_verbose(f"Warning: Unicode decode error in file {path}. Skipping.", verbose)
def print_verbose(message, verbose):
if verbose:
print(message)
def process_directory(base_path, old_string, new_string, recursive,
rename_folders, rename_files, replace_in_content,
preview, verbose, include_hidden, rename_paths):
"""
Traverse directory tree and perform operations based on flags:
- replace_in_content: replace inside file contents
- rename_files: rename files whose names contain old_string
- rename_folders: rename folders whose names contain old_string
- rename_paths: match old_string as a relative path and move matching items to new_string path.
Additionally, when rename_paths is set, update module paths in Python files by replacing path separators with dots.
"""
# Full-path move logic
if rename_paths:
for root, dirs, files in os.walk(base_path):
if not include_hidden:
dirs[:] = [d for d in dirs if not d.startswith('.')]
files = [f for f in files if not f.startswith('.')]
for name in files + dirs:
full_src = os.path.join(root, name)
rel = os.path.relpath(full_src, base_path)
if old_string in rel:
new_rel = rel.replace(old_string, new_string)
full_dst = os.path.join(base_path, new_rel)
print_verbose(f"Moving {full_src}{full_dst}", verbose)
if not preview:
os.makedirs(os.path.dirname(full_dst), exist_ok=True)
os.rename(full_src, full_dst)
if not recursive:
break
# After moving, replace module paths in Python files
for root, dirs, files in os.walk(base_path):
if not include_hidden:
dirs[:] = [d for d in dirs if not d.startswith('.')]
files = [f for f in files if not f.startswith('.')]
for f in files:
if f.endswith('.py'):
replace_content(
os.path.join(root, f),
old_string.replace('/', os.sep),
new_string.replace('/', '.'),
preview,
verbose
)
if not recursive:
break
# Only return early when only path-mode is active
if not (rename_files or replace_in_content):
return
# Collect folder renames to apply after traversal
folders_to_rename = []
for root, dirs, files in os.walk(base_path):
if not include_hidden:
dirs[:] = [d for d in dirs if not d.startswith('.')]
files = [f for f in files if not f.startswith('.')]
# Content replacement
if replace_in_content:
for f in files:
replace_content(os.path.join(root, f), old_string, new_string, preview, verbose)
# File renaming
if rename_files:
for f in files:
if old_string in f:
src = os.path.join(root, f)
dst = os.path.join(root, f.replace(old_string, new_string))
print_verbose(f"Renaming file: {src}{dst}", verbose)
if not preview:
os.rename(src, dst)
# Gather folder renames
if rename_folders:
for d in dirs:
if old_string in d:
src = os.path.join(root, d)
dst = os.path.join(root, d.replace(old_string, new_string))
folders_to_rename.append((src, dst))
if not recursive:
break
# Apply folder renames
for src, dst in folders_to_rename:
print_verbose(f"Renaming directory: {src}{dst}", verbose)
if not preview:
os.rename(src, dst)
def main():
parser = argparse.ArgumentParser(
description="Bulk string replacer with optional full-path moves."
)
parser.add_argument('paths', nargs='+', help="Base directories to process.")
parser.add_argument('old_string', help="String or relative path to replace.")
parser.add_argument('-n', '--new', dest='new_string', default='',
help="Replacement string or new relative path.")
parser.add_argument('-r', '--recursive', action='store_true',
help="Recurse into subdirectories.")
parser.add_argument('-F', '--folders', action='store_true',
help="Rename folder names.")
parser.add_argument('-f', '--files', action='store_true',
help="Rename file names.")
parser.add_argument('-c', '--content', action='store_true',
help="Replace inside file contents.")
parser.add_argument('-p', '--preview', action='store_true',
help="Preview only; no changes.")
parser.add_argument('-P', '--path', dest='rename_paths', action='store_true',
help="Match old_string as relative path and move to new_string path.")
parser.add_argument('-v', '--verbose', action='store_true',
help="Verbose mode.")
parser.add_argument('-H', '--hidden', action='store_true',
help="Include hidden files and folders.")
args = parser.parse_args()
# Disallow using --path and --folders together
if args.rename_paths and args.folders:
parser.error("Cannot use --path and --folders together.")
base_paths = [os.path.expanduser(p) for p in args.paths]
for base in base_paths:
print_verbose(f"Processing: {base}", args.verbose)
process_directory(
base_path=base,
old_string=args.old_string,
new_string=args.new_string,
recursive=args.recursive,
rename_folders=args.folders,
rename_files=args.files,
replace_in_content=args.content,
preview=args.preview,
verbose=args.verbose,
include_hidden=args.hidden,
rename_paths=args.rename_paths
)
if __name__ == '__main__':
main()