math/mpgen, symm/multigen: Various minor cleanups.
[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.reset()
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
72 class 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
101 class 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'" %
109 (', '.join(row), ', '.join(me._head)))
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
118 def 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
123 def read_file(spec):
124 file, head = spec.split(':', 1)
125 rel = Relation([c.strip() for c in head.split(',')])
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
132 def read_thing(spec):
133 if spec.startswith('@'): read_file(spec[1:])
134 else: read_immediate(spec)
135
136 ###--------------------------------------------------------------------------
137 ### Template structure.
138
139 class BasicTemplate (object):
140 pass
141
142 class 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
153 class 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
168 class SequenceTemplate (BasicTemplate):
169 def __new__(cls, seq, **kw):
170 if len(seq) == 1:
171 return seq[0]
172 else:
173 return super(SequenceTemplate, cls).__new__(cls, seq = seq, **kw)
174
175 def __init__(me, seq, **kw):
176 super(SequenceTemplate, me).__init__(**kw)
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
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
193 class 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
213 class 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):
220 try: me.curr = me._it.next()
221 except StopIteration: me.curr = None
222 else: me._i += 1
223 def error(me, msg):
224 die('%s:%d: %s' % (me._file, me._i, msg))
225
226 class token (object):
227 def __init__(me, name):
228 me._name = name
229 def __repr__(me):
230 return '#<%s>' % me._name
231
232 EOF = token('eof')
233 END = token('end')
234
235 R_SIMPLETAG = RX.compile(r'@ (\w+)', RX.VERBOSE)
236 R_COMPLEXTAG = RX.compile(r'@ { (\w+) ((?: : \w+)*) }', RX.VERBOSE)
237
238 OPMAP = {}
239
240 def 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
247 def op_u(val): return val.upper()
248
249 @defop
250 def op_l(val): return val.lower()
251
252 R_NOTIDENT = RX.compile(r'[^a-zA-Z0-9_]+')
253 @defop
254 def op_c(val): return R_NOTIDENT.sub('_', val)
255
256 def _pairify(val):
257 c = val.find('=')
258 if c >= 0: return val[:c], val[c + 1:]
259 else: return val, val
260
261 @defop
262 def op_left(val): return _pairify(val)[0]
263 @defop
264 def op_right(val): return _pairify(val)[1]
265
266 def 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)
292 ops = m.lastindex >= 2 and m.group(2)
293 wholeop = None
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
308 DIRECT = []
309
310 def direct(rx):
311 def _(func):
312 DIRECT.append((RX.compile(rx, RX.VERBOSE), func))
313 return func
314 return _
315
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()
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
331 def 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')
345 def dir_repeat(ps, m):
346 return RepeatTemplate(parse_templseq(ps, True))
347
348 @direct(r'end')
349 def dir_end(ps, m):
350 return END
351
352 def compile_template(file, text):
353 ps = ParseState(file, text)
354 t = parse_templseq(ps, False)
355 return t
356
357 ###--------------------------------------------------------------------------
358 ### Main code.
359
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()
374
375 if len(args) < 1: op.error('missing FILE')
376 filepat = args[0]
377 for rel in args[1:]: read_thing(rel)
378 filetempl = compile_template('<output>', filepat)
379
380 def 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
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):
399 new = file + '.new'
400 with open(new, 'w') as out:
401 templ.subst(out, cs)
402 OS.rename(new, file)
403 else:
404 raise Exception, 'What am I doing here?'
405
406 ###----- That's all, folks --------------------------------------------------