1 #! /usr/bin/env python 2 3 # Perform massive identifier substitution on C source files. 4 # This actually tokenizes the files (to some extent) so it can 5 # avoid making substitutions inside strings or comments. 6 # Inside strings, substitutions are never made; inside comments, 7 # it is a user option (off by default). 8 # 9 # The substitutions are read from one or more files whose lines, 10 # when not empty, after stripping comments starting with #, 11 # must contain exactly two words separated by whitespace: the 12 # old identifier and its replacement. 13 # 14 # The option -r reverses the sense of the substitutions (this may be 15 # useful to undo a particular substitution). 16 # 17 # If the old identifier is prefixed with a '*' (with no intervening 18 # whitespace), then it will not be substituted inside comments. 19 # 20 # Command line arguments are files or directories to be processed. 21 # Directories are searched recursively for files whose name looks 22 # like a C file (ends in .h or .c). The special filename '-' means 23 # operate in filter mode: read stdin, write stdout. 24 # 25 # Symbolic links are always ignored (except as explicit directory 26 # arguments). 27 # 28 # The original files are kept as back-up with a "~" suffix. 29 # 30 # Changes made are reported to stdout in a diff-like format. 31 # 32 # NB: by changing only the function fixline() you can turn this 33 # into a program for different changes to C source files; by 34 # changing the function wanted() you can make a different selection of 35 # files. 36 37 import sys 38 import re 39 import os 40 from stat import * 41 import getopt 42 43 err = sys.stderr.write 44 dbg = err 45 rep = sys.stdout.write 46 47 def usage(): 48 progname = sys.argv[0] 49 err('Usage: ' + progname + 50 ' [-c] [-r] [-s file] ... file-or-directory ...\n') 51 err('\n') 52 err('-c : substitute inside comments\n') 53 err('-r : reverse direction for following -s options\n') 54 err('-s substfile : add a file of substitutions\n') 55 err('\n') 56 err('Each non-empty non-comment line in a substitution file must\n') 57 err('contain exactly two words: an identifier and its replacement.\n') 58 err('Comments start with a # character and end at end of line.\n') 59 err('If an identifier is preceded with a *, it is not substituted\n') 60 err('inside a comment even when -c is specified.\n') 61 62 def main(): 63 try: 64 opts, args = getopt.getopt(sys.argv[1:], 'crs:') 65 except getopt.error, msg: 66 err('Options error: ' + str(msg) + '\n') 67 usage() 68 sys.exit(2) 69 bad = 0 70 if not args: # No arguments 71 usage() 72 sys.exit(2) 73 for opt, arg in opts: 74 if opt == '-c': 75 setdocomments() 76 if opt == '-r': 77 setreverse() 78 if opt == '-s': 79 addsubst(arg) 80 for arg in args: 81 if os.path.isdir(arg): 82 if recursedown(arg): bad = 1 83 elif os.path.islink(arg): 84 err(arg + ': will not process symbolic links\n') 85 bad = 1 86 else: 87 if fix(arg): bad = 1 88 sys.exit(bad) 89 90 # Change this regular expression to select a different set of files 91 Wanted = '^[a-zA-Z0-9_]+\.[ch]$' 92 def wanted(name): 93 return re.match(Wanted, name) >= 0 94 95 def recursedown(dirname): 96 dbg('recursedown(%r)\n' % (dirname,)) 97 bad = 0 98 try: 99 names = os.listdir(dirname) 100 except os.error, msg: 101 err(dirname + ': cannot list directory: ' + str(msg) + '\n') 102 return 1 103 names.sort() 104 subdirs = [] 105 for name in names: 106 if name in (os.curdir, os.pardir): continue 107 fullname = os.path.join(dirname, name) 108 if os.path.islink(fullname): pass 109 elif os.path.isdir(fullname): 110 subdirs.append(fullname) 111 elif wanted(name): 112 if fix(fullname): bad = 1 113 for fullname in subdirs: 114 if recursedown(fullname): bad = 1 115 return bad 116 117 def fix(filename): 118 ## dbg('fix(%r)\n' % (filename,)) 119 if filename == '-': 120 # Filter mode 121 f = sys.stdin 122 g = sys.stdout 123 else: 124 # File replacement mode 125 try: 126 f = open(filename, 'r') 127 except IOError, msg: 128 err(filename + ': cannot open: ' + str(msg) + '\n') 129 return 1 130 head, tail = os.path.split(filename) 131 tempname = os.path.join(head, '@' + tail) 132 g = None 133 # If we find a match, we rewind the file and start over but 134 # now copy everything to a temp file. 135 lineno = 0 136 initfixline() 137 while 1: 138 line = f.readline() 139 if not line: break 140 lineno = lineno + 1 141 while line[-2:] == '\\\n': 142 nextline = f.readline() 143 if not nextline: break 144 line = line + nextline 145 lineno = lineno + 1 146 newline = fixline(line) 147 if newline != line: 148 if g is None: 149 try: 150 g = open(tempname, 'w') 151 except IOError, msg: 152 f.close() 153 err(tempname+': cannot create: '+ 154 str(msg)+'\n') 155 return 1 156 f.seek(0) 157 lineno = 0 158 initfixline() 159 rep(filename + ':\n') 160 continue # restart from the beginning 161 rep(repr(lineno) + '\n') 162 rep('< ' + line) 163 rep('> ' + newline) 164 if g is not None: 165 g.write(newline) 166 167 # End of file 168 if filename == '-': return 0 # Done in filter mode 169 f.close() 170 if not g: return 0 # No changes 171 172 # Finishing touch -- move files 173 174 # First copy the file's mode to the temp file 175 try: 176 statbuf = os.stat(filename) 177 os.chmod(tempname, statbuf[ST_MODE] & 07777) 178 except os.error, msg: 179 err(tempname + ': warning: chmod failed (' + str(msg) + ')\n') 180 # Then make a backup of the original file as filename~ 181 try: 182 os.rename(filename, filename + '~') 183 except os.error, msg: 184 err(filename + ': warning: backup failed (' + str(msg) + ')\n') 185 # Now move the temp file to the original file 186 try: 187 os.rename(tempname, filename) 188 except os.error, msg: 189 err(filename + ': rename failed (' + str(msg) + ')\n') 190 return 1 191 # Return success 192 return 0 193 194 # Tokenizing ANSI C (partly) 195 196 Identifier = '\(struct \)?[a-zA-Z_][a-zA-Z0-9_]+' 197 String = '"\([^\n\\"]\|\\\\.\)*"' 198 Char = '\'\([^\n\\\']\|\\\\.\)*\'' 199 CommentStart = '/\*' 200 CommentEnd = '\*/' 201 202 Hexnumber = '0[xX][0-9a-fA-F]*[uUlL]*' 203 Octnumber = '0[0-7]*[uUlL]*' 204 Decnumber = '[1-9][0-9]*[uUlL]*' 205 Intnumber = Hexnumber + '\|' + Octnumber + '\|' + Decnumber 206 Exponent = '[eE][-+]?[0-9]+' 207 Pointfloat = '\([0-9]+\.[0-9]*\|\.[0-9]+\)\(' + Exponent + '\)?' 208 Expfloat = '[0-9]+' + Exponent 209 Floatnumber = Pointfloat + '\|' + Expfloat 210 Number = Floatnumber + '\|' + Intnumber 211 212 # Anything else is an operator -- don't list this explicitly because of '/*' 213 214 OutsideComment = (Identifier, Number, String, Char, CommentStart) 215 OutsideCommentPattern = '(' + '|'.join(OutsideComment) + ')' 216 OutsideCommentProgram = re.compile(OutsideCommentPattern) 217 218 InsideComment = (Identifier, Number, CommentEnd) 219 InsideCommentPattern = '(' + '|'.join(InsideComment) + ')' 220 InsideCommentProgram = re.compile(InsideCommentPattern) 221 222 def initfixline(): 223 global Program 224 Program = OutsideCommentProgram 225 226 def fixline(line): 227 global Program 228 ## print '-->', repr(line) 229 i = 0 230 while i < len(line): 231 i = Program.search(line, i) 232 if i < 0: break 233 found = Program.group(0) 234 ## if Program is InsideCommentProgram: print '...', 235 ## else: print ' ', 236 ## print found 237 if len(found) == 2: 238 if found == '/*': 239 Program = InsideCommentProgram 240 elif found == '*/': 241 Program = OutsideCommentProgram 242 n = len(found) 243 if Dict.has_key(found): 244 subst = Dict[found] 245 if Program is InsideCommentProgram: 246 if not Docomments: 247 print 'Found in comment:', found 248 i = i + n 249 continue 250 if NotInComment.has_key(found): 251 ## print 'Ignored in comment:', 252 ## print found, '-->', subst 253 ## print 'Line:', line, 254 subst = found 255 ## else: 256 ## print 'Substituting in comment:', 257 ## print found, '-->', subst 258 ## print 'Line:', line, 259 line = line[:i] + subst + line[i+n:] 260 n = len(subst) 261 i = i + n 262 return line 263 264 Docomments = 0 265 def setdocomments(): 266 global Docomments 267 Docomments = 1 268 269 Reverse = 0 270 def setreverse(): 271 global Reverse 272 Reverse = (not Reverse) 273 274 Dict = {} 275 NotInComment = {} 276 def addsubst(substfile): 277 try: 278 fp = open(substfile, 'r') 279 except IOError, msg: 280 err(substfile + ': cannot read substfile: ' + str(msg) + '\n') 281 sys.exit(1) 282 lineno = 0 283 while 1: 284 line = fp.readline() 285 if not line: break 286 lineno = lineno + 1 287 try: 288 i = line.index('#') 289 except ValueError: 290 i = -1 # Happens to delete trailing \n 291 words = line[:i].split() 292 if not words: continue 293 if len(words) == 3 and words[0] == 'struct': 294 words[:2] = [words[0] + ' ' + words[1]] 295 elif len(words) <> 2: 296 err(substfile + '%s:%r: warning: bad line: %r' % (substfile, lineno, line)) 297 continue 298 if Reverse: 299 [value, key] = words 300 else: 301 [key, value] = words 302 if value[0] == '*': 303 value = value[1:] 304 if key[0] == '*': 305 key = key[1:] 306 NotInComment[key] = value 307 if Dict.has_key(key): 308 err('%s:%r: warning: overriding: %r %r\n' % (substfile, lineno, key, value)) 309 err('%s:%r: warning: previous: %r\n' % (substfile, lineno, Dict[key])) 310 Dict[key] = value 311 fp.close() 312 313 if __name__ == '__main__': 314 main() 315