3 ### Generate files by filling in simple templates
5 ### (c) 2013 Straylight/Edgeware
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of Catacomb.
12 ### Catacomb is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU Library General Public License as
14 ### published by the Free Software Foundation; either version 2 of the
15 ### License, or (at your option) any later version.
17 ### Catacomb is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU Library General Public License for more details.
22 ### You should have received a copy of the GNU Library General Public
23 ### License along with Catacomb; if not, write to the Free
24 ### Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
25 ### MA 02111-1307, USA.
27 from __future__ import with_statement
29 import itertools as IT
33 from cStringIO import StringIO
34 from sys import argv, exit, stderr
36 ###--------------------------------------------------------------------------
39 QUIS = OS.path.basename(argv[0])
42 stderr.write('%s: %s\n' % (QUIS, msg))
46 return IT.izip(IT.count(), seq)
48 ###--------------------------------------------------------------------------
49 ### Reading the input values.
53 class Cursor (object):
54 def __init__(me, rel):
60 if me._i >= len(me._rel):
61 me._i = me._row = None
63 me._row = me._rel[me._i]
68 def __getitem__(me, i):
71 return '#<Cursor %r[%d] = %r>' % (me._rel, me._i, me._row)
73 class CursorSet (object):
82 if r in me._map: continue
83 c = me._map[r] = Cursor(r)
86 me._stack.append((me._act, rr))
90 while i < len(me._act):
91 if me._act[i].step(): return True
92 if i >= len(me._act): return False
97 me._act, rels = me._stack.pop()
98 for r in rels: del me._map[r]
100 return me._map[rel][i]
102 class Relation (object):
103 def __init__(me, head):
106 for i, c in indexed(head): COLMAP[c] = me, i
108 if len(row) != len(me._head):
109 die("mismatch: row `%s' doesn't match heading `%s'" %
110 (', '.join(row), ', '.join(head)))
114 def __getitem__(me, i):
117 return '#<Relation %r>' % me._head
119 def read_immediate(word):
120 head, rels = word.split('=', 1)
121 rel = Relation([c.strip() for c in head.split(',')])
122 for row in rels.split(): rel.addrow([c.strip() for c in row.split(',')])
125 file, head = spec.split(':', 1)
126 rel = Relation([c.strip() for c in head.split(',')])
127 cols = [c.strip() for c in head.split(',')]
128 with open(file) as f:
131 if line.startswith('#') or line == '': continue
132 rel.addrow(line.split())
134 def read_thing(spec):
135 if spec.startswith('@'): read_file(spec[1:])
136 else: read_immediate(spec)
138 ###--------------------------------------------------------------------------
139 ### Template structure.
141 class BasicTemplate (object):
144 class LiteralTemplate (BasicTemplate):
145 def __init__(me, text, **kw):
146 super(LiteralTemplate, me).__init__(**kw)
150 def subst(me, out, cs):
153 return '#<LiteralTemplate %r>' % me._text
155 class TagTemplate (BasicTemplate):
156 def __init__(me, rel, i, op, **kw):
157 super(TagTemplate, me).__init__(**kw)
162 return set([me._rel])
163 def subst(me, out, cs):
164 val = cs.get(me._rel, me._i)
165 if me._op is not None: val = me._op(val)
168 return '#<TagTemplate %s>' % me._rel._head[me._i]
170 class SequenceTemplate (BasicTemplate):
171 def __new__(cls, seq, **kw):
175 me = super(SequenceTemplate, cls).__new__(cls, seq = seq, **kw)
179 if isinstance(t, cls): tt += t._seq
183 def __init__(me, seq, **kw):
184 super(SequenceTemplate, me).__init__(**kw)
187 for t in me._seq: rr.update(t.relations())
189 def subst(me, out, cs):
190 for t in me._seq: t.subst(out, cs)
192 return '#<SequenceTemplate %r>' % me._seq
194 class RepeatTemplate (BasicTemplate):
195 def __init__(me, sub):
199 def subst(me, out, cs):
200 rr = me._sub.relations()
202 if len(r) == 0: return
205 me._sub.subst(out, cs)
206 if not cs.step(): break
209 return '#<RepeatTemplate %r>' % me._sub
211 ###--------------------------------------------------------------------------
212 ### Some slightly cheesy parsing machinery.
214 class ParseState (object):
215 def __init__(me, file, text):
218 me._it = iter(text.splitlines(True))
221 me.curr = next(me._it, None)
222 if me.curr is not None: me._i += 1
224 die('%s:%d: %s' % (me._file, me._i, msg))
226 class token (object):
227 def __init__(me, name):
230 return '#<%s>' % me._name
235 R_SIMPLETAG = RX.compile(r'@ (\w+)', RX.VERBOSE)
236 R_COMPLEXTAG = RX.compile(r'@ { (\w+) ((?: : \w+)*) }', RX.VERBOSE)
241 name = func.func_name
242 if name.startswith('op_'): name = name[3:]
247 def op_u(val): return val.upper()
250 def op_l(val): return val.lower()
252 R_NOTIDENT = RX.compile(r'[^a-zA-Z0-9_]+')
254 def op_c(val): return R_NOTIDENT.sub('_', val)
258 if c >= 0: return val[:c], val[c + 1:]
259 else: return val, val
262 def op_left(val): return _pairify(val)[0]
264 def op_right(val): return _pairify(val)[1]
271 if l: tt.append(LiteralTemplate(l))
276 if line is None: break
277 elif line.startswith('%'):
278 if line.startswith('%#'): ps.step(); continue
279 elif line.startswith('%%'): line = line[1:]
283 j = line.find('@', i)
286 m = R_SIMPLETAG.match(line, j)
287 if not m: m = R_COMPLEXTAG.match(line, j)
288 if not m: ps.error('invalid tag')
290 try: rel, i = COLMAP[col]
291 except KeyError: ps.error("unknown column `%s'" % col)
293 ops = m.lastindex >= 2 and m.group(2)
295 for opname in ops[1:].split(':'):
296 try: op = OPMAP[opname]
297 except KeyError: ps.error("unknown operation `%s'" % opname)
298 if wholeop is None: wholeop = op
299 else: wholeop = (lambda f, g: lambda x: f(g(x)))(op, wholeop)
301 tt.append(TagTemplate(rel, i, wholeop))
306 return SequenceTemplate(tt)
312 DIRECT.append((RX.compile(rx, RX.VERBOSE), func))
316 def parse_template(ps):
317 while ps.curr is not None and ps.curr.startswith('%#'): ps.step()
318 if ps.curr is None: return EOF
319 elif ps.curr.startswith('%'):
320 if ps.curr.startswith('%%'): return parse_text(ps)
321 for rx, func in DIRECT:
322 line = ps.curr[1:].strip()
327 ps.error("unrecognized directive")
329 return parse_text(ps)
331 def parse_templseq(ps, nestp):
334 t = parse_template(ps)
337 else: ps.error("unexpected `end' directive")
339 if nestp: ps.error("unexpected end of file")
342 return SequenceTemplate(tt)
345 def dir_repeat(ps, m):
346 return RepeatTemplate(parse_templseq(ps, True))
352 def compile_template(file, text):
353 ps = ParseState(file, text)
354 t = parse_templseq(ps, False)
357 ###--------------------------------------------------------------------------
360 op = OP.OptionParser(
361 description = 'Generates files by filling in simple templates',
362 usage = 'usage: %prog [-gl] FILE [COL,...=VAL,... ... | @FILE:COL,...] ...',
363 version = 'Catacomb version @VERSION@')
364 for short, long, kw in [
365 ('-l', '--list', dict(
366 action = 'store_const', const = 'list', dest = 'mode',
367 help = 'list filenames generated')),
368 ('-g', '--generate', dict(
369 action = 'store', metavar = 'PATH', dest = 'input',
370 help = 'generate output (default)'))]:
371 op.add_option(short, long, **kw)
372 op.set_defaults(mode = 'gen')
373 opts, args = op.parse_args()
375 if len(args) < 1: op.error('missing FILE')
377 for rel in args[1:]: read_thing(rel)
378 filetempl = compile_template('<output>', filepat)
380 def filenames(filetempl):
382 rr = filetempl.relations()
384 if not len(r): return
388 filetempl.subst(out, cs)
389 yield out.getvalue(), cs
390 if not cs.step(): break
393 if opts.mode == 'list':
394 for file, cs in filenames(filetempl): print file
395 elif opts.mode == 'gen':
396 with open(opts.input) as f:
397 templ = RepeatTemplate(compile_template(opts.input, f.read()))
398 for file, cs in filenames(filetempl):
400 with open(new, 'w') as out:
404 raise Exception, 'What am I doing here?'
406 ###----- That's all, folks --------------------------------------------------