|
| 1 | +#!/usr/bin/env python |
| 2 | +# FileGenerator.py - implemented 2013 by Neil Hodgson [email protected] |
| 3 | +# Released to the public domain. |
| 4 | + |
| 5 | +# Generate or regenerate source files based on comments in those files. |
| 6 | +# May be modified in-place or a template may be generated into a complete file. |
| 7 | +# Requires Python 2.5 or later |
| 8 | +# The files are copied to a string apart from sections between a |
| 9 | +# ++Autogenerated comment and a --Autogenerated comment which is |
| 10 | +# generated by the CopyWithInsertion function. After the whole string is |
| 11 | +# instantiated, it is compared with the target file and if different the file |
| 12 | +# is rewritten. |
| 13 | + |
| 14 | +from __future__ import with_statement |
| 15 | + |
| 16 | +import codecs, os, re, string, sys |
| 17 | + |
| 18 | +lineEnd = "\r\n" if sys.platform == "win32" else "\n" |
| 19 | + |
| 20 | +def UpdateFile(filename, updated): |
| 21 | + """ If the file contents are different to updated then copy updated into the |
| 22 | + file else leave alone so Mercurial and make don't treat it as modified. """ |
| 23 | + newOrChanged = "Changed" |
| 24 | + try: |
| 25 | + with codecs.open(filename, "r", "utf-8") as infile: |
| 26 | + original = infile.read() |
| 27 | + if updated == original: |
| 28 | + # Same as before so don't write |
| 29 | + return |
| 30 | + os.unlink(filename) |
| 31 | + except IOError: # File is not there yet |
| 32 | + newOrChanged = "New" |
| 33 | + with codecs.open(filename, "w", "utf-8") as outfile: |
| 34 | + outfile.write(updated) |
| 35 | + print("%s %s" % (newOrChanged, filename)) |
| 36 | + |
| 37 | +# Automatically generated sections contain start and end comments, |
| 38 | +# a definition line and the results. |
| 39 | +# The results are replaced by regenerating based on the definition line. |
| 40 | +# The definition line is a comment prefix followed by "**". |
| 41 | +# If there is a digit after the ** then this indicates which list to use |
| 42 | +# and the digit and next character are not part of the definition |
| 43 | +# Backslash is used as an escape within the definition line. |
| 44 | +# The part between \( and \) is repeated for each item in the list. |
| 45 | +# \* is replaced by each list item. \t, and \n are tab and newline. |
| 46 | +# If there is no definition line than the first list is copied verbatim. |
| 47 | +# If retainDefs then the comments controlling generation are copied. |
| 48 | +def CopyWithInsertion(input, commentPrefix, retainDefs, lists): |
| 49 | + copying = 1 |
| 50 | + generated = False |
| 51 | + listid = 0 |
| 52 | + output = [] |
| 53 | + for line in input.splitlines(0): |
| 54 | + isStartGenerated = line.lstrip().startswith(commentPrefix + "++Autogenerated") |
| 55 | + if copying and not isStartGenerated: |
| 56 | + output.append(line) |
| 57 | + if isStartGenerated: |
| 58 | + if retainDefs: |
| 59 | + output.append(line) |
| 60 | + copying = 0 |
| 61 | + generated = False |
| 62 | + elif not copying and not generated: |
| 63 | + # Generating |
| 64 | + if line.startswith(commentPrefix + "**"): |
| 65 | + # Pattern to transform input data |
| 66 | + if retainDefs: |
| 67 | + output.append(line) |
| 68 | + definition = line[len(commentPrefix + "**"):] |
| 69 | + if (commentPrefix == "<!--") and (" -->" in definition): |
| 70 | + definition = definition.replace(" -->", "") |
| 71 | + listid = 0 |
| 72 | + if definition[0] in string.digits: |
| 73 | + listid = int(definition[:1]) |
| 74 | + definition = definition[2:] |
| 75 | + # Hide double slashes as a control character |
| 76 | + definition = definition.replace("\\\\", "\001") |
| 77 | + # Do some normal C style transforms |
| 78 | + definition = definition.replace("\\n", "\n") |
| 79 | + definition = definition.replace("\\t", "\t") |
| 80 | + # Get the doubled backslashes back as single backslashes |
| 81 | + definition = definition.replace("\001", "\\") |
| 82 | + startRepeat = definition.find("\\(") |
| 83 | + endRepeat = definition.find("\\)") |
| 84 | + intro = definition[:startRepeat] |
| 85 | + out = "" |
| 86 | + if intro.endswith("\n"): |
| 87 | + pos = 0 |
| 88 | + else: |
| 89 | + pos = len(intro) |
| 90 | + out += intro |
| 91 | + middle = definition[startRepeat+2:endRepeat] |
| 92 | + for i in lists[listid]: |
| 93 | + item = middle.replace("\\*", i) |
| 94 | + if pos and (pos + len(item) >= 80): |
| 95 | + out += "\\\n" |
| 96 | + pos = 0 |
| 97 | + out += item |
| 98 | + pos += len(item) |
| 99 | + if item.endswith("\n"): |
| 100 | + pos = 0 |
| 101 | + outro = definition[endRepeat+2:] |
| 102 | + out += outro |
| 103 | + out = out.replace("\n", lineEnd) # correct EOLs in generated content |
| 104 | + output.append(out) |
| 105 | + else: |
| 106 | + # Simple form with no rule to transform input |
| 107 | + output.extend(lists[0]) |
| 108 | + generated = True |
| 109 | + if line.lstrip().startswith(commentPrefix + "--Autogenerated") or \ |
| 110 | + line.lstrip().startswith(commentPrefix + "~~Autogenerated"): |
| 111 | + copying = 1 |
| 112 | + if retainDefs: |
| 113 | + output.append(line) |
| 114 | + #output = [line.rstrip(" \t") for line in output] # trim trailing whitespace |
| 115 | + return lineEnd.join(output) + lineEnd |
| 116 | + |
| 117 | +def GenerateFile(inpath, outpath, commentPrefix, retainDefs, *lists): |
| 118 | + """Generate 'outpath' from 'inpath'. |
| 119 | + """ |
| 120 | + |
| 121 | + try: |
| 122 | + with codecs.open(inpath, "r", "UTF-8") as infile: |
| 123 | + original = infile.read() |
| 124 | + updated = CopyWithInsertion(original, commentPrefix, |
| 125 | + retainDefs, lists) |
| 126 | + UpdateFile(outpath, updated) |
| 127 | + except IOError: |
| 128 | + print("Can not open %s" % inpath) |
| 129 | + |
| 130 | +def Generate(inpath, outpath, commentPrefix, *lists): |
| 131 | + """Generate 'outpath' from 'inpath'. |
| 132 | + """ |
| 133 | + GenerateFile(inpath, outpath, commentPrefix, inpath == outpath, *lists) |
| 134 | + |
| 135 | +def Regenerate(filename, commentPrefix, *lists): |
| 136 | + """Regenerate the given file. |
| 137 | + """ |
| 138 | + Generate(filename, filename, commentPrefix, *lists) |
| 139 | + |
| 140 | +def UpdateLineInPlistFile(path, key, value): |
| 141 | + """Replace a single string value preceded by 'key' in an XML plist file. |
| 142 | + """ |
| 143 | + lines = [] |
| 144 | + keyCurrent = "" |
| 145 | + with codecs.open(path, "rb", "utf-8") as f: |
| 146 | + for l in f.readlines(): |
| 147 | + ls = l.strip() |
| 148 | + if ls.startswith("<key>"): |
| 149 | + keyCurrent = ls.replace("<key>", "").replace("</key>", "") |
| 150 | + elif ls.startswith("<string>"): |
| 151 | + if keyCurrent == key: |
| 152 | + start, tag, rest = l.partition("<string>") |
| 153 | + val, etag, end = rest.partition("</string>") |
| 154 | + l = start + tag + value + etag + end |
| 155 | + lines.append(l) |
| 156 | + contents = "".join(lines) |
| 157 | + UpdateFile(path, contents) |
| 158 | + |
| 159 | +def UpdateLineInFile(path, linePrefix, lineReplace): |
| 160 | + lines = [] |
| 161 | + updated = False |
| 162 | + with codecs.open(path, "r", "utf-8") as f: |
| 163 | + for l in f.readlines(): |
| 164 | + l = l.rstrip() |
| 165 | + if not updated and l.startswith(linePrefix): |
| 166 | + lines.append(lineReplace) |
| 167 | + updated = True |
| 168 | + else: |
| 169 | + lines.append(l) |
| 170 | + contents = lineEnd.join(lines) + lineEnd |
| 171 | + UpdateFile(path, contents) |
| 172 | + |
| 173 | +def ReplaceREInFile(path, match, replace): |
| 174 | + with codecs.open(path, "r", "utf-8") as f: |
| 175 | + contents = f.read() |
| 176 | + contents = re.sub(match, replace, contents) |
| 177 | + UpdateFile(path, contents) |
0 commit comments