chiark / gitweb /
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 --------------------------------------------------