|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +""" |
| 4 | +
|
| 5 | +
|
| 6 | +
|
| 7 | +Recursively find and replace text in files under a specific folder with preview of changed data in dry-run mode |
| 8 | +============ |
| 9 | +
|
| 10 | +Example Usage |
| 11 | +--------------- |
| 12 | +
|
| 13 | +**See what is going to change (dry run):** |
| 14 | +
|
| 15 | +> flip all dates from 2017-12-31 to 31-12-2017 |
| 16 | +
|
| 17 | + find_replace.py --dir project/myfolder --search-regex "\d{4}-\d{2}-\d{2}" --replace-regex "\3-\2-\1" --dry-run |
| 18 | +
|
| 19 | +**Do actual replacement:** |
| 20 | +
|
| 21 | + find_replace.py --dir project/myfolder --search-regex "\d{4}-\d{2}-\d{2}" --replace-regex "\3-\2-\1" |
| 22 | +
|
| 23 | +**Do actual replacement and create backup files:** |
| 24 | +
|
| 25 | + find_replace.py --dir project/myfolder --search-regex "\d{4}-\d{2}-\d{2}" --replace-regex "\3-\2-\1" --create-backup |
| 26 | +
|
| 27 | +**Same action as previous command with short-hand syntax:** |
| 28 | +
|
| 29 | + find_replace.py -d project/myfolder -s "\d{4}-\d{2}-\d{2}" -r "\3-\2-\1" -b |
| 30 | +
|
| 31 | +Output of `find_replace.py -h`: |
| 32 | +
|
| 33 | +usage: find-replace-in-files-regex.py [-h] [--dir DIR] --search-regex |
| 34 | + SEARCH_REGEX --replace-regex |
| 35 | + REPLACE_REGEX [--glob GLOB] [--dry-run] |
| 36 | + [--create-backup] [--verbose] |
| 37 | + [--print-parent-folder] |
| 38 | +
|
| 39 | +USAGE: |
| 40 | + find-replace-in-files-regex.py -d [my_folder] -s <search_regex> -r <replace_regex> -g [glob_pattern] |
| 41 | +""" |
| 42 | + |
| 43 | +from __future__ import print_function |
| 44 | +import os |
| 45 | +import fnmatch |
| 46 | +import sys |
| 47 | +import shutil |
| 48 | +import re |
| 49 | + |
| 50 | +import argparse |
| 51 | + |
| 52 | + |
| 53 | +class Colors: |
| 54 | + Default = "\033[39m" |
| 55 | + Black = "\033[30m" |
| 56 | + Red = "\033[31m" |
| 57 | + Green = "\033[32m" |
| 58 | + Yellow = "\033[33m" |
| 59 | + Blue = "\033[34m" |
| 60 | + Magenta = "\033[35m" |
| 61 | + Cyan = "\033[36m" |
| 62 | + LightGray = "\033[37m" |
| 63 | + DarkGray = "\033[90m" |
| 64 | + LightRed = "\033[91m" |
| 65 | + LightGreen = "\033[92m" |
| 66 | + LightYellow = "\033[93m" |
| 67 | + LightBlue = "\033[94m" |
| 68 | + LightMagenta = "\033[95m" |
| 69 | + LightCyan = "\033[96m" |
| 70 | + White = "\033[97m" |
| 71 | + NoColor = "\033[0m" |
| 72 | + |
| 73 | + |
| 74 | +def find_replace(cfg): |
| 75 | + |
| 76 | + search_pattern = re.compile(cfg.search_regex) |
| 77 | + |
| 78 | + if cfg.dry_run: |
| 79 | + print('THIS IS A DRY RUN -- NO FILES WILL BE CHANGED!') |
| 80 | + |
| 81 | + for path, dirs, files in os.walk(os.path.abspath(cfg.dir)): |
| 82 | + for filename in fnmatch.filter(files, cfg.glob): |
| 83 | + |
| 84 | + if cfg.print_parent_folder: |
| 85 | + pardir = os.path.normpath(os.path.join(path, '..')) |
| 86 | + pardir = os.path.split(pardir)[-1] |
| 87 | + print('[%s]' % pardir) |
| 88 | + full_path = os.path.join(path, filename) |
| 89 | + |
| 90 | + # backup original file |
| 91 | + if cfg.create_backup: |
| 92 | + backup_path = full_path + '.bak' |
| 93 | + |
| 94 | + while os.path.exists(backup_path): |
| 95 | + backup_path += '.bak' |
| 96 | + print('DBG: creating backup', backup_path) |
| 97 | + shutil.copyfile(full_path, backup_path) |
| 98 | + |
| 99 | + if os.path.islink(full_path): |
| 100 | + print("{}File {} is a symlink. Skipping{}".format(Colors.Red, full_path, Colors.NoColor)) |
| 101 | + continue |
| 102 | + |
| 103 | + with open(full_path) as f: |
| 104 | + old_text = f.read() |
| 105 | + |
| 106 | + all_matches = search_pattern.findall(old_text) |
| 107 | + |
| 108 | + if all_matches: |
| 109 | + |
| 110 | + print('{}Found {} match(es) in file {}{}'.format(Colors.LightMagenta, len(all_matches), filename, Colors.NoColor)) |
| 111 | + |
| 112 | + new_text = search_pattern.sub(cfg.replace_regex, old_text) |
| 113 | + |
| 114 | + if not cfg.dry_run: |
| 115 | + with open(full_path, "w") as f: |
| 116 | + print('DBG: replacing in file', full_path) |
| 117 | + f.write(new_text) |
| 118 | + # else: |
| 119 | + # for idx, matches in enumerate(all_matches): |
| 120 | + # print("Match #{}: {}".format(idx, matches)) |
| 121 | + |
| 122 | + if cfg.verbose or cfg.dry_run: |
| 123 | + colorized_old = search_pattern.sub(Colors.LightBlue + r"\g<0>" + Colors.NoColor, old_text) |
| 124 | + colorized_old = '\n'.join(['\t' + line.strip() for line in colorized_old.split('\n') if Colors.LightBlue in line]) |
| 125 | + |
| 126 | + colorized = search_pattern.sub(Colors.Green + cfg.replace_regex + Colors.NoColor, old_text) |
| 127 | + colorized = '\n'.join(['\t' + line.strip() for line in colorized.split('\n') if Colors.Green in line]) |
| 128 | + print("{}BEFORE:{}\n{}".format(Colors.White, Colors.NoColor, colorized_old)) |
| 129 | + print("{}AFTER :{}\n{}".format(Colors.Yellow, Colors.NoColor, colorized)) |
| 130 | + |
| 131 | + elif cfg.list_non_matching: |
| 132 | + print('File {} does not contain search regex "{}"'.format(filename, cfg.search_regex)) |
| 133 | + |
| 134 | + |
| 135 | +if __name__ == '__main__': |
| 136 | + |
| 137 | + parser = argparse.ArgumentParser(description='''DESCRIPTION: |
| 138 | + Find and replace recursively from the given folder using regular expressions''', |
| 139 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 140 | + epilog='''USAGE: |
| 141 | + {0} -d [my_folder] -s <search_regex> -r <replace_regex> -g [glob_pattern] |
| 142 | +
|
| 143 | + '''.format(os.path.basename(sys.argv[0]))) |
| 144 | + |
| 145 | + parser.add_argument('--dir', '-d', |
| 146 | + help='folder to search in; by default current folder', |
| 147 | + default='.') |
| 148 | + |
| 149 | + parser.add_argument('--search-regex', '-s', |
| 150 | + help='search regex', |
| 151 | + required=True) |
| 152 | + |
| 153 | + parser.add_argument('--replace-regex', '-r', |
| 154 | + help='replacement regex', |
| 155 | + required=True) |
| 156 | + |
| 157 | + parser.add_argument('--glob', '-g', |
| 158 | + help='glob pattern, i.e. *.html', |
| 159 | + default="*.*") |
| 160 | + |
| 161 | + parser.add_argument('--dry-run', '-dr', |
| 162 | + action='store_true', |
| 163 | + help="don't replace anything just show what is going to be done", |
| 164 | + default=False) |
| 165 | + |
| 166 | + parser.add_argument('--create-backup', '-b', |
| 167 | + action='store_true', |
| 168 | + help='Create backup files', |
| 169 | + default=False) |
| 170 | + |
| 171 | + parser.add_argument('--verbose', '-v', |
| 172 | + action='store_true', |
| 173 | + help="Show files which don't match the search regex", |
| 174 | + default=False) |
| 175 | + |
| 176 | + parser.add_argument('--print-parent-folder', '-p', |
| 177 | + action='store_true', |
| 178 | + help="Show the parent info for debug", |
| 179 | + default=False) |
| 180 | + |
| 181 | + parser.add_argument('--list-non-matching', '-n', |
| 182 | + action='store_true', |
| 183 | + help="Supress colors", |
| 184 | + default=False) |
| 185 | + |
| 186 | + config = parser.parse_args(sys.argv[1:]) |
| 187 | + |
| 188 | + find_replace(config) |
| 189 | + |
0 commit comments