224 lines
8.9 KiB
Python
224 lines
8.9 KiB
Python
#!/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 == "<!--") and (" -->" 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("<key>"):
|
|
keyCurrent = ls.replace("<key>", "").replace("</key>", "")
|
|
elif ls.startswith("<string>"):
|
|
if keyCurrent == key:
|
|
start, tag, rest = l.partition("<string>")
|
|
_val, etag, end = rest.partition("</string>")
|
|
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)
|