Commit | Line | Data |
---|---|---|
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 | ||
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 -------------------------------------------------- |