ifdef decorator

Some time ago I wrote a piece of code to fix one of my biggest pet peeves in our code at work – the fact that a large number of #if(def) foo statements have #else or #endif statements attached which are blank. In other words, the #endif doesn’t have /*foo*/ after it to tell you which #ifdef you’re coming from. This drives me nuts, so I fixed it. The code is in python, and looks like this:

import walk, re
from os.path import split
from os import getcwd
from sys import argv
from optparse import OptionParser

#basic setup
patternmatch ="*.c;*.h"
ifdeflist = []

#define the re's we need
ifpattern = re.compile("^[ t]*"        #find any number of whitespace chars before the #if
                          "#if"        #find a #if at least
                          "(n?def)?"    #allow only one def (or ndef) to complete the previous #if      (group 0)
                          " +"         #find at least one space    
                          "(.*)"        #find everything else                                           (group 1)
                          "$"           #find the end of the line
                          ,re.VERBOSE)

elseendpattern = re.compile("^[ t]*"   #find any number of whitespace chars before the #else
                            "#"        #find a #
                            "(else|"    #find an else or...
                            "endif)"    #find an endif
                            "[t ]*"    #find any amount of whitespace 
                            "(.*)$"     #find everything else, that runs to the end of the line     (group 0)
                          ,re.VERBOSE)

def writeListToFile(filename,list,head=""):
    '''Writes a list of lines to a supplied file name, preceded by a supplied header. Returns true if no errors, false otherwise.'''
    try:
        file = open(filename,"a")
        #check if appending or writing a new file
        if file.tell() >= 1:
            #appending, so add a delimiter from previous file contents
            file.write("n------------------------------------------------------------n")
        if head: file.write(head)
        file.write("n".join(list))
    except:
        file.close()
        return False
    file.close()
    return True

def processFiles(dir, pattern, errorfile, goodfile, usecomment=False,recurse=False):
    '''Generates a list of files matching the pattern supplied. Calls the decorateFile function on each file found,
    then records details of success or failure and the number of files processed and actually edited.'''
    errorFiles = []
    goodFiles = []
    numFiles = 0
    #find and process each file that matches
    for file in walk.find_files_in_tree(dir,pattern,not recurse):
        code = decorateFile(file,usecomment)
        if not code:
            errorFiles.append(file)
        elif code == 2:
            goodFiles.append(file)
        numFiles += 1

#Print the results
    print "n%d file(s) in total were processed, while %d had #if(def)'s that needed decorating. %s file(s) reported errors." % (numFiles,len(goodFiles),len(errorFiles))

    if goodFiles:
        if writeListToFile(goodfile, goodFiles,"These files had their if(def)'s decorated:nn"):
            print "A list of successfully decorated files is available in %s" % (split(goodfile)[1],)
        else:
            print "Unfortunately, an error occurred during the writing of the list of decorated files to %s" % (split(goodfile)[1],)
            
    if errorFiles:
        if writeListToFile(errorfile, errorFiles,"These files had errors:nn"):
            print "A list of files with errors is available in %s" % (split(errorfile)[1],)
        else:
            print "Unfortunately, an error occurred during the writing of the list of error-causing files to %s" % (split(errorfile)[1],)

def decorateFile(file,usecomment=False):
    '''Opens a passed in file and parses it for undecorated #else and #endif statements.
    These are decorated with the text attached to the associated #if(def).
    The changes are written back to the passed in file.
    Returns False if some error occured.
    Returns 1 if no changes were made, 2 if some changes were made.'''
    returnCode = 1
    #try to open the file and get into a n-less list. 2 try's ensure we handle the exception, but also always close the file
    try:
        try:
            fileobj = open(file,"r")
            linelist = fileobj.readlines()
            linelist = [line.strip("n") for line in linelist]
        #close the fileobj if we hit a failure and return false
        finally:
            fileobj.close()
    except:
        return False

    #try to open the file again, to write into. 2 try's ensure we handle the exception, but also always close the file
    try:
        try:
            fileobj = open(file,"w")

            for line in linelist:
                #check for #else or #endif matches
                matchline = elseendpattern.search(line)
                #check that we have a match to #else or #endif with only whitespace after it
                if matchline and not matchline.groups()[1]:
                    extraText = ifdeflist[-1]
                    #see if we have a #endif - need to pop from the ifdeflist
                    if matchline.groups()[0] == "endif":
                        ifdeflist.pop()
                    #print the #else or #endif along with the ifdef text commented after it
                    fileobj.write("%s %s%s%sn" % (line.rstrip(), usecomment and "/*" or "", extraText, usecomment and "*/" or ""))
                    returnCode = 2
                    continue
                #check for #ifdef matches - grabbing only char's before a comment starts
                matchline = ifpattern.search(line.split("/*")[0].strip("t "))
                if matchline:
                    #Found a #if(def), so grab its text
                    ifdeflist.append(matchline.groups()[1])
                    #intentional fall-through
                #write the line back to the file
                fileobj.write("%sn" % (line,))
 
        #close the fileobj if we hit a failure and return false
        finally:
            fileobj.close()
    except:
        return False

    #all was well, so return True        
    return returnCode

def main(argv):
    usage='''Searches (possibly recursively) through a directory of files which match a given pattern to decorate the #if(def)'s.
    This is done by replacing all occurances of #endif and #else which do not have accompanying text with the text of the associated #if(def).
    '''
    parser = OptionParser(usage=usage)
    parser.add_option("-p", "--pattern",
                      action="store", type="string", dest="patternmatch",
                      default="*.c;*.h",
                      help="Define a pattern of files to look into. Default pattern: %default")
    parser.add_option("-r", "--recurse",
                      default=False,
                      action="store_true", dest="recurse",
                      help="Recursively look into subdirectories as well. Default: %default")
    parser.add_option("-c", "--comment",
                      default=False,
                      action="store_true", dest="usecomment",
                      help="Put comment chars around the inserted text. Default: %default")
    parser.add_option("-e","--errorfile",
                      default="errors.txt",
                      action="store", type="string", dest="errorfile",
                      help="Define the file to write the list of files which caused errors into.nA file is only created if any errors actually ocurred.n Default: %default in the current dir")
    parser.add_option("-d","--basedir",
                      default=getcwd(),
                      action="store", type="string", dest="basedir",
                      help="The base directory to start in. Default: The current dir")
    parser.add_option("-f","--outfile",
                      default="files.txt",
                      action="store", type="string", dest="goodfile",
                      help="Define the file to write the list of edited files into. Default: %default in the current dir")
    (options, args) = parser.parse_args()

    processFiles(options.basedir, options.patternmatch, options.errorfile, options.goodfile, options.usecomment, options.recurse)

if __name__ == "__main__":
    main(argv[1:])

Not the finest ever written, but it does the trick.

Leave a Reply

Your email address will not be published. Required fields are marked *