+++ /dev/null
-#! @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 '#<Cursor %r[%d] = %r>' % (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 '#<Relation %r>' % 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 '#<LiteralTemplate %r>' % 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 '#<TagTemplate %s>' % 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 '#<SequenceTemplate %r>' % 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 '#<RepeatTemplate %r>' % 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('<output>', 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 --------------------------------------------------