-#!/usr/bin/python
-
-class phrase_replacer:
- """ A simple replacement tool that honors some C/C++ syntax when replacing.
-
- This will take a particular phrase given by the user and find it in a set of
- documents. That phrase will be replaced when it appears completely, and is not
- in a C or C++ style comment (// or /* ... */). It also must be clear of any
- other alphanumeric pollution, and only be surrounded by white space or operation
- characters.
- """
-
- def __init__(self, argv):
- """ Initializes the class with a set of arguments to work with.
-
- The arguments need to be in the form described by print_instructions().
- """
- self.arguments = argv
- # we have three states for the processing: consuming normal code (not within a comment),
- # consuming a single line comment, and consuming a multi-line comment.
- self.EATING_NORMAL_TEXT = 0
- self.EATING_ONELINE_COMMENT = 1
- self.EATING_MULTILINE_COMMENT = 2
-
- def print_instructions(self):
- """ Shows the instructions for using this class. """
- print("""
-This script will replace all occurrences of a phrase you specify in a set of files. The
-replacement process will be careful about C and C++ syntax and will not replace occurrences
-within comments or which are not "complete" phrases (due to other alpha-numeric characters
-that abut the phrase). The arguments to the script are:
-
- {0}: PhraseToReplace ReplacementPhrase File1 [File2 ...]
-
-For example, if the phrase to replace is Goop, it will be replaced in these contexts:
- Goop[32]
- molo-Goop
- *Goop
-but it will not be found in these contexts:
- // doop de Goop
- rGoop
- Goop23
-""".format(self.arguments[0]))
-
- def validate_and_consume_command_line(self):
- """ Performs command line argument handling. """
- arg_count = len(self.arguments)
-# for i in range(1, arg_count):
-# print("i is {0}, arg is {1}".format(i, self.arguments[i]))
- # we need more than 2 arguments, since there needs to be at least one file also.
- if arg_count < 4:
- return False
- self.phrase_to_replace = self.arguments[1]
- self.replacement_bit = self.arguments[2]
- print("got phrase to replace: \'{0}\' and replacement: \'{1}\'".format(self.phrase_to_replace, self.replacement_bit))
- self.files = self.arguments[3:]
- return True
-
- def read_file_data(self, filename):
- """ loads the file into our memory buffer for processing. """
- try:
- our_file = open(filename, "rb")
- try:
- file_buffer = our_file.read()
- except IOError:
- print("There was an error reading the file {0}".format(filename))
- return False
- finally:
- our_file.close()
- except IOError:
- print("There was an error opening the file {0}".format(filename))
- return False
- self.file_lines = file_buffer.splitlines()
- return True
-
- def write_file_data(self, filename):
- """ takes the processed buffer and sends it back out to the filename. """
-# output_filename = filename + ".new" # safe testing version.
- output_filename = filename
- try:
- our_file = open(output_filename, "wb")
- try:
- file_buffer = our_file.write(self.processed_buffer)
- except IOError:
- print("There was an error writing the file {0}".format(output_filename))
- return False
- finally:
- our_file.close()
- except IOError:
- print("There was an error opening the file {0}".format(output_filename))
- return False
- return True
-
- def is_alphanumeric(self, check_char):
- """ given a character, this returns true if it's between a-z, A-Z or 0-9. """
- if (check_char[0] == "_"):
- return True
- if ( (check_char[0] <= "z") and (check_char[0] >= "a")):
- return True
- if ( (check_char[0] <= "Z") and (check_char[0] >= "A")):
- return True
- if ( (check_char[0] <= "9") and (check_char[0] >= "0")):
- return True
- return False
-
- def replace_within_string(self, fix_string):
- """ given a string to fix, this replaces all appropriate locations of the phrase. """
- indy = 0
-# print("got to replace within string")
- while (indy < len(fix_string)):
- # locate next occurrence of replacement text, if any.
- indy = fix_string.find(self.phrase_to_replace, indy)
-# print("find indy={0}".format(indy))
- if (indy > -1):
-# print("found occurrence of replacement string")
- # we found an occurrence, but we have to validate it's separated enough.
- char_before = "?" # simple default that won't fail our check.
- char_after = "?"
- if (indy > 0):
- char_before = fix_string[indy-1]
- if (indy + len(self.phrase_to_replace) < len(fix_string) - 1):
- char_after = fix_string[indy+len(self.phrase_to_replace)]
-# print("char before {0}, char after {1}".format(char_before, char_after))
- if (not self.is_alphanumeric(char_before) and not self.is_alphanumeric(char_after)):
- # this looks like a good candidate for replacement.
- fix_string = "{0}{1}{2}".format(fix_string[0:indy], self.replacement_bit, fix_string[indy+len(self.phrase_to_replace):])
-# print("changed string to: {0}".format(fix_string))
- else:
- break
- indy += 1 # no matches means we have to keep skipping forward.
- return fix_string # give back processed form.
-
- def emit_normal_accumulator(self):
- """ handle emission of a chunk of normal code (without comments). """
- # process the text to perform the replacement...
- self.normal_accumulator = self.replace_within_string(self.normal_accumulator)
- # then send the text into our main buffer; we're done looking at it.
- self.processed_buffer += self.normal_accumulator
- self.normal_accumulator = ""
-
- def emit_comment_accumulator(self):
- """ emits the piled up text for comments found in the code. """
- self.processed_buffer += self.comment_accumulator
- self.comment_accumulator = ""
-
- def process_file_data(self):
- """ iterates through the stored version of the file and replaces the phrase. """
- self.state = self.EATING_NORMAL_TEXT;
- # clear out any previously processed text.
- self.processed_buffer = "" # reset our new version of the file contents.
- self.normal_accumulator = ""
- self.comment_accumulator = ""
- # iterate through the file's lines.
- while (len(self.file_lines) > 0):
- # get the next line out of the input.
- next_line = self.file_lines[0]
- # drop that line from the remaining items.
- self.file_lines = self.file_lines[1:]
-# print("next line: {0}".format(next_line))
- # decide if we need a state transition.
- indy = 0
- if ((len(next_line) > 0) and (self.state == self.EATING_NORMAL_TEXT) and ('/' in next_line)):
- # loop to catch cases where multiple slashes are in line and one IS a comment.
- while (indy < len(next_line)):
- # locate next slash, if any.
- indy = next_line.find('/', indy)
- if (indy < 0):
- break
- if ((len(next_line) > indy + 1) and (next_line[indy + 1] == '/')):
- # switch states and handle any pent-up text.
- self.normal_accumulator += next_line[0:indy] # get last tidbit before comment start.
- next_line = next_line[indy:] # keep only the stuff starting at slash.
- self.state = self.EATING_ONELINE_COMMENT
-# print("state => oneline comment")
- self.emit_normal_accumulator()
- break
- if ((len(next_line) > indy + 1) and (next_line[indy + 1] == '*')):
- # switch states and deal with accumulated text.
- self.normal_accumulator += next_line[0:indy] # get last tidbit before comment start.
- next_line = next_line[indy:] # keep only the stuff starting at slash.
- self.state = self.EATING_MULTILINE_COMMENT
-# print("state => multiline comment")
- self.emit_normal_accumulator()
- break
- indy += 1 # no matches means we have to keep skipping forward.
-
- # now handle things appropriately for our current state.
- if (self.state == self.EATING_NORMAL_TEXT):
- # add the text to the normal accumulator.
-# print("would handle normal text")
- self.normal_accumulator += next_line + "\n"
- elif (self.state == self.EATING_ONELINE_COMMENT):
- # save the text in comment accumulator.
-# print("would handle oneline comment")
- self.comment_accumulator += next_line + "\n"
- self.emit_comment_accumulator()
- self.state = self.EATING_NORMAL_TEXT
- elif (self.state == self.EATING_MULTILINE_COMMENT):
- # save the text in comment accumulator.
-# print("would handle multiline comment")
- self.comment_accumulator += next_line + "\n"
- # check for whether the multi-line comment is completed on this line.
- if ("*/" in next_line):
-# print("found completion for multiline comment on line.")
- self.emit_comment_accumulator()
- self.state = self.EATING_NORMAL_TEXT
- # verify we're not in the wrong state still.
- if (self.state == self.EATING_MULTILINE_COMMENT):
- print("file seems to have unclosed multi-line comment.")
- # last step is to spit out whatever was trailing in the accumulator.
- self.emit_normal_accumulator()
- # if we got to here, we seem to have happily consumed the file.
- return True
-
- def replace_all_occurrences(self):
- """ Orchestrates the process of replacing the phrases. """
- # process our command line arguments to see what we need to do.
- try_command_line = self.validate_and_consume_command_line()
- if (try_command_line != True):
- print("failed to process the command line...\n")
- self.print_instructions()
- exit(1)
- # iterate through the list of files we were given and process them.
- for i in range(0, len(self.files)):
- print("file {0} is \'{1}\'".format(i, self.files[i]))
- worked = self.read_file_data(self.files[i])
- if (worked is False):
- print("skipping since file read failed on: {0}".format(self.files[i]))
- continue
-# print("{0} got file contents:\n{1}".format(self.files[i], self.file_lines))
- worked = self.process_file_data()
- if (worked is False):
- print("skipping, since processing failed on: {0}".format(self.files[i]))
- continue
- worked = self.write_file_data(self.files[i])
- if (worked is False):
- print("writing file back failed on: {0}".format(self.files[i]))
- print("finished processing all files.")
-
-
-if __name__ == "__main__":
- import sys
- slicer = phrase_replacer(sys.argv)
- slicer.replace_all_occurrences()
-
-##############
-
-# parking lot of things to do in future:
-
-#hmmm: actually sometimes one DOES want to replace within comments. argh.
-# make ignoring inside comments an optional thing. later.
-
-# hmmm: one little issue here is if the text to be replaced happens to reside on
-# the same line after a multi-line comment. we are okay with ignoring that
-# possibility for now since it seems brain-dead to write code that way.
-
-