Rename an identifier in all source files in a directory tree.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | #!/usr/bin/python
# NOTE: This script has only been tested on Linux. In order to get it to work
# on Windows or Mac OS, find a working implementation of getchar/getch and
# replace the function getch below.
import os
import os.path
import re
import getopt
import sys, tty, termios
# Prevent infinite recursion in case of mistake
ITERATION_LIMIT = 100
FILENAME_ENDINGS = ['.py','.js','.html']
INTERACTIVE = False
MAKE_BACKUPS = False
DRY_RUN = False
TOP = os.getcwd()
USAGE = """Usage: deeprename [-i] [-b] oldword newword
Replace all occurrences of oldword with newword in all source files
(javascript, python, html) throughout the entire directory tree rooted at the
current working directory. Hidden files and directories are skipped.
-i: interactive mode
-b: make backups
-l: do not rename, just print out files that will be checked and exit
-d: dry run: just show files and lines that will be changed, don't make any changes
"""
### Getch
def getch(echo = False):
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
if echo: sys.stdout.write(ch)
return ch
def gen_filepaths():
return (pth for pth in all_filepaths() if pth.find('/.') == -1 and any(pth.endswith(ending) for ending in FILENAME_ENDINGS))
def all_filepaths():
for path, dirlist, filelist in os.walk(TOP):
for filename in filelist:
yield os.path.join(path, filename)
def replaced_line(pat,sub,original_line):
if DRY_RUN: return original_line
line = original_line
n = 0
while True:
if n > ITERATION_LIMIT:
raise Exception("iteration limit exceeded on line: \n%s" % original_line)
n += 1
if pat.search(line):
line = re.sub(pat,sub,line)
else:
return line
def file_has_pattern(pat,filepath):
f = open(filepath)
for line in f:
if pat.search(line):
f.close()
return True
f.close()
return False
def prompt_line(pat,line):
line = line.rstrip()
matchobj = pat.search(line)
start, end = matchobj.start(), matchobj.end()
start = max(0,start - 25)
prompt_line = line[start:end + 25]
prompt_line = prompt_line[:60]
prompt_line = prompt_line + (60 - len(prompt_line)) * ' '
if start > 0:
prompt_line = '...' + prompt_line[3:]
return prompt_line
def user_prompt(pat,line):
sys.stdout.write(prompt_line(pat,line) + ": ")
return_value = getch(True)
sys.stdout.write('\n')
return return_value
def deeprename_files(pat,sub):
for filepath in gen_filepaths():
if file_has_pattern(pat,filepath):
print "========================================"
print "== %s" % filepath
print "========================================"
filepath_bak = filepath + '.bak'
os.rename(filepath, filepath_bak)
f = open(filepath_bak)
g = open(filepath,"w")
if INTERACTIVE:
continue_rename = rename_one_interactive(f,g,pat,sub)
if not continue_rename:
break
else:
rename_one(f,g,pat,sub)
if not MAKE_BACKUPS:
os.remove(filepath_bak)
def rename_one_interactive(f,g,pat,sub):
f = iter(f)
while True:
try:
line = f.next()
except StopIteration:
return True
if not pat.search(line):
g.write(line)
else:
user_cmd = user_prompt(pat,line)
if user_cmd == 'y':
# replace the line and continue
g.write(replaced_line(pat,sub,line))
elif user_cmd == 'n':
# use original line
g.write(line)
elif user_cmd == 'q':
# quit
g.write(line)
# flush remaining lines
for line in f:
if pat.search(line): print prompt_line(pat,line)
g.write(line)
return False
def rename_one(f,g,pat,sub):
for line in f:
if pat.search(line):
print prompt_line(pat,line)
g.write(replaced_line(pat,sub,line))
else:
g.write(line)
def check_bakfiles():
bakpaths = [pth for pth in all_filepaths() if pth.endswith('.bak')]
if len(bakpaths) > 0:
raise Exception("Fatal: backup files ('.bak') detected: %s" % ', '.join(bakpaths))
def check_swapfiles():
swappaths = [pth for pth in all_filepaths() if pth.endswith('.swp')]
if len(swappaths) > 0:
raise Exception("Fatal: editor swap files ('.swp') detected: %s" % ', '.join(swappaths))
def exec_cmd():
global INTERACTIVE, MAKE_BACKUPS, DRY_RUN
oplist,args = getopt.getopt(sys.argv[1:],"ibldh")
check_bakfiles()
check_swapfiles()
for opt, val in oplist:
if opt == '-h':
print USAGE
return None
if opt == '-i':
INTERACTIVE = True
elif opt == '-b':
MAKE_BACKUPS = True
elif opt == '-d':
DRY_RUN = True
elif opt == '-l':
for filename in gen_filepaths():
print filename
return None
if len(args) != 2:
print USAGE
return None
name, sub = args
deeprename_files(re.compile(r"\b" + name + r"\b"), sub)
if __name__ == '__main__':
exec_cmd()
|
Known issues:
- Not compatible with Windows or Mac OS yet
- Dry run still moves and copies files
And what about using standard shell tools ? Something like : find /PATH -".py" -exec sed -i s/old/new/g {} \;
Sed provides dry-run mode, regular expr and optional backup file too. There's no interactive mode, but for standard use it is ok.
Why not use readline to edit the file names?