330 lines
9.3 KiB
Python
330 lines
9.3 KiB
Python
|
#!/usr/bin/env python3
|
||
|
"""
|
||
|
takes templated file .xxx.src and produces .xxx file where .xxx is
|
||
|
.i or .c or .h, using the following template rules
|
||
|
|
||
|
/**begin repeat -- on a line by itself marks the start of a repeated code
|
||
|
segment
|
||
|
/**end repeat**/ -- on a line by itself marks it's end
|
||
|
|
||
|
After the /**begin repeat and before the */, all the named templates are placed
|
||
|
these should all have the same number of replacements
|
||
|
|
||
|
Repeat blocks can be nested, with each nested block labeled with its depth,
|
||
|
i.e.
|
||
|
/**begin repeat1
|
||
|
*....
|
||
|
*/
|
||
|
/**end repeat1**/
|
||
|
|
||
|
When using nested loops, you can optionally exclude particular
|
||
|
combinations of the variables using (inside the comment portion of the inner loop):
|
||
|
|
||
|
:exclude: var1=value1, var2=value2, ...
|
||
|
|
||
|
This will exclude the pattern where var1 is value1 and var2 is value2 when
|
||
|
the result is being generated.
|
||
|
|
||
|
|
||
|
In the main body each replace will use one entry from the list of named replacements
|
||
|
|
||
|
Note that all #..# forms in a block must have the same number of
|
||
|
comma-separated entries.
|
||
|
|
||
|
Example:
|
||
|
|
||
|
An input file containing
|
||
|
|
||
|
/**begin repeat
|
||
|
* #a = 1,2,3#
|
||
|
* #b = 1,2,3#
|
||
|
*/
|
||
|
|
||
|
/**begin repeat1
|
||
|
* #c = ted, jim#
|
||
|
*/
|
||
|
@a@, @b@, @c@
|
||
|
/**end repeat1**/
|
||
|
|
||
|
/**end repeat**/
|
||
|
|
||
|
produces
|
||
|
|
||
|
line 1 "template.c.src"
|
||
|
|
||
|
/*
|
||
|
*********************************************************************
|
||
|
** This file was autogenerated from a template DO NOT EDIT!!**
|
||
|
** Changes should be made to the original source (.src) file **
|
||
|
*********************************************************************
|
||
|
*/
|
||
|
|
||
|
#line 9
|
||
|
1, 1, ted
|
||
|
|
||
|
#line 9
|
||
|
1, 1, jim
|
||
|
|
||
|
#line 9
|
||
|
2, 2, ted
|
||
|
|
||
|
#line 9
|
||
|
2, 2, jim
|
||
|
|
||
|
#line 9
|
||
|
3, 3, ted
|
||
|
|
||
|
#line 9
|
||
|
3, 3, jim
|
||
|
|
||
|
"""
|
||
|
|
||
|
__all__ = ['process_str', 'process_file']
|
||
|
|
||
|
import os
|
||
|
import sys
|
||
|
import re
|
||
|
|
||
|
# names for replacement that are already global.
|
||
|
global_names = {}
|
||
|
|
||
|
# header placed at the front of head processed file
|
||
|
header =\
|
||
|
"""
|
||
|
/*
|
||
|
*****************************************************************************
|
||
|
** This file was autogenerated from a template DO NOT EDIT!!!! **
|
||
|
** Changes should be made to the original source (.src) file **
|
||
|
*****************************************************************************
|
||
|
*/
|
||
|
|
||
|
"""
|
||
|
# Parse string for repeat loops
|
||
|
def parse_structure(astr, level):
|
||
|
"""
|
||
|
The returned line number is from the beginning of the string, starting
|
||
|
at zero. Returns an empty list if no loops found.
|
||
|
|
||
|
"""
|
||
|
if level == 0 :
|
||
|
loopbeg = "/**begin repeat"
|
||
|
loopend = "/**end repeat**/"
|
||
|
else :
|
||
|
loopbeg = "/**begin repeat%d" % level
|
||
|
loopend = "/**end repeat%d**/" % level
|
||
|
|
||
|
ind = 0
|
||
|
line = 0
|
||
|
spanlist = []
|
||
|
while True:
|
||
|
start = astr.find(loopbeg, ind)
|
||
|
if start == -1:
|
||
|
break
|
||
|
start2 = astr.find("*/", start)
|
||
|
start2 = astr.find("\n", start2)
|
||
|
fini1 = astr.find(loopend, start2)
|
||
|
fini2 = astr.find("\n", fini1)
|
||
|
line += astr.count("\n", ind, start2+1)
|
||
|
spanlist.append((start, start2+1, fini1, fini2+1, line))
|
||
|
line += astr.count("\n", start2+1, fini2)
|
||
|
ind = fini2
|
||
|
spanlist.sort()
|
||
|
return spanlist
|
||
|
|
||
|
|
||
|
def paren_repl(obj):
|
||
|
torep = obj.group(1)
|
||
|
numrep = obj.group(2)
|
||
|
return ','.join([torep]*int(numrep))
|
||
|
|
||
|
parenrep = re.compile(r"\(([^)]*)\)\*(\d+)")
|
||
|
plainrep = re.compile(r"([^*]+)\*(\d+)")
|
||
|
def parse_values(astr):
|
||
|
# replaces all occurrences of '(a,b,c)*4' in astr
|
||
|
# with 'a,b,c,a,b,c,a,b,c,a,b,c'. Empty braces generate
|
||
|
# empty values, i.e., ()*4 yields ',,,'. The result is
|
||
|
# split at ',' and a list of values returned.
|
||
|
astr = parenrep.sub(paren_repl, astr)
|
||
|
# replaces occurrences of xxx*3 with xxx, xxx, xxx
|
||
|
astr = ','.join([plainrep.sub(paren_repl, x.strip())
|
||
|
for x in astr.split(',')])
|
||
|
return astr.split(',')
|
||
|
|
||
|
|
||
|
stripast = re.compile(r"\n\s*\*?")
|
||
|
named_re = re.compile(r"#\s*(\w*)\s*=([^#]*)#")
|
||
|
exclude_vars_re = re.compile(r"(\w*)=(\w*)")
|
||
|
exclude_re = re.compile(":exclude:")
|
||
|
def parse_loop_header(loophead) :
|
||
|
"""Find all named replacements in the header
|
||
|
|
||
|
Returns a list of dictionaries, one for each loop iteration,
|
||
|
where each key is a name to be substituted and the corresponding
|
||
|
value is the replacement string.
|
||
|
|
||
|
Also return a list of exclusions. The exclusions are dictionaries
|
||
|
of key value pairs. There can be more than one exclusion.
|
||
|
[{'var1':'value1', 'var2', 'value2'[,...]}, ...]
|
||
|
|
||
|
"""
|
||
|
# Strip out '\n' and leading '*', if any, in continuation lines.
|
||
|
# This should not effect code previous to this change as
|
||
|
# continuation lines were not allowed.
|
||
|
loophead = stripast.sub("", loophead)
|
||
|
# parse out the names and lists of values
|
||
|
names = []
|
||
|
reps = named_re.findall(loophead)
|
||
|
nsub = None
|
||
|
for rep in reps:
|
||
|
name = rep[0]
|
||
|
vals = parse_values(rep[1])
|
||
|
size = len(vals)
|
||
|
if nsub is None :
|
||
|
nsub = size
|
||
|
elif nsub != size :
|
||
|
msg = "Mismatch in number of values, %d != %d\n%s = %s"
|
||
|
raise ValueError(msg % (nsub, size, name, vals))
|
||
|
names.append((name, vals))
|
||
|
|
||
|
|
||
|
# Find any exclude variables
|
||
|
excludes = []
|
||
|
|
||
|
for obj in exclude_re.finditer(loophead):
|
||
|
span = obj.span()
|
||
|
# find next newline
|
||
|
endline = loophead.find('\n', span[1])
|
||
|
substr = loophead[span[1]:endline]
|
||
|
ex_names = exclude_vars_re.findall(substr)
|
||
|
excludes.append(dict(ex_names))
|
||
|
|
||
|
# generate list of dictionaries, one for each template iteration
|
||
|
dlist = []
|
||
|
if nsub is None :
|
||
|
raise ValueError("No substitution variables found")
|
||
|
for i in range(nsub):
|
||
|
tmp = {name: vals[i] for name, vals in names}
|
||
|
dlist.append(tmp)
|
||
|
return dlist
|
||
|
|
||
|
replace_re = re.compile(r"@(\w+)@")
|
||
|
def parse_string(astr, env, level, line) :
|
||
|
lineno = "#line %d\n" % line
|
||
|
|
||
|
# local function for string replacement, uses env
|
||
|
def replace(match):
|
||
|
name = match.group(1)
|
||
|
try :
|
||
|
val = env[name]
|
||
|
except KeyError:
|
||
|
msg = 'line %d: no definition of key "%s"'%(line, name)
|
||
|
raise ValueError(msg) from None
|
||
|
return val
|
||
|
|
||
|
code = [lineno]
|
||
|
struct = parse_structure(astr, level)
|
||
|
if struct :
|
||
|
# recurse over inner loops
|
||
|
oldend = 0
|
||
|
newlevel = level + 1
|
||
|
for sub in struct:
|
||
|
pref = astr[oldend:sub[0]]
|
||
|
head = astr[sub[0]:sub[1]]
|
||
|
text = astr[sub[1]:sub[2]]
|
||
|
oldend = sub[3]
|
||
|
newline = line + sub[4]
|
||
|
code.append(replace_re.sub(replace, pref))
|
||
|
try :
|
||
|
envlist = parse_loop_header(head)
|
||
|
except ValueError as e:
|
||
|
msg = "line %d: %s" % (newline, e)
|
||
|
raise ValueError(msg)
|
||
|
for newenv in envlist :
|
||
|
newenv.update(env)
|
||
|
newcode = parse_string(text, newenv, newlevel, newline)
|
||
|
code.extend(newcode)
|
||
|
suff = astr[oldend:]
|
||
|
code.append(replace_re.sub(replace, suff))
|
||
|
else :
|
||
|
# replace keys
|
||
|
code.append(replace_re.sub(replace, astr))
|
||
|
code.append('\n')
|
||
|
return ''.join(code)
|
||
|
|
||
|
def process_str(astr):
|
||
|
code = [header]
|
||
|
code.extend(parse_string(astr, global_names, 0, 1))
|
||
|
return ''.join(code)
|
||
|
|
||
|
|
||
|
include_src_re = re.compile(r"(\n|\A)#include\s*['\"]"
|
||
|
r"(?P<name>[\w\d./\\]+[.]src)['\"]", re.I)
|
||
|
|
||
|
def resolve_includes(source):
|
||
|
d = os.path.dirname(source)
|
||
|
with open(source) as fid:
|
||
|
lines = []
|
||
|
for line in fid:
|
||
|
m = include_src_re.match(line)
|
||
|
if m:
|
||
|
fn = m.group('name')
|
||
|
if not os.path.isabs(fn):
|
||
|
fn = os.path.join(d, fn)
|
||
|
if os.path.isfile(fn):
|
||
|
lines.extend(resolve_includes(fn))
|
||
|
else:
|
||
|
lines.append(line)
|
||
|
else:
|
||
|
lines.append(line)
|
||
|
return lines
|
||
|
|
||
|
def process_file(source):
|
||
|
lines = resolve_includes(source)
|
||
|
sourcefile = os.path.normcase(source).replace("\\", "\\\\")
|
||
|
try:
|
||
|
code = process_str(''.join(lines))
|
||
|
except ValueError as e:
|
||
|
raise ValueError('In "%s" loop at %s' % (sourcefile, e)) from None
|
||
|
return '#line 1 "%s"\n%s' % (sourcefile, code)
|
||
|
|
||
|
|
||
|
def unique_key(adict):
|
||
|
# this obtains a unique key given a dictionary
|
||
|
# currently it works by appending together n of the letters of the
|
||
|
# current keys and increasing n until a unique key is found
|
||
|
# -- not particularly quick
|
||
|
allkeys = list(adict.keys())
|
||
|
done = False
|
||
|
n = 1
|
||
|
while not done:
|
||
|
newkey = "".join([x[:n] for x in allkeys])
|
||
|
if newkey in allkeys:
|
||
|
n += 1
|
||
|
else:
|
||
|
done = True
|
||
|
return newkey
|
||
|
|
||
|
|
||
|
def main():
|
||
|
try:
|
||
|
file = sys.argv[1]
|
||
|
except IndexError:
|
||
|
fid = sys.stdin
|
||
|
outfile = sys.stdout
|
||
|
else:
|
||
|
fid = open(file, 'r')
|
||
|
(base, ext) = os.path.splitext(file)
|
||
|
newname = base
|
||
|
outfile = open(newname, 'w')
|
||
|
|
||
|
allstr = fid.read()
|
||
|
try:
|
||
|
writestr = process_str(allstr)
|
||
|
except ValueError as e:
|
||
|
raise ValueError("In %s loop at %s" % (file, e)) from None
|
||
|
|
||
|
outfile.write(writestr)
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|