X-Git-Url: https://git.distorted.org.uk/u/mdw/catacomb/blobdiff_plain/813390c45f438f411662b1a55678e63f11681eb4..7db733d4d0fd085313e4ae3d8ff9b7cc533616bb:/multigen diff --git a/multigen b/multigen new file mode 100755 index 0000000..2d626c2 --- /dev/null +++ b/multigen @@ -0,0 +1,406 @@ +#! @PYTHON@ +### +### Generate files by filling in simple templates +### +### (c) 2013 Straylight/Edgeware +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of Catacomb. +### +### Catacomb is free software; you can redistribute it and/or modify +### it under the terms of the GNU Library General Public License as +### published by the Free Software Foundation; either version 2 of the +### License, or (at your option) any later version. +### +### Catacomb is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU Library General Public License for more details. +### +### You should have received a copy of the GNU Library General Public +### License along with Catacomb; if not, write to the Free +### Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, +### MA 02111-1307, USA. + +from __future__ import with_statement + +import itertools as IT +import optparse as OP +import os as OS +import re as RX +from cStringIO import StringIO +from sys import argv, exit, stderr + +###-------------------------------------------------------------------------- +### Utilities. + +QUIS = OS.path.basename(argv[0]) + +def die(msg): + stderr.write('%s: %s\n' % (QUIS, msg)) + exit(1) + +def indexed(seq): + return IT.izip(IT.count(), seq) + +###-------------------------------------------------------------------------- +### Reading the input values. + +COLMAP = {} + +class Cursor (object): + def __init__(me, rel): + me._rel = rel + me._i = 0 + me._row = rel[0] + def step(me): + me._i += 1 + if me._i >= len(me._rel): + me._i = me._row = None + return False + me._row = me._rel[me._i] + return True + def reset(me): + me._i = 0 + me._row = me._rel[0] + def __getitem__(me, i): + return me._row[i] + def __repr__(me): + return '#' % (me._rel, me._i, me._row) + +class CursorSet (object): + def __init__(me): + me._map = {} + me._stack = [] + me._act = None + def push(me, rels): + cc = [] + rr = [] + for r in rels: + if r in me._map: continue + c = me._map[r] = Cursor(r) + rr.append(r) + cc.append(c) + me._stack.append((me._act, rr)) + me._act = cc + def step(me): + i = 0 + while i < len(me._act): + if me._act[i].step(): return True + if i >= len(me._act): return False + me._act[i].reset() + i += 1 + return False + def pop(me): + me._act, rels = me._stack.pop() + for r in rels: del me._map[r] + def get(me, rel, i): + return me._map[rel][i] + +class Relation (object): + def __init__(me, head): + me._head = head + me._rows = [] + for i, c in indexed(head): COLMAP[c] = me, i + def addrow(me, row): + if len(row) != len(me._head): + die("mismatch: row `%s' doesn't match heading `%s'" % + (', '.join(row), ', '.join(head))) + me._rows.append(row) + def __len__(me): + return len(me._rows) + def __getitem__(me, i): + return me._rows[i] + def __repr__(me): + return '#' % me._head + +def read_immediate(word): + head, rels = word.split('=', 1) + rel = Relation([c.strip() for c in head.split(',')]) + for row in rels.split(): rel.addrow([c.strip() for c in row.split(',')]) + +def read_file(spec): + file, head = spec.split(':', 1) + rel = Relation([c.strip() for c in head.split(',')]) + cols = [c.strip() for c in head.split(',')] + with open(file) as f: + for line in f: + line = line.strip() + if line.startswith('#') or line == '': continue + rel.addrow(line.split()) + +def read_thing(spec): + if spec.startswith('@'): read_file(spec[1:]) + else: read_immediate(spec) + +###-------------------------------------------------------------------------- +### Template structure. + +class BasicTemplate (object): + pass + +class LiteralTemplate (BasicTemplate): + def __init__(me, text, **kw): + super(LiteralTemplate, me).__init__(**kw) + me._text = text + def relations(me): + return set() + def subst(me, out, cs): + out.write(me._text) + def __repr__(me): + return '#' % me._text + +class TagTemplate (BasicTemplate): + def __init__(me, rel, i, op, **kw): + super(TagTemplate, me).__init__(**kw) + me._rel = rel + me._i = i + me._op = op + def relations(me): + return set([me._rel]) + def subst(me, out, cs): + val = cs.get(me._rel, me._i) + if me._op is not None: val = me._op(val) + out.write(val) + def __repr__(me): + return '#' % me._rel._head[me._i] + +class SequenceTemplate (BasicTemplate): + def __new__(cls, seq, **kw): + if len(seq) == 1: + return seq[0] + else: + me = super(SequenceTemplate, cls).__new__(cls, seq = seq, **kw) + tt = [] + cls = type(me) + for t in seq: + if isinstance(t, cls): tt += t._seq + else: tt.append(t) + me._seq = tt + return me + def __init__(me, seq, **kw): + super(SequenceTemplate, me).__init__(**kw) + def relations(me): + rr = set() + for t in me._seq: rr.update(t.relations()) + return rr + def subst(me, out, cs): + for t in me._seq: t.subst(out, cs) + def __repr__(me): + return '#' % me._seq + +class RepeatTemplate (BasicTemplate): + def __init__(me, sub): + me._sub = sub + def relations(me): + return set() + def subst(me, out, cs): + rr = me._sub.relations() + for r in rr: + if len(r) == 0: return + cs.push(rr) + while True: + me._sub.subst(out, cs) + if not cs.step(): break + cs.pop() + def __repr__(me): + return '#' % me._sub + +###-------------------------------------------------------------------------- +### Some slightly cheesy parsing machinery. + +class ParseState (object): + def __init__(me, file, text): + me._file = file + me._i = 0 + me._it = iter(text.splitlines(True)) + me.step() + def step(me): + me.curr = next(me._it, None) + if me.curr is not None: me._i += 1 + def error(me, msg): + die('%s:%d: %s' % (me._file, me._i, msg)) + +class token (object): + def __init__(me, name): + me._name = name + def __repr__(me): + return '#<%s>' % me._name + +EOF = token('eof') +END = token('end') + +R_SIMPLETAG = RX.compile(r'@ (\w+)', RX.VERBOSE) +R_COMPLEXTAG = RX.compile(r'@ { (\w+) ((?: : \w+)*) }', RX.VERBOSE) + +OPMAP = {} + +def defop(func): + name = func.func_name + if name.startswith('op_'): name = name[3:] + OPMAP[name] = func + return func + +@defop +def op_u(val): return val.upper() + +@defop +def op_l(val): return val.lower() + +R_NOTIDENT = RX.compile(r'[^a-zA-Z0-9_]+') +@defop +def op_c(val): return R_NOTIDENT.sub('_', val) + +def _pairify(val): + c = val.find('=') + if c >= 0: return val[:c], val[c + 1:] + else: return val, val + +@defop +def op_left(val): return _pairify(val)[0] +@defop +def op_right(val): return _pairify(val)[1] + +def parse_text(ps): + tt = [] + lit = StringIO() + def spill(): + l = lit.getvalue() + if l: tt.append(LiteralTemplate(l)) + lit.reset() + lit.truncate() + while True: + line = ps.curr + if line is None: break + elif line.startswith('%'): + if line.startswith('%#'): ps.step(); continue + elif line.startswith('%%'): line = line[1:] + else: break + i = 0 + while True: + j = line.find('@', i) + if j < 0: break + lit.write(line[i:j]) + m = R_SIMPLETAG.match(line, j) + if not m: m = R_COMPLEXTAG.match(line, j) + if not m: ps.error('invalid tag') + col = m.group(1) + try: rel, i = COLMAP[col] + except KeyError: ps.error("unknown column `%s'" % col) + wholeop = None + ops = m.lastindex >= 2 and m.group(2) + if ops: + for opname in ops[1:].split(':'): + try: op = OPMAP[opname] + except KeyError: ps.error("unknown operation `%s'" % opname) + if wholeop is None: wholeop = op + else: wholeop = (lambda f, g: lambda x: f(g(x)))(op, wholeop) + spill() + tt.append(TagTemplate(rel, i, wholeop)) + i = m.end() + lit.write(line[i:]) + ps.step() + spill() + return SequenceTemplate(tt) + +DIRECT = [] + +def direct(rx): + def _(func): + DIRECT.append((RX.compile(rx, RX.VERBOSE), func)) + return func + return _ + +def parse_template(ps): + while ps.curr is not None and ps.curr.startswith('%#'): ps.step() + if ps.curr is None: return EOF + elif ps.curr.startswith('%'): + if ps.curr.startswith('%%'): return parse_text(ps) + for rx, func in DIRECT: + line = ps.curr[1:].strip() + m = rx.match(line) + if m: + ps.step() + return func(ps, m) + ps.error("unrecognized directive") + else: + return parse_text(ps) + +def parse_templseq(ps, nestp): + tt = [] + while True: + t = parse_template(ps) + if t is END: + if nestp: break + else: ps.error("unexpected `end' directive") + elif t is EOF: + if nestp: ps.error("unexpected end of file") + else: break + tt.append(t) + return SequenceTemplate(tt) + +@direct(r'repeat') +def dir_repeat(ps, m): + return RepeatTemplate(parse_templseq(ps, True)) + +@direct(r'end') +def dir_end(ps, m): + return END + +def compile_template(file, text): + ps = ParseState(file, text) + t = parse_templseq(ps, False) + return t + +###-------------------------------------------------------------------------- +### Main code. + +op = OP.OptionParser( + description = 'Generates files by filling in simple templates', + usage = 'usage: %prog [-gl] FILE [COL,...=VAL,... ... | @FILE:COL,...] ...', + version = 'Catacomb version @VERSION@') +for short, long, kw in [ + ('-l', '--list', dict( + action = 'store_const', const = 'list', dest = 'mode', + help = 'list filenames generated')), + ('-g', '--generate', dict( + action = 'store', metavar = 'PATH', dest = 'input', + help = 'generate output (default)'))]: + op.add_option(short, long, **kw) +op.set_defaults(mode = 'gen') +opts, args = op.parse_args() + +if len(args) < 1: op.error('missing FILE') +filepat = args[0] +for rel in args[1:]: read_thing(rel) +filetempl = compile_template('', filepat) + +def filenames(filetempl): + cs = CursorSet() + rr = filetempl.relations() + for r in rr: + if not len(r): return + cs.push(rr) + while True: + out = StringIO() + filetempl.subst(out, cs) + yield out.getvalue(), cs + if not cs.step(): break + cs.pop() + +if opts.mode == 'list': + for file, cs in filenames(filetempl): print file +elif opts.mode == 'gen': + with open(opts.input) as f: + templ = RepeatTemplate(compile_template(opts.input, f.read())) + for file, cs in filenames(filetempl): + new = file + '.new' + with open(new, 'w') as out: + templ.subst(out, cs) + OS.rename(new, file) +else: + raise Exception, 'What am I doing here?' + +###----- That's all, folks --------------------------------------------------