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
37import sys
38import re
39import os
40from stat import *
41import getopt
42
43err = sys.stderr.write
44dbg = err
45rep = sys.stdout.write
46
47def 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
62def 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
91Wanted = '^[a-zA-Z0-9_]+\.[ch]$'
92def wanted(name):
93    return re.match(Wanted, name) >= 0
94
95def 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
117def 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
196Identifier = '\(struct \)?[a-zA-Z_][a-zA-Z0-9_]+'
197String = '"\([^\n\\"]\|\\\\.\)*"'
198Char = '\'\([^\n\\\']\|\\\\.\)*\''
199CommentStart = '/\*'
200CommentEnd = '\*/'
201
202Hexnumber = '0[xX][0-9a-fA-F]*[uUlL]*'
203Octnumber = '0[0-7]*[uUlL]*'
204Decnumber = '[1-9][0-9]*[uUlL]*'
205Intnumber = Hexnumber + '\|' + Octnumber + '\|' + Decnumber
206Exponent = '[eE][-+]?[0-9]+'
207Pointfloat = '\([0-9]+\.[0-9]*\|\.[0-9]+\)\(' + Exponent + '\)?'
208Expfloat = '[0-9]+' + Exponent
209Floatnumber = Pointfloat + '\|' + Expfloat
210Number = Floatnumber + '\|' + Intnumber
211
212# Anything else is an operator -- don't list this explicitly because of '/*'
213
214OutsideComment = (Identifier, Number, String, Char, CommentStart)
215OutsideCommentPattern = '(' + '|'.join(OutsideComment) + ')'
216OutsideCommentProgram = re.compile(OutsideCommentPattern)
217
218InsideComment = (Identifier, Number, CommentEnd)
219InsideCommentPattern = '(' + '|'.join(InsideComment) + ')'
220InsideCommentProgram = re.compile(InsideCommentPattern)
221
222def initfixline():
223    global Program
224    Program = OutsideCommentProgram
225
226def 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
264Docomments = 0
265def setdocomments():
266    global Docomments
267    Docomments = 1
268
269Reverse = 0
270def setreverse():
271    global Reverse
272    Reverse = (not Reverse)
273
274Dict = {}
275NotInComment = {}
276def 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
313if __name__ == '__main__':
314    main()
315