#!/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[\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()