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