symm/multigen: Fix for Python 2.5.
[catacomb] / symm / multigen
1 #! @PYTHON@
2 ###
3 ### Generate files by filling in simple templates
4 ###
5 ### (c) 2013 Straylight/Edgeware
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of Catacomb.
11 ###
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.
16 ###
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.
21 ###
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.
26
27 from __future__ import with_statement
28
29 import itertools as IT
30 import optparse as OP
31 import os as OS
32 import re as RX
33 from cStringIO import StringIO
34 from sys import argv, exit, stderr
35
36 ###--------------------------------------------------------------------------
37 ### Utilities.
38
39 QUIS = OS.path.basename(argv[0])
40
41 def die(msg):
42 stderr.write('%s: %s\n' % (QUIS, msg))
43 exit(1)
44
45 def indexed(seq):
46 return IT.izip(IT.count(), seq)
47
48 ###--------------------------------------------------------------------------
49 ### Reading the input values.
50
51 COLMAP = {}
52
53 class Cursor (object):
54 def __init__(me, rel):
55 me._rel = rel
56 me._i = 0
57 me._row = rel[0]
58 def step(me):
59 me._i += 1
60 if me._i >= len(me._rel):
61 me._i = me._row = None
62 return False
63 me._row = me._rel[me._i]
64 return True
65 def reset(me):
66 me._i = 0
67 me._row = me._rel[0]
68 def __getitem__(me, i):
69 return me._row[i]
70 def __repr__(me):
71 return '#<Cursor %r[%d] = %r>' % (me._rel, me._i, me._row)
72
73 class CursorSet (object):
74 def __init__(me):
75 me._map = {}
76 me._stack = []
77 me._act = None
78 def push(me, rels):
79 cc = []
80 rr = []
81 for r in rels:
82 if r in me._map: continue
83 c = me._map[r] = Cursor(r)
84 rr.append(r)
85 cc.append(c)
86 me._stack.append((me._act, rr))
87 me._act = cc
88 def step(me):
89 i = 0
90 while i < len(me._act):
91 if me._act[i].step(): return True
92 if i >= len(me._act): return False
93 me._act[i].reset()
94 i += 1
95 return False
96 def pop(me):
97 me._act, rels = me._stack.pop()
98 for r in rels: del me._map[r]
99 def get(me, rel, i):
100 return me._map[rel][i]
101
102 class Relation (object):
103 def __init__(me, head):
104 me._head = head
105 me._rows = []
106 for i, c in indexed(head): COLMAP[c] = me, i
107 def addrow(me, row):
108 if len(row) != len(me._head):
109 die("mismatch: row `%s' doesn't match heading `%s'" %
110 (', '.join(row), ', '.join(head)))
111 me._rows.append(row)
112 def __len__(me):
113 return len(me._rows)
114 def __getitem__(me, i):
115 return me._rows[i]
116 def __repr__(me):
117 return '#<Relation %r>' % me._head
118
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(',')])
123
124 def read_file(spec):
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:
129 for line in f:
130 line = line.strip()
131 if line.startswith('#') or line == '': continue
132 rel.addrow(line.split())
133
134 def read_thing(spec):
135 if spec.startswith('@'): read_file(spec[1:])
136 else: read_immediate(spec)
137
138 ###--------------------------------------------------------------------------
139 ### Template structure.
140
141 class BasicTemplate (object):
142 pass
143
144 class LiteralTemplate (BasicTemplate):
145 def __init__(me, text, **kw):
146 super(LiteralTemplate, me).__init__(**kw)
147 me._text = text
148 def relations(me):
149 return set()
150 def subst(me, out, cs):
151 out.write(me._text)
152 def __repr__(me):
153 return '#<LiteralTemplate %r>' % me._text
154
155 class TagTemplate (BasicTemplate):
156 def __init__(me, rel, i, op, **kw):
157 super(TagTemplate, me).__init__(**kw)
158 me._rel = rel
159 me._i = i
160 me._op = op
161 def relations(me):
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)
166 out.write(val)
167 def __repr__(me):
168 return '#<TagTemplate %s>' % me._rel._head[me._i]
169
170 class SequenceTemplate (BasicTemplate):
171 def __new__(cls, seq, **kw):
172 if len(seq) == 1:
173 return seq[0]
174 else:
175 me = super(SequenceTemplate, cls).__new__(cls, seq = seq, **kw)
176 tt = []
177 cls = type(me)
178 for t in seq:
179 if isinstance(t, cls): tt += t._seq
180 else: tt.append(t)
181 me._seq = tt
182 return me
183 def __init__(me, seq, **kw):
184 super(SequenceTemplate, me).__init__(**kw)
185 def relations(me):
186 rr = set()
187 for t in me._seq: rr.update(t.relations())
188 return rr
189 def subst(me, out, cs):
190 for t in me._seq: t.subst(out, cs)
191 def __repr__(me):
192 return '#<SequenceTemplate %r>' % me._seq
193
194 class RepeatTemplate (BasicTemplate):
195 def __init__(me, sub):
196 me._sub = sub
197 def relations(me):
198 return set()
199 def subst(me, out, cs):
200 rr = me._sub.relations()
201 for r in rr:
202 if len(r) == 0: return
203 cs.push(rr)
204 while True:
205 me._sub.subst(out, cs)
206 if not cs.step(): break
207 cs.pop()
208 def __repr__(me):
209 return '#<RepeatTemplate %r>' % me._sub
210
211 ###--------------------------------------------------------------------------
212 ### Some slightly cheesy parsing machinery.
213
214 class ParseState (object):
215 def __init__(me, file, text):
216 me._file = file
217 me._i = 0
218 me._it = iter(text.splitlines(True))
219 me.step()
220 def step(me):
221 try: me.curr = me._it.next()
222 except StopIteration: me.curr = None
223 else: me._i += 1
224 def error(me, msg):
225 die('%s:%d: %s' % (me._file, me._i, msg))
226
227 class token (object):
228 def __init__(me, name):
229 me._name = name
230 def __repr__(me):
231 return '#<%s>' % me._name
232
233 EOF = token('eof')
234 END = token('end')
235
236 R_SIMPLETAG = RX.compile(r'@ (\w+)', RX.VERBOSE)
237 R_COMPLEXTAG = RX.compile(r'@ { (\w+) ((?: : \w+)*) }', RX.VERBOSE)
238
239 OPMAP = {}
240
241 def defop(func):
242 name = func.func_name
243 if name.startswith('op_'): name = name[3:]
244 OPMAP[name] = func
245 return func
246
247 @defop
248 def op_u(val): return val.upper()
249
250 @defop
251 def op_l(val): return val.lower()
252
253 R_NOTIDENT = RX.compile(r'[^a-zA-Z0-9_]+')
254 @defop
255 def op_c(val): return R_NOTIDENT.sub('_', val)
256
257 def _pairify(val):
258 c = val.find('=')
259 if c >= 0: return val[:c], val[c + 1:]
260 else: return val, val
261
262 @defop
263 def op_left(val): return _pairify(val)[0]
264 @defop
265 def op_right(val): return _pairify(val)[1]
266
267 def parse_text(ps):
268 tt = []
269 lit = StringIO()
270 def spill():
271 l = lit.getvalue()
272 if l: tt.append(LiteralTemplate(l))
273 lit.reset()
274 lit.truncate()
275 while True:
276 line = ps.curr
277 if line is None: break
278 elif line.startswith('%'):
279 if line.startswith('%#'): ps.step(); continue
280 elif line.startswith('%%'): line = line[1:]
281 else: break
282 i = 0
283 while True:
284 j = line.find('@', i)
285 if j < 0: break
286 lit.write(line[i:j])
287 m = R_SIMPLETAG.match(line, j)
288 if not m: m = R_COMPLEXTAG.match(line, j)
289 if not m: ps.error('invalid tag')
290 col = m.group(1)
291 try: rel, i = COLMAP[col]
292 except KeyError: ps.error("unknown column `%s'" % col)
293 wholeop = None
294 ops = m.lastindex >= 2 and m.group(2)
295 if ops:
296 for opname in ops[1:].split(':'):
297 try: op = OPMAP[opname]
298 except KeyError: ps.error("unknown operation `%s'" % opname)
299 if wholeop is None: wholeop = op
300 else: wholeop = (lambda f, g: lambda x: f(g(x)))(op, wholeop)
301 spill()
302 tt.append(TagTemplate(rel, i, wholeop))
303 i = m.end()
304 lit.write(line[i:])
305 ps.step()
306 spill()
307 return SequenceTemplate(tt)
308
309 DIRECT = []
310
311 def direct(rx):
312 def _(func):
313 DIRECT.append((RX.compile(rx, RX.VERBOSE), func))
314 return func
315 return _
316
317 def parse_template(ps):
318 while ps.curr is not None and ps.curr.startswith('%#'): ps.step()
319 if ps.curr is None: return EOF
320 elif ps.curr.startswith('%'):
321 if ps.curr.startswith('%%'): return parse_text(ps)
322 for rx, func in DIRECT:
323 line = ps.curr[1:].strip()
324 m = rx.match(line)
325 if m:
326 ps.step()
327 return func(ps, m)
328 ps.error("unrecognized directive")
329 else:
330 return parse_text(ps)
331
332 def parse_templseq(ps, nestp):
333 tt = []
334 while True:
335 t = parse_template(ps)
336 if t is END:
337 if nestp: break
338 else: ps.error("unexpected `end' directive")
339 elif t is EOF:
340 if nestp: ps.error("unexpected end of file")
341 else: break
342 tt.append(t)
343 return SequenceTemplate(tt)
344
345 @direct(r'repeat')
346 def dir_repeat(ps, m):
347 return RepeatTemplate(parse_templseq(ps, True))
348
349 @direct(r'end')
350 def dir_end(ps, m):
351 return END
352
353 def compile_template(file, text):
354 ps = ParseState(file, text)
355 t = parse_templseq(ps, False)
356 return t
357
358 ###--------------------------------------------------------------------------
359 ### Main code.
360
361 op = OP.OptionParser(
362 description = 'Generates files by filling in simple templates',
363 usage = 'usage: %prog [-gl] FILE [COL,...=VAL,... ... | @FILE:COL,...] ...',
364 version = 'Catacomb version @VERSION@')
365 for short, long, kw in [
366 ('-l', '--list', dict(
367 action = 'store_const', const = 'list', dest = 'mode',
368 help = 'list filenames generated')),
369 ('-g', '--generate', dict(
370 action = 'store', metavar = 'PATH', dest = 'input',
371 help = 'generate output (default)'))]:
372 op.add_option(short, long, **kw)
373 op.set_defaults(mode = 'gen')
374 opts, args = op.parse_args()
375
376 if len(args) < 1: op.error('missing FILE')
377 filepat = args[0]
378 for rel in args[1:]: read_thing(rel)
379 filetempl = compile_template('<output>', filepat)
380
381 def filenames(filetempl):
382 cs = CursorSet()
383 rr = filetempl.relations()
384 for r in rr:
385 if not len(r): return
386 cs.push(rr)
387 while True:
388 out = StringIO()
389 filetempl.subst(out, cs)
390 yield out.getvalue(), cs
391 if not cs.step(): break
392 cs.pop()
393
394 if opts.mode == 'list':
395 for file, cs in filenames(filetempl): print file
396 elif opts.mode == 'gen':
397 with open(opts.input) as f:
398 templ = RepeatTemplate(compile_template(opts.input, f.read()))
399 for file, cs in filenames(filetempl):
400 new = file + '.new'
401 with open(new, 'w') as out:
402 templ.subst(out, cs)
403 OS.rename(new, file)
404 else:
405 raise Exception, 'What am I doing here?'
406
407 ###----- That's all, folks --------------------------------------------------