mirror of
https://github.com/kevinveenbirkenbach/bulk-string-replacer.git
synced 2025-09-10 04:09:35 +02:00
Compare commits
17 Commits
5cbf98a926
...
main
Author | SHA1 | Date | |
---|---|---|---|
82784bdcff | |||
40ff300a92 | |||
c54cb81812 | |||
de4f9511d5 | |||
5ffb317dbf | |||
792732b44b | |||
84bc27a81a | |||
e4aa2d8b8a | |||
c5b0074bf1 | |||
a30461bada | |||
daf4538938 | |||
2b3fa423bd | |||
ce96418878 | |||
c7bf51941b | |||
ecdc5ce920 | |||
5136ddf27f | |||
e6b4f73eaa |
7
.github/FUNDING.yml
vendored
Normal file
7
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
github: kevinveenbirkenbach
|
||||
|
||||
patreon: kevinveenbirkenbach
|
||||
|
||||
buy_me_a_coffee: kevinveenbirkenbach
|
||||
|
||||
custom: https://s.veen.world/paypaldonate
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*__pycache__
|
117
README.md
117
README.md
@@ -1,45 +1,98 @@
|
||||
# bulk-string-replacer
|
||||
bulk-string-replacer is a tool designed to traverse directories and perform bulk string replacement in filenames, folder names, and file contents. Whether you want to target hidden items, preview changes before execution, or recursively navigate through folders, this versatile utility has you covered.
|
||||
# Bulk String Replacer CLI (bsr) 🔄
|
||||
|
||||
## Author
|
||||
[](https://github.com/sponsors/kevinveenbirkenbach) [](https://www.patreon.com/c/kevinveenbirkenbach) [](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
||||
|
||||
Kevin Veen-Birkenbach
|
||||
- 📧 Email: [kevin@veen.world](mailto:kevin@veen.world)
|
||||
- 🌍 Website: [https://www.veen.world/](https://www.veen.world/)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0.en.html) [](https://github.com/kevinveenbirkenbach/bulk-string-replacer/stargazers)
|
||||
|
||||
## Link to Original Conversation
|
||||
Bulk String Replacer CLI (bsr) is a powerful Python-based command-line tool that lets you search and replace strings in file names, folder names, and within file contents across multiple directories. Perfect for performing bulk updates quickly and efficiently, bsr supports recursive traversal, hidden files, and a preview mode so you can review changes before they’re applied. 🔧📂
|
||||
|
||||
For more context on how this tool was developed, you can [view the original conversation here](https://chat.openai.com/share/8567c240-3905-4521-b30e-04104015bb9b).
|
||||
---
|
||||
|
||||
## Setup and Usage
|
||||
## 🛠 Features
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/kevinveenbirkenbach/bulk-string-replacer.git
|
||||
```
|
||||
* **Comprehensive Replacement:** Replace strings in folder names, file names, and inside file contents.
|
||||
* **Recursive Processing:** Traverse directories recursively to update all matching files.
|
||||
* **Hidden Files Support:** Option to include hidden files and directories.
|
||||
* **Preview Mode:** Preview changes without modifying any files.
|
||||
* **Verbose Output:** Display detailed logs of the operations performed.
|
||||
* **Full-Path Moves:** Match an `old_string` as a relative path (including `/`) and move matching subtrees to a new location.
|
||||
|
||||
2. Navigate to the cloned directory:
|
||||
```bash
|
||||
cd bulk-string-replacer
|
||||
```
|
||||
---
|
||||
|
||||
3. Run the script with Python:
|
||||
```bash
|
||||
python replace_string.py [path] [old_string] [new_string] [options]
|
||||
```
|
||||
## 📥 Installation
|
||||
|
||||
### Options:
|
||||
Install Bulk String Replacer CLI easily via [Kevin's Package Manager](https://github.com/kevinveenbirkenbach/package-manager) under the alias `bsr`:
|
||||
|
||||
- `--recursive`: Replace in all subdirectories and files.
|
||||
- `--folder`: Replace in folder names.
|
||||
- `--files`: Replace in file names.
|
||||
- `--content`: Replace inside file contents.
|
||||
- `--preview`: Preview changes without making actual replacements.
|
||||
- `--verbose`: Verbose mode - view detailed outputs.
|
||||
- `--hidden`: Target hidden files and folders.
|
||||
```bash
|
||||
package-manager clone bsr
|
||||
package-manager install bsr
|
||||
```
|
||||
|
||||
For more detailed options, refer to the inline script help or the aforementioned conversation.
|
||||
This command makes the tool globally available as `bsr` in your terminal. 🚀
|
||||
|
||||
## License
|
||||
---
|
||||
|
||||
This project is licensed under the GNU Affero General Public License v3.0. The full license text is available in the `LICENSE` file of this repository.
|
||||
## 🚀 Usage
|
||||
|
||||
Once installed, run Bulk String Replacer CLI using the alias:
|
||||
|
||||
```bash
|
||||
bsr old_string -n "replacement_value" [options] [paths...]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
* **`old_string`**: The string or relative path to search for.
|
||||
* **`-n, --new`**: The replacement string or new relative path (default is empty string).
|
||||
* **`-r, --recursive`**: Recurse into all subdirectories and files.
|
||||
* **`-F, --folders`**: Replace occurrences within folder names.
|
||||
* **`-f, --files`**: Replace occurrences within file names.
|
||||
* **`-c, --content`**: Replace occurrences inside file contents.
|
||||
* **`-P, --path`**: Match `old_string` as a relative path (e.g. `vars/config.yml`) and move matching subtree to `new` relative path.
|
||||
* **`-p, --preview`**: Preview changes without applying them.
|
||||
* **`-v, --verbose`**: Display detailed logs during execution.
|
||||
* **`-H, --hidden`**: Include hidden files and directories in the operation.
|
||||
|
||||
### Examples
|
||||
|
||||
Replace text within filenames, folder names, and file contents:
|
||||
|
||||
```bash
|
||||
bsr "old_value" -n "new_value" -r -F -f -c /path/to/dir
|
||||
```
|
||||
|
||||
Move every `vars/configuration.yml` to `config/main.yml` in each parent directory:
|
||||
|
||||
```bash
|
||||
bsr "vars/configuration.yml" -n "config/main.yml" -r -P ./
|
||||
```
|
||||
|
||||
Preview a full-path move without changes:
|
||||
|
||||
```bash
|
||||
bsr "vars/configuration.yml" -n "config/main.yml" -r -P -p ./
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧑💻 Author
|
||||
|
||||
Developed by **Kevin Veen-Birkenbach**
|
||||
|
||||
* 📧 [kevin@veen.world](mailto:kevin@veen.world)
|
||||
* 🌐 [https://www.veen.world/](https://www.veen.world/)
|
||||
|
||||
Learn more about the development of this tool in the [original ChatGPT conversation](https://chat.openai.com/share/cfdbc008-8374-47f8-8853-2e00ee27c959).
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the **GNU Affero General Public License, Version 3, 19 November 2007**.
|
||||
For more details, see the [LICENSE](./LICENSE) file.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributions
|
||||
|
||||
Contributions are welcome! Feel free to fork the repository, submit pull requests, or open issues to help improve Bulk String Replacer CLI. Let’s make bulk updates even easier! 😊
|
||||
|
0
__init__.py
Normal file
0
__init__.py
Normal file
234
main.py
Executable file
234
main.py
Executable file
@@ -0,0 +1,234 @@
|
||||
#!/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,
|
||||
auto_path
|
||||
):
|
||||
"""
|
||||
Traverse the 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: treat old_string as a relative path and move matching items to new_string path.
|
||||
When rename_paths is set, prompts occur for Python files unless auto_path is True.
|
||||
Additionally, for test_*.py files when auto_path is False, prompts to extract 'test_' prefix into a subdirectory.
|
||||
"""
|
||||
if rename_paths:
|
||||
# Full-path moves and optional test_ prefix extraction
|
||||
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)
|
||||
|
||||
# Special handling for test_*.py when auto_path is False
|
||||
if (
|
||||
os.path.isfile(full_src)
|
||||
and name.startswith('test_')
|
||||
and name.endswith('.py')
|
||||
and not auto_path
|
||||
):
|
||||
choice = None
|
||||
while choice not in ('y', 'n'):
|
||||
choice = input(
|
||||
f"File {full_src} is a test module. "
|
||||
"Extract 'test_' prefix into subdirectory? [y/N]: "
|
||||
).strip().lower() or 'n'
|
||||
if choice == 'y':
|
||||
rel_dir = os.path.dirname(rel)
|
||||
orig_basename = os.path.basename(rel)
|
||||
base_noext = orig_basename[:-3] # remove .py
|
||||
subdir_name = base_noext[len('test_'):]
|
||||
new_name_part = os.path.basename(new_string)
|
||||
new_filename = f"test_{new_name_part}.py"
|
||||
new_rel = os.path.join(rel_dir, subdir_name, new_filename)
|
||||
|
||||
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
|
||||
|
||||
# Line-by-line content replacement 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'):
|
||||
file_path = os.path.join(root, f)
|
||||
print_verbose(f"Processing Python file: {file_path}", verbose)
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as ff:
|
||||
lines = ff.readlines()
|
||||
except UnicodeDecodeError:
|
||||
print_verbose(f"Warning: Unicode decode error in file {file_path}. Skipping.", verbose)
|
||||
continue
|
||||
old_slash = old_string.replace('/', os.sep)
|
||||
new_dot = new_string.replace('/', '.')
|
||||
changed = False
|
||||
for idx, line in enumerate(lines):
|
||||
if old_slash in line:
|
||||
start = max(0, idx - 3)
|
||||
end = min(len(lines), idx + 4)
|
||||
print(f"\nContext in {file_path}, line {idx+1}:")
|
||||
for i in range(start, end):
|
||||
prefix = '>' if i == idx else ' '
|
||||
print(f"{prefix} {i+1}: {lines[i].rstrip()}")
|
||||
|
||||
if auto_path:
|
||||
choice = '1'
|
||||
else:
|
||||
choice = None
|
||||
while choice not in ('1', '2'):
|
||||
choice = input(
|
||||
f"Replace this line:\n"
|
||||
f" 1) slash-based: '{old_slash}' → '{new_string}'\n"
|
||||
f" 2) dot-based: '{old_slash}' → '{new_dot}'\n"
|
||||
f"Choose [1/2]: "
|
||||
).strip()
|
||||
if choice == '1':
|
||||
lines[idx] = line.replace(old_slash, new_string)
|
||||
else:
|
||||
lines[idx] = line.replace(old_slash, new_dot)
|
||||
changed = True
|
||||
print_verbose(f"Replaced line {idx+1} in {file_path}", verbose)
|
||||
if changed and not preview:
|
||||
with open(file_path, 'w', encoding='utf-8') as fw:
|
||||
fw.writelines(lines)
|
||||
if not recursive:
|
||||
break
|
||||
|
||||
# Exit early if only path-mode is requested
|
||||
if not (rename_files or replace_in_content):
|
||||
return
|
||||
|
||||
# Fallback operations: replace content, rename files, rename folders
|
||||
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('.')]
|
||||
|
||||
if replace_in_content:
|
||||
for f in files:
|
||||
replace_content(os.path.join(root, f), old_string, new_string, preview, verbose)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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="Treat old_string as path and move matching items.")
|
||||
parser.add_argument('-y', '--yes', dest='auto_path', action='store_true',
|
||||
help="Skip prompts and apply default replacements.")
|
||||
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()
|
||||
|
||||
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,
|
||||
auto_path=args.auto_path
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@@ -1,77 +0,0 @@
|
||||
import os
|
||||
import argparse
|
||||
|
||||
def replace_content(path, old_string, new_string, preview, verbose):
|
||||
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)
|
||||
|
||||
if verbose:
|
||||
print(f"Replacing content in: {path}")
|
||||
|
||||
if not preview:
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
def process_directory(base_path, old_string, new_string, recursive, folder, files, content, preview, verbose, hidden):
|
||||
# Eine Liste, um die Pfade der umzubenennenden Ordner zu speichern
|
||||
directories_to_rename = []
|
||||
|
||||
for root, dirs, filenames in os.walk(base_path):
|
||||
# Wenn "hidden" nicht gesetzt ist, versteckte Dateien/Ordner aus der Liste entfernen
|
||||
if not hidden:
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||
filenames = [f for f in filenames if not f.startswith(".")]
|
||||
|
||||
if content:
|
||||
for f in filenames:
|
||||
replace_content(os.path.join(root, f), old_string, new_string, preview, verbose)
|
||||
|
||||
if files:
|
||||
for f in filenames:
|
||||
if old_string in f:
|
||||
old_path = os.path.join(root, f)
|
||||
new_path = os.path.join(root, f.replace(old_string, new_string))
|
||||
if verbose:
|
||||
print(f"Renaming file from: {old_path} to: {new_path}")
|
||||
if not preview:
|
||||
os.rename(old_path, new_path)
|
||||
|
||||
# Pfade von zu ändernden Ordnern speichern
|
||||
if folder:
|
||||
for d in dirs:
|
||||
if old_string in d:
|
||||
old_path = os.path.join(root, d)
|
||||
new_path = os.path.join(root, d.replace(old_string, new_string))
|
||||
directories_to_rename.append((old_path, new_path))
|
||||
|
||||
if not recursive:
|
||||
break
|
||||
|
||||
# Ordnernamen ändern nach dem os.walk() Durchlauf
|
||||
for old_path, new_path in directories_to_rename:
|
||||
if verbose:
|
||||
print(f"Renaming directory from: {old_path} to: {new_path}")
|
||||
if not preview:
|
||||
os.rename(old_path, new_path)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Replace strings in directories and files.")
|
||||
parser.add_argument('path', help="Path in which replacements should be made.")
|
||||
parser.add_argument('old_string', help="The string to be replaced.")
|
||||
parser.add_argument('new_string', nargs='?', default="", help="The string to replace with. Default is empty string.")
|
||||
parser.add_argument('--recursive', action='store_true', help="Replace in all subdirectories and files.")
|
||||
parser.add_argument('--folder', action='store_true', help="Replace in folder names.")
|
||||
parser.add_argument('--files', action='store_true', help="Replace in file names.")
|
||||
parser.add_argument('--content', action='store_true', help="Replace inside file contents.")
|
||||
parser.add_argument('--preview', action='store_true', help="Preview changes without replacing.")
|
||||
parser.add_argument('--verbose', action='store_true', help="Verbose mode.")
|
||||
parser.add_argument('--hidden', action='store_true', help="Apply to hidden files and folders.")
|
||||
args = parser.parse_args()
|
||||
|
||||
process_directory(args.path, args.old_string, args.new_string, args.recursive, args.folder, args.files, args.content, args.preview, args.verbose, args.hidden)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
212
test.py
Normal file
212
test.py
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
import argparse
|
||||
|
||||
from main import process_directory, replace_content, main as cli_main
|
||||
|
||||
class TestBulkStringReplacer(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# create isolated temp dir
|
||||
self.base = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.base)
|
||||
|
||||
def create_file(self, relpath, content=''):
|
||||
full = os.path.join(self.base, relpath)
|
||||
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||
with open(full, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return full
|
||||
|
||||
def test_test_prefix_extraction_prompt_no_auto(self):
|
||||
# Setup a test file with test_ prefix
|
||||
src = self.create_file('dir/test_module.py', 'print("hello")')
|
||||
# Monkeypatch input to simulate 'y'
|
||||
inputs = iter(['y'])
|
||||
original_input = __builtins__['input']
|
||||
__builtins__['input'] = lambda _: next(inputs)
|
||||
try:
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='module',
|
||||
new_string='module_new',
|
||||
recursive=True,
|
||||
rename_folders=False,
|
||||
rename_files=False,
|
||||
replace_in_content=False,
|
||||
preview=False,
|
||||
verbose=False,
|
||||
include_hidden=True,
|
||||
rename_paths=True,
|
||||
auto_path=False
|
||||
)
|
||||
finally:
|
||||
__builtins__['input'] = original_input
|
||||
|
||||
# After extraction, expect dir/module/test_module_new.py
|
||||
dst = os.path.join(self.base, 'dir', 'module', 'test_module_new.py')
|
||||
self.assertTrue(os.path.exists(dst))
|
||||
|
||||
def test_test_prefix_default_no_extraction_auto(self):
|
||||
# Setup a test file with test_ prefix
|
||||
src = self.create_file('dir/test_file.py', 'print("hello")')
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='file',
|
||||
new_string='file_new',
|
||||
recursive=True,
|
||||
rename_folders=False,
|
||||
rename_files=False,
|
||||
replace_in_content=False,
|
||||
preview=False,
|
||||
verbose=False,
|
||||
include_hidden=True,
|
||||
rename_paths=True,
|
||||
auto_path=True
|
||||
)
|
||||
# With auto_path, should do simple path replace: dir/test_file.py -> dir/test_file_new.py
|
||||
dst = os.path.join(self.base, 'dir', 'test_file_new.py')
|
||||
self.assertTrue(os.path.exists(dst))
|
||||
|
||||
|
||||
def test_replace_content(self):
|
||||
f = self.create_file('foo.txt', 'hello OLD world')
|
||||
replace_content(f, 'OLD', 'NEW', preview=False, verbose=False)
|
||||
with open(f, 'r', encoding='utf-8') as fp:
|
||||
self.assertIn('hello NEW world', fp.read())
|
||||
|
||||
def test_rename_file(self):
|
||||
f = self.create_file('OLDfile.txt', '')
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='OLD', new_string='NEW',
|
||||
recursive=False,
|
||||
rename_folders=False, rename_files=True,
|
||||
replace_in_content=False,
|
||||
preview=False, verbose=False,
|
||||
include_hidden=True, rename_paths=False, auto_path=True
|
||||
)
|
||||
self.assertTrue(os.path.exists(os.path.join(self.base, 'NEWfile.txt')))
|
||||
self.assertFalse(os.path.exists(f))
|
||||
|
||||
def test_rename_folder(self):
|
||||
os.makedirs(os.path.join(self.base, 'OLDfolder', 'x'))
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='OLD', new_string='NEW',
|
||||
recursive=False,
|
||||
rename_folders=True, rename_files=False,
|
||||
replace_in_content=False,
|
||||
preview=False, verbose=False,
|
||||
include_hidden=True, rename_paths=False, auto_path=True
|
||||
)
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.base, 'NEWfolder')))
|
||||
self.assertFalse(os.path.isdir(os.path.join(self.base, 'OLDfolder')))
|
||||
|
||||
def test_full_path_move(self):
|
||||
cfg = 'vars/configuration.yml'
|
||||
full = self.create_file(cfg, 'DATA')
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='vars/configuration.yml',
|
||||
new_string='config/main.yml',
|
||||
recursive=True,
|
||||
rename_folders=False, rename_files=False,
|
||||
replace_in_content=False,
|
||||
preview=False, verbose=False,
|
||||
include_hidden=True, rename_paths=True, auto_path=True
|
||||
)
|
||||
self.assertFalse(os.path.exists(full))
|
||||
self.assertTrue(os.path.exists(os.path.join(self.base, 'config', 'main.yml')))
|
||||
|
||||
def test_path_and_folders_conflict(self):
|
||||
parser = argparse.ArgumentParser()
|
||||
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'])
|
||||
with self.assertRaises(SystemExit):
|
||||
if args.rename_paths and args.folders:
|
||||
parser.error("Cannot use --path and --folders together.")
|
||||
|
||||
def test_preview_does_nothing(self):
|
||||
f = self.create_file('OLD.txt', 'OLD')
|
||||
os.makedirs(os.path.join(self.base, 'OLDdir'))
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='OLD', new_string='NEW',
|
||||
recursive=True,
|
||||
rename_folders=True, rename_files=True,
|
||||
replace_in_content=True,
|
||||
preview=True, verbose=False,
|
||||
include_hidden=True, rename_paths=True, auto_path=True
|
||||
)
|
||||
self.assertTrue(os.path.exists(f))
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.base, 'OLDdir')))
|
||||
with open(f, 'r', encoding='utf-8') as fp:
|
||||
self.assertIn('OLD', fp.read())
|
||||
|
||||
def test_module_path_replacement_in_python_files(self):
|
||||
content = 'from old/path import func'
|
||||
src = self.create_file('old/path/module.py', content)
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='old/path', new_string='old.path',
|
||||
recursive=True,
|
||||
rename_folders=False, rename_files=False,
|
||||
replace_in_content=False,
|
||||
preview=False, verbose=False,
|
||||
include_hidden=True, rename_paths=True, auto_path=True
|
||||
)
|
||||
new_path = os.path.join(self.base, 'old.path', 'module.py')
|
||||
self.assertTrue(os.path.exists(new_path))
|
||||
with open(new_path, 'r', encoding='utf-8') as fp:
|
||||
self.assertIn('from old.path import func', fp.read())
|
||||
|
||||
def test_non_python_files_are_not_content_updated(self):
|
||||
content = 'some reference to old/path in text'
|
||||
txt = self.create_file('old/path/readme.txt', content)
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='old/path', new_string='old.path',
|
||||
recursive=True,
|
||||
rename_folders=False, rename_files=False,
|
||||
replace_in_content=False,
|
||||
preview=False, verbose=False,
|
||||
include_hidden=True, rename_paths=True, auto_path=True
|
||||
)
|
||||
new_txt = os.path.join(self.base, 'old.path', 'readme.txt')
|
||||
self.assertTrue(os.path.exists(new_txt))
|
||||
with open(new_txt, 'r', encoding='utf-8') as fp:
|
||||
self.assertIn('old/path', fp.read())
|
||||
|
||||
def test_auto_path_line_level_replacement(self):
|
||||
# create a Python file with two occurrences
|
||||
lines = [
|
||||
'import a\n',
|
||||
'path = "old/path/to/module"\n',
|
||||
'print("old/path/example")\n'
|
||||
]
|
||||
py = self.create_file('old/path/test.py', ''.join(lines))
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='old/path', new_string='new/path',
|
||||
recursive=True,
|
||||
rename_folders=False, rename_files=False,
|
||||
replace_in_content=False,
|
||||
preview=False, verbose=False,
|
||||
include_hidden=True, rename_paths=True, auto_path=True
|
||||
)
|
||||
new_py = os.path.join(self.base, 'new', 'path', 'test.py')
|
||||
self.assertTrue(os.path.exists(new_py))
|
||||
with open(new_py, 'r', encoding='utf-8') as fp:
|
||||
content = fp.read()
|
||||
# all replaced slash-based
|
||||
self.assertIn('path = "new/path/to/module"', content)
|
||||
self.assertIn('print("new/path/example")', content)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Reference in New Issue
Block a user