#! @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 --------------------------------------------------