symm/multigen: Fix for Python 2.5.
[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
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
73class 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
102class 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
119def 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
124def 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
134def read_thing(spec):
135 if spec.startswith('@'): read_file(spec[1:])
136 else: read_immediate(spec)
137
138###--------------------------------------------------------------------------
139### Template structure.
140
141class BasicTemplate (object):
142 pass
143
144class 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
155class 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
170class 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
194class 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
214class 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):
28ffcb2a
MW
221 try: me.curr = me._it.next()
222 except StopIteration: me.curr = None
223 else: me._i += 1
7db733d4
MW
224 def error(me, msg):
225 die('%s:%d: %s' % (me._file, me._i, msg))
226
227class token (object):
228 def __init__(me, name):
229 me._name = name
230 def __repr__(me):
231 return '#<%s>' % me._name
232
233EOF = token('eof')
234END = token('end')
235
236R_SIMPLETAG = RX.compile(r'@ (\w+)', RX.VERBOSE)
237R_COMPLEXTAG = RX.compile(r'@ { (\w+) ((?: : \w+)*) }', RX.VERBOSE)
238
239OPMAP = {}
240
241def 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
248def op_u(val): return val.upper()
249
250@defop
251def op_l(val): return val.lower()
252
253R_NOTIDENT = RX.compile(r'[^a-zA-Z0-9_]+')
254@defop
255def op_c(val): return R_NOTIDENT.sub('_', val)
256
257def _pairify(val):
258 c = val.find('=')
259 if c >= 0: return val[:c], val[c + 1:]
260 else: return val, val
261
262@defop
263def op_left(val): return _pairify(val)[0]
264@defop
265def op_right(val): return _pairify(val)[1]
266
267def 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
309DIRECT = []
310
311def direct(rx):
312 def _(func):
313 DIRECT.append((RX.compile(rx, RX.VERBOSE), func))
314 return func
315 return _
316
317def 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
332def 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')
346def dir_repeat(ps, m):
347 return RepeatTemplate(parse_templseq(ps, True))
348
349@direct(r'end')
350def dir_end(ps, m):
351 return END
352
353def compile_template(file, text):
354 ps = ParseState(file, text)
355 t = parse_templseq(ps, False)
356 return t
357
358###--------------------------------------------------------------------------
359### Main code.
360
361op = 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@')
365for 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)
373op.set_defaults(mode = 'gen')
374opts, args = op.parse_args()
375
376if len(args) < 1: op.error('missing FILE')
377filepat = args[0]
378for rel in args[1:]: read_thing(rel)
379filetempl = compile_template('<output>', filepat)
380
381def 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
394if opts.mode == 'list':
395 for file, cs in filenames(filetempl): print file
396elif 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)
404else:
405 raise Exception, 'What am I doing here?'
406
407###----- That's all, folks --------------------------------------------------