Welcome, guest | Sign In | My Account | Store | Cart

Rename an identifier in all source files in a directory tree.

Python, 185 lines
  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

2 comments

sebastien.renard 13 years, 1 month ago  # | flag

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.

Alain Mellan 13 years, 1 month ago  # | flag

Why not use readline to edit the file names?