X-Git-Url: https://git.distorted.org.uk/u/mdw/catacomb/blobdiff_plain/ba6e6b64033b1f9de49feccb5c9cd438354481f7..0f00dc4c8eb47e67bc0f148c2dd109f73a451e0a:/multigen diff --git a/multigen b/multigen deleted file mode 100755 index 2d626c2..0000000 --- a/multigen +++ /dev/null @@ -1,406 +0,0 @@ -#! @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 --------------------------------------------------