#!/usr/bin/env python3 # FileGenerator.py - implemented 2013 by Neil Hodgson neilh@scintilla.org # Released to the public domain. # Generate or regenerate source files based on comments in those files. # May be modified in-place or a template may be generated into a complete file. # Requires Python 2.7 or later # The files are copied to a string apart from sections between a # ++Autogenerated comment and a --Autogenerated comment which is # generated by the CopyWithInsertion function. After the whole string is # instantiated, it is compared with the target file and if different the file # is rewritten. from __future__ import with_statement import codecs, os, re, string, sys lineEnd = "\r\n" if sys.platform == "win32" else "\n" def UpdateFile(filename, updated): """ If the file contents are different to updated then copy updated into the file else leave alone so Mercurial and make don't treat it as modified. """ newOrChanged = "Changed" try: with codecs.open(filename, "r", "utf-8") as infile: original = infile.read() if updated == original: # Same as before so don't write return os.unlink(filename) except IOError: # File is not there yet newOrChanged = "New" with codecs.open(filename, "w", "utf-8") as outfile: outfile.write(updated) print("%s %s" % (newOrChanged, filename)) # Automatically generated sections contain start and end comments, # a definition line and the results. # The results are replaced by regenerating based on the definition line. # The definition line is a comment prefix followed by "**". # If there is a digit after the ** then this indicates which list to use # and the digit and next character are not part of the definition # Backslash is used as an escape within the definition line. # The part between \( and \) is repeated for each item in the list. # \* is replaced by each list item. \t, and \n are tab and newline. # If there is no definition line than the first list is copied verbatim. # If retainDefs then the comments controlling generation are copied. def CopyWithInsertion(input, commentPrefix, retainDefs, lists): copying = 1 generated = False listid = 0 output = [] for line in input.splitlines(0): isStartGenerated = line.lstrip().startswith(commentPrefix + "++Autogenerated") if copying and not isStartGenerated: output.append(line) if isStartGenerated: if retainDefs: output.append(line) copying = 0 generated = False elif not copying and not generated: # Generating if line.startswith(commentPrefix + "**"): # Pattern to transform input data if retainDefs: output.append(line) definition = line[len(commentPrefix + "**"):] if (commentPrefix == "" in definition): definition = definition.replace(" -->", "") listid = 0 if definition[0] in string.digits: listid = int(definition[:1]) definition = definition[2:] # Hide double slashes as a control character definition = definition.replace("\\\\", "\001") # Do some normal C style transforms definition = definition.replace("\\n", "\n") definition = definition.replace("\\t", "\t") # Get the doubled backslashes back as single backslashes definition = definition.replace("\001", "\\") startRepeat = definition.find("\\(") endRepeat = definition.find("\\)") intro = definition[:startRepeat] out = "" if intro.endswith("\n"): pos = 0 else: pos = len(intro) out += intro middle = definition[startRepeat+2:endRepeat] for i in lists[listid]: item = middle.replace("\\*", i) if pos and (pos + len(item) >= 80): out += "\\\n" pos = 0 out += item pos += len(item) if item.endswith("\n"): pos = 0 outro = definition[endRepeat+2:] out += outro out = out.replace("\n", lineEnd) # correct EOLs in generated content output.append(out) else: # Simple form with no rule to transform input output.extend(lists[0]) generated = True if line.lstrip().startswith(commentPrefix + "--Autogenerated") or \ line.lstrip().startswith(commentPrefix + "~~Autogenerated"): copying = 1 if retainDefs: output.append(line) output = [line.rstrip(" \t") for line in output] # trim trailing whitespace return lineEnd.join(output) + lineEnd def GenerateFile(inpath, outpath, commentPrefix, retainDefs, *lists): """Generate 'outpath' from 'inpath'. """ try: with codecs.open(inpath, "r", "UTF-8") as infile: original = infile.read() updated = CopyWithInsertion(original, commentPrefix, retainDefs, lists) UpdateFile(outpath, updated) except IOError: print("Can not open %s" % inpath) def Generate(inpath, outpath, commentPrefix, *lists): """Generate 'outpath' from 'inpath'. """ GenerateFile(inpath, outpath, commentPrefix, inpath == outpath, *lists) def Regenerate(filename, commentPrefix, *lists): """Regenerate the given file. """ Generate(filename, filename, commentPrefix, *lists) def UpdateLineInPlistFile(path, key, value): """Replace a single string value preceded by 'key' in an XML plist file. """ lines = [] keyCurrent = "" with codecs.open(path, "rb", "utf-8") as f: for l in f.readlines(): ls = l.strip() if ls.startswith(""): keyCurrent = ls.replace("", "").replace("", "") elif ls.startswith(""): if keyCurrent == key: start, tag, rest = l.partition("") _val, etag, end = rest.partition("") l = start + tag + value + etag + end lines.append(l) contents = "".join(lines) UpdateFile(path, contents) def UpdateLineInFile(path, linePrefix, lineReplace): lines = [] updated = False with codecs.open(path, "r", "utf-8") as f: for l in f.readlines(): l = l.rstrip() if not updated and l.startswith(linePrefix): lines.append(lineReplace) updated = True else: lines.append(l) if not updated: print(f"{path}:0: Can't find '{linePrefix}'") contents = lineEnd.join(lines) + lineEnd UpdateFile(path, contents) def ReadFileAsList(path): """Read all the lnes in the file and return as a list of strings without line ends. """ with codecs.open(path, "r", "utf-8") as f: return [l.rstrip('\n') for l in f] def UpdateFileFromLines(path, lines, lineEndToUse): """Join the lines with the lineEndToUse then update file if the result is different. """ contents = lineEndToUse.join(lines) + lineEndToUse UpdateFile(path, contents) def FindSectionInList(lines, markers): """Find a section defined by an initial start marker, an optional secondary marker and an end marker. The section is between the secondary/initial start and the end. Report as a slice object so the section can be extracted or replaced. Raises an exception if the markers can't be found. """ start = -1 end = -1 state = 0 for i, l in enumerate(lines): if markers[0] in l: if markers[1]: state = 1 else: start = i+1 state = 2 elif state == 1: if markers[1] in l: start = i+1 state = 2 elif state == 2: if markers[2] in l: end = i state = 3 # Check that section was found if start == -1: raise Exception("Could not find start marker(s) |" + markers[0] + "|" + markers[1] + "|") if end == -1: raise Exception("Could not find end marker " + markers[2]) return slice(start, end) def ReplaceREInFile(path, match, replace, count=1): with codecs.open(path, "r", "utf-8") as f: contents = f.read() contents = re.sub(match, replace, contents, count) UpdateFile(path, contents)