configure.ac: Replace with a new version.
[u/mdw/catacomb] / 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 me.curr = next(me._it, None)
222 if me.curr is not None: 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 wholeop = None
293 ops = m.lastindex >= 2 and m.group(2)
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 --------------------------------------------------