mirror of
https://github.com/kevinveenbirkenbach/bulk-string-replacer.git
synced 2025-08-01 18:11:09 +02:00
Implemented string replacement in file in combination with path
This commit is contained in:
parent
5ffb317dbf
commit
de4f9511d5
47
main.py
47
main.py
@ -18,21 +18,19 @@ def replace_content(path, old_string, new_string, preview, verbose):
|
|||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
print_verbose(f"Warning: Unicode decode error in file {path}. Skipping.", verbose)
|
print_verbose(f"Warning: Unicode decode error in file {path}. Skipping.", verbose)
|
||||||
|
|
||||||
|
|
||||||
def print_verbose(message, verbose):
|
def print_verbose(message, verbose):
|
||||||
if verbose:
|
if verbose:
|
||||||
print(message)
|
print(message)
|
||||||
|
|
||||||
|
|
||||||
def process_directory(base_path, old_string, new_string, recursive,
|
def process_directory(base_path, old_string, new_string, recursive,
|
||||||
rename_folders, rename_files, replace_in_content,
|
rename_folders, rename_files, replace_in_content,
|
||||||
preview, verbose, include_hidden, rename_paths):
|
preview, verbose, include_hidden, rename_paths):
|
||||||
"""
|
"""
|
||||||
Traverse directory tree at base_path and perform operations based on flags:
|
Traverse directory tree and perform operations based on flags:
|
||||||
- replace_in_content: replace contents
|
- replace_in_content: replace inside file contents
|
||||||
- rename_files: rename files whose names contain old_string
|
- rename_files: rename files whose names contain old_string
|
||||||
- rename_folders: rename folders whose names contain old_string
|
- rename_folders: rename folders whose names contain old_string
|
||||||
- rename_paths: treat old_string as a relative path and move to new_string
|
- rename_paths: match old_string as a relative path and move matching items to new_string
|
||||||
"""
|
"""
|
||||||
# Full-path move logic
|
# Full-path move logic
|
||||||
if rename_paths:
|
if rename_paths:
|
||||||
@ -52,18 +50,23 @@ def process_directory(base_path, old_string, new_string, recursive,
|
|||||||
os.rename(full_src, full_dst)
|
os.rename(full_src, full_dst)
|
||||||
if not recursive:
|
if not recursive:
|
||||||
break
|
break
|
||||||
return
|
# 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 = []
|
folders_to_rename = []
|
||||||
for root, dirs, files in os.walk(base_path):
|
for root, dirs, files in os.walk(base_path):
|
||||||
if not include_hidden:
|
if not include_hidden:
|
||||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||||
files = [f for f in files if not f.startswith('.')]
|
files = [f for f in files if not f.startswith('.')]
|
||||||
|
|
||||||
|
# Content replacement
|
||||||
if replace_in_content:
|
if replace_in_content:
|
||||||
for f in files:
|
for f in files:
|
||||||
replace_content(os.path.join(root, f), old_string, new_string, preview, verbose)
|
replace_content(os.path.join(root, f), old_string, new_string, preview, verbose)
|
||||||
|
|
||||||
|
# File renaming
|
||||||
if rename_files:
|
if rename_files:
|
||||||
for f in files:
|
for f in files:
|
||||||
if old_string in f:
|
if old_string in f:
|
||||||
@ -73,6 +76,7 @@ def process_directory(base_path, old_string, new_string, recursive,
|
|||||||
if not preview:
|
if not preview:
|
||||||
os.rename(src, dst)
|
os.rename(src, dst)
|
||||||
|
|
||||||
|
# Gather folder renames
|
||||||
if rename_folders:
|
if rename_folders:
|
||||||
for d in dirs:
|
for d in dirs:
|
||||||
if old_string in d:
|
if old_string in d:
|
||||||
@ -83,12 +87,12 @@ def process_directory(base_path, old_string, new_string, recursive,
|
|||||||
if not recursive:
|
if not recursive:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Apply folder renames
|
||||||
for src, dst in folders_to_rename:
|
for src, dst in folders_to_rename:
|
||||||
print_verbose(f"Renaming directory: {src} → {dst}", verbose)
|
print_verbose(f"Renaming directory: {src} → {dst}", verbose)
|
||||||
if not preview:
|
if not preview:
|
||||||
os.rename(src, dst)
|
os.rename(src, dst)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Bulk string replacer with optional full-path moves."
|
description="Bulk string replacer with optional full-path moves."
|
||||||
@ -97,20 +101,30 @@ def main():
|
|||||||
parser.add_argument('old_string', help="String or relative path to replace.")
|
parser.add_argument('old_string', help="String or relative path to replace.")
|
||||||
parser.add_argument('-n', '--new', dest='new_string', default='',
|
parser.add_argument('-n', '--new', dest='new_string', default='',
|
||||||
help="Replacement string or new relative path.")
|
help="Replacement string or new relative path.")
|
||||||
|
parser.add_argument('-r', '--recursive', action='store_true',
|
||||||
parser.add_argument('-r', '--recursive', action='store_true', help="Recurse into subdirectories.")
|
help="Recurse into subdirectories.")
|
||||||
parser.add_argument('-F', '--folders', action='store_true', help="Rename folder names.")
|
parser.add_argument('-F', '--folders', action='store_true',
|
||||||
parser.add_argument('-f', '--files', action='store_true', help="Rename file names.")
|
help="Rename folder names.")
|
||||||
parser.add_argument('-c', '--content', action='store_true', help="Replace inside file contents.")
|
parser.add_argument('-f', '--files', action='store_true',
|
||||||
parser.add_argument('-p', '--preview', action='store_true', help="Preview only; no changes.")
|
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',
|
parser.add_argument('-P', '--path', dest='rename_paths', action='store_true',
|
||||||
help="Match old_string as relative path and move to new_string path.")
|
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('-v', '--verbose', action='store_true',
|
||||||
parser.add_argument('-H', '--hidden', action='store_true', help="Include hidden files and folders.")
|
help="Verbose mode.")
|
||||||
|
parser.add_argument('-H', '--hidden', action='store_true',
|
||||||
|
help="Include hidden files and folders.")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
base_paths = [os.path.expanduser(p) for p in args.paths]
|
|
||||||
|
|
||||||
|
# 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:
|
for base in base_paths:
|
||||||
print_verbose(f"Processing: {base}", args.verbose)
|
print_verbose(f"Processing: {base}", args.verbose)
|
||||||
process_directory(
|
process_directory(
|
||||||
@ -129,4 +143,3 @@ def main():
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
main()
|
|
||||||
|
108
test.py
108
test.py
@ -3,20 +3,19 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
import argparse
|
||||||
|
|
||||||
from main import process_directory, replace_content
|
from main import process_directory, replace_content, main as cli_main
|
||||||
|
|
||||||
class TestBulkStringReplacer(unittest.TestCase):
|
class TestBulkStringReplacer(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Create an isolated temporary directory for each test
|
# create isolated temp dir
|
||||||
self.base = tempfile.mkdtemp()
|
self.base = tempfile.mkdtemp()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
# Clean up when done
|
|
||||||
shutil.rmtree(self.base)
|
shutil.rmtree(self.base)
|
||||||
|
|
||||||
def create_file(self, relpath, content=''):
|
def create_file(self, relpath, content=''):
|
||||||
"""Helper: make a file (and any parent dirs) under self.base."""
|
|
||||||
full = os.path.join(self.base, relpath)
|
full = os.path.join(self.base, relpath)
|
||||||
os.makedirs(os.path.dirname(full), exist_ok=True)
|
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||||
with open(full, 'w', encoding='utf-8') as f:
|
with open(full, 'w', encoding='utf-8') as f:
|
||||||
@ -24,92 +23,91 @@ class TestBulkStringReplacer(unittest.TestCase):
|
|||||||
return full
|
return full
|
||||||
|
|
||||||
def test_replace_content(self):
|
def test_replace_content(self):
|
||||||
f = self.create_file('foo.txt', 'hello old world')
|
f = self.create_file('foo.txt', 'hello OLD world')
|
||||||
replace_content(f, 'old', 'new', preview=False, verbose=False)
|
replace_content(f, 'OLD', 'NEW', preview=False, verbose=False)
|
||||||
with open(f, 'r', encoding='utf-8') as fp:
|
with open(f, 'r', encoding='utf-8') as fp:
|
||||||
self.assertIn('hello new world', fp.read())
|
self.assertIn('hello NEW world', fp.read())
|
||||||
|
|
||||||
def test_rename_file(self):
|
def test_rename_file(self):
|
||||||
f = self.create_file('oldfile.txt', '')
|
f = self.create_file('OLDfile.txt', '')
|
||||||
process_directory(
|
process_directory(
|
||||||
base_path=self.base,
|
base_path=self.base,
|
||||||
old_string='old',
|
old_string='OLD', new_string='NEW',
|
||||||
new_string='new',
|
|
||||||
recursive=False,
|
recursive=False,
|
||||||
rename_folders=False,
|
rename_folders=False, rename_files=True,
|
||||||
rename_files=True,
|
|
||||||
replace_in_content=False,
|
replace_in_content=False,
|
||||||
preview=False,
|
preview=False, verbose=False,
|
||||||
verbose=False,
|
include_hidden=True, rename_paths=False
|
||||||
include_hidden=True,
|
|
||||||
rename_paths=False
|
|
||||||
)
|
)
|
||||||
self.assertTrue(os.path.exists(os.path.join(self.base, 'newfile.txt')))
|
# file moved
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(self.base, 'NEWfile.txt')))
|
||||||
self.assertFalse(os.path.exists(f))
|
self.assertFalse(os.path.exists(f))
|
||||||
|
|
||||||
def test_rename_folder(self):
|
def test_rename_folder(self):
|
||||||
os.makedirs(os.path.join(self.base, 'oldfolder', 'inner'))
|
os.makedirs(os.path.join(self.base, 'OLDfolder', 'x'))
|
||||||
process_directory(
|
process_directory(
|
||||||
base_path=self.base,
|
base_path=self.base,
|
||||||
old_string='old',
|
old_string='OLD', new_string='NEW',
|
||||||
new_string='new',
|
|
||||||
recursive=False,
|
recursive=False,
|
||||||
rename_folders=True,
|
rename_folders=True, rename_files=False,
|
||||||
rename_files=False,
|
|
||||||
replace_in_content=False,
|
replace_in_content=False,
|
||||||
preview=False,
|
preview=False, verbose=False,
|
||||||
verbose=False,
|
include_hidden=True, rename_paths=False
|
||||||
include_hidden=True,
|
|
||||||
rename_paths=False
|
|
||||||
)
|
)
|
||||||
self.assertTrue(os.path.isdir(os.path.join(self.base, 'newfolder')))
|
self.assertTrue(os.path.isdir(os.path.join(self.base, 'NEWfolder')))
|
||||||
self.assertFalse(os.path.isdir(os.path.join(self.base, 'oldfolder')))
|
self.assertFalse(os.path.isdir(os.path.join(self.base, 'OLDfolder')))
|
||||||
|
|
||||||
def test_full_path_move(self):
|
def test_full_path_move(self):
|
||||||
# Create nested path vars/configuration.yml
|
# prepare vars/configuration.yml
|
||||||
cfg = 'vars/configuration.yml'
|
cfg = 'vars/configuration.yml'
|
||||||
full_cfg = self.create_file(cfg, 'x')
|
full = self.create_file(cfg, 'DATA')
|
||||||
# Now move vars/configuration.yml -> config/main.yml
|
# run with -P
|
||||||
process_directory(
|
process_directory(
|
||||||
base_path=self.base,
|
base_path=self.base,
|
||||||
old_string='vars/configuration.yml',
|
old_string='vars/configuration.yml',
|
||||||
new_string='config/main.yml',
|
new_string='config/main.yml',
|
||||||
recursive=True,
|
recursive=True,
|
||||||
rename_folders=False,
|
rename_folders=False, rename_files=False,
|
||||||
rename_files=False,
|
|
||||||
replace_in_content=False,
|
replace_in_content=False,
|
||||||
preview=False,
|
preview=False, verbose=False,
|
||||||
verbose=False,
|
include_hidden=True, rename_paths=True
|
||||||
include_hidden=True,
|
|
||||||
rename_paths=True
|
|
||||||
)
|
)
|
||||||
# Original should be gone
|
# original gone, new exists
|
||||||
self.assertFalse(os.path.exists(full_cfg))
|
self.assertFalse(os.path.exists(full))
|
||||||
# New location should exist
|
|
||||||
self.assertTrue(os.path.exists(os.path.join(self.base, 'config', 'main.yml')))
|
self.assertTrue(os.path.exists(os.path.join(self.base, 'config', 'main.yml')))
|
||||||
|
|
||||||
def test_preview_mode(self):
|
def test_path_and_folders_conflict(self):
|
||||||
# Create file and folder that would match
|
# simulate CLI error when combining --path and --folders
|
||||||
f = self.create_file('oldfile.txt', 'old')
|
parser = argparse.ArgumentParser()
|
||||||
os.makedirs(os.path.join(self.base, 'oldfolder'))
|
# replicate only the conflict check
|
||||||
|
parser.add_argument('-P', dest='rename_paths', action='store_true')
|
||||||
|
parser.add_argument('-F', dest='folders', action='store_true')
|
||||||
|
args = parser.parse_args(['-P', '-F'])
|
||||||
|
# manual conflict
|
||||||
|
with self.assertRaises(SystemExit) as cm:
|
||||||
|
# mimic the parser.error behavior
|
||||||
|
if args.rename_paths and args.folders:
|
||||||
|
parser.error("Cannot use --path and --folders together.")
|
||||||
|
self.assertNotEqual(cm.exception.code, 0)
|
||||||
|
|
||||||
|
def test_preview_does_nothing(self):
|
||||||
|
# create file and folder matching
|
||||||
|
f = self.create_file('OLD.txt', 'OLD')
|
||||||
|
os.makedirs(os.path.join(self.base, 'OLDdir'))
|
||||||
process_directory(
|
process_directory(
|
||||||
base_path=self.base,
|
base_path=self.base,
|
||||||
old_string='old',
|
old_string='OLD', new_string='NEW',
|
||||||
new_string='new',
|
|
||||||
recursive=True,
|
recursive=True,
|
||||||
rename_folders=True,
|
rename_folders=True, rename_files=True,
|
||||||
rename_files=True,
|
|
||||||
replace_in_content=True,
|
replace_in_content=True,
|
||||||
preview=True,
|
preview=True, verbose=False,
|
||||||
verbose=False,
|
include_hidden=True, rename_paths=True
|
||||||
include_hidden=True,
|
|
||||||
rename_paths=True
|
|
||||||
)
|
)
|
||||||
# Nothing changed
|
# nothing changed
|
||||||
self.assertTrue(os.path.exists(f))
|
self.assertTrue(os.path.exists(f))
|
||||||
self.assertTrue(os.path.isdir(os.path.join(self.base, 'oldfolder')))
|
self.assertTrue(os.path.isdir(os.path.join(self.base, 'OLDdir')))
|
||||||
with open(f, 'r', encoding='utf-8') as fp:
|
with open(f, 'r', encoding='utf-8') as fp:
|
||||||
self.assertIn('old', fp.read())
|
self.assertIn('OLD', fp.read())
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user