blob: 665926a12b8dadd6e61c8981bc69dc59d1d70870 [file] [log] [blame]
Neels Hofmeyr697a6172018-08-22 17:32:21 +02001#!/usr/bin/env python3
Neels Hofmeyr57e42882018-08-23 15:19:35 +02002'''Take values from a config file and fill them into a set of templates.
3Write the result to the current directory.'''
Neels Hofmeyr697a6172018-08-22 17:32:21 +02004
5import os, sys, re, shutil
Neels Hofmeyr57e42882018-08-23 15:19:35 +02006import argparse
Neels Hofmeyr697a6172018-08-22 17:32:21 +02007
Neels Hofmeyr57e42882018-08-23 15:19:35 +02008def file_newer(path_a, than_path_b):
9 return os.path.getmtime(path_a) > os.path.getmtime(than_path_b)
Neels Hofmeyr697a6172018-08-22 17:32:21 +020010
Neels Hofmeyr57e42882018-08-23 15:19:35 +020011LAST_LOCAL_CONFIG_FILE = '.last_config'
12LAST_TMPL_DIR = '.last_templates'
Neels Hofmeyr697a6172018-08-22 17:32:21 +020013
Neels Hofmeyr57e42882018-08-23 15:19:35 +020014parser = argparse.ArgumentParser(description=__doc__,
15 formatter_class=argparse.RawDescriptionHelpFormatter)
16parser.add_argument('sources', metavar='SRC', nargs='*',
17 help='Pass both a template directory and a config file.')
18parser.add_argument('-s', '--check-stale', dest='check_stale', action='store_true',
19 help='only verify age of generated files vs. config and templates.'
20 ' Exit nonzero when any source file is newer. Do not write anything.')
21
22args = parser.parse_args()
23
24local_config_file = None
25tmpl_dir = None
26
27for src in args.sources:
28 if os.path.isdir(src):
29 if tmpl_dir is not None:
30 print('Error: only one template dir permitted. (%r vs. %r)' % (tmpl_dir, src))
31 tmpl_dir = src
32 elif os.path.isfile(src):
33 if local_config_file is not None:
34 print('Error: only one config file permitted. (%r vs. %r)' % (local_config_file, src))
35 local_config_file = src
36
37if local_config_file is None and os.path.isfile(LAST_LOCAL_CONFIG_FILE):
38 local_config_file = open(LAST_LOCAL_CONFIG_FILE).read().strip()
39
40if tmpl_dir is None and os.path.isfile(LAST_TMPL_DIR):
41 tmpl_dir = open(LAST_TMPL_DIR).read().strip()
42
43if not tmpl_dir or not os.path.isdir(tmpl_dir):
Neels Hofmeyr697a6172018-08-22 17:32:21 +020044 print("Template dir does not exist: %r" % tmpl_dir)
45 exit(1)
46
Neels Hofmeyr57e42882018-08-23 15:19:35 +020047if not local_config_file or not os.path.isfile(local_config_file):
Neels Hofmeyrd924e332018-09-10 00:05:29 +020048 print("No such config file: %r" % local_config_file)
Neels Hofmeyr57e42882018-08-23 15:19:35 +020049 exit(1)
50
51local_config_file = os.path.realpath(local_config_file)
52tmpl_dir = os.path.realpath(tmpl_dir)
Oliver Smithfd7446f2018-10-22 11:59:55 +020053net_dir = os.path.realpath(".")
Neels Hofmeyr57e42882018-08-23 15:19:35 +020054
Oliver Smithfd7446f2018-10-22 11:59:55 +020055print('using config file %r\non templates %r\nwith NET_DIR %r' % (local_config_file, tmpl_dir, net_dir))
Neels Hofmeyr697a6172018-08-22 17:32:21 +020056
Neels Hofmeyr57e42882018-08-23 15:19:35 +020057with open(LAST_LOCAL_CONFIG_FILE, 'w') as last_file:
58 last_file.write(local_config_file)
59with open(LAST_TMPL_DIR, 'w') as last_file:
60 last_file.write(tmpl_dir)
61
Neels Hofmeyr697a6172018-08-22 17:32:21 +020062# read in variable values from config file
Oliver Smithfd7446f2018-10-22 11:59:55 +020063# NET_DIR is the folder where fill_config.py was started
64local_config = {"NET_DIR": net_dir}
Neels Hofmeyr697a6172018-08-22 17:32:21 +020065
66line_nr = 0
67for line in open(local_config_file):
68 line_nr += 1
69 line = line.strip('\n')
Neels Hofmeyr57e42882018-08-23 15:19:35 +020070
71 if line.startswith('#'):
72 continue
73
Neels Hofmeyr697a6172018-08-22 17:32:21 +020074 if not '=' in line:
75 if line:
76 print("Error: %r line %d: %r" % (local_config_file, line_nr, line))
77 exit(1)
78 continue
79
80 split_pos = line.find('=')
81 name = line[:split_pos]
82 val = line[split_pos + 1:]
83
84 if val.startswith('"') and val.endswith('"'):
85 val = val[1:-1]
86
87 if name in local_config:
88 print("Error: duplicate identifier in %r line %d: %r" % (local_config_file, line_nr, line))
89 local_config[name] = val
90
Neels Hofmeyr697a6172018-08-22 17:32:21 +020091# replace variable names with above values recursively
92replace_re = re.compile('\$\{([A-Za-z0-9_]*)\}')
93command_re = re.compile('\$\{([A-Za-z0-9_]*)\(([^)]*)\)\}')
94
95idx = 0
96
Neels Hofmeyr57e42882018-08-23 15:19:35 +020097def check_stale(src_path, target_path):
98 if file_newer(src_path, target_path):
99 print('Stale: %r is newer than %r' % (src_path, target_path))
100 exit(1)
101
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100102def replace_vars(tmpl, tmpl_dir, tmpl_src, local_config, strict=True):
103 used_vars = set()
104 for m in replace_re.finditer(tmpl):
105 name = m.group(1)
106 if not name in local_config:
107 if strict:
108 print('Error: undefined var %r in %r' % (name, tmpl_src))
109 exit(1)
110 else:
111 continue
112 used_vars.add(name)
113
114 for var in used_vars:
115 tmpl = tmpl.replace('${%s}' % var, local_config.get(var))
116
117 return tmpl
118
119def insert_includes(tmpl, tmpl_dir, tmpl_src, local_config, arg):
120 include_path = os.path.join(tmpl_dir, arg)
121 if not os.path.isfile(include_path):
122 print('Error: included file does not exist: %r in %r' % (include_path, tmpl_src))
123 exit(1)
124 try:
125 incl = open(include_path).read()
126 except:
127 print('Cannot read %r for %r' % (include_path, tmpl_src))
128 raise
129 if args.check_stale:
130 check_stale(include_path, dst)
131
132 # recurse, to follow the paths that the included bits come from
133 incl = handle_commands(incl, os.path.dirname(include_path), include_path, local_config)
134
135 return tmpl.replace('${include(%s)}' % arg, incl)
136
137def insert_foreach(tmpl, tmpl_dir, tmpl_src, match, local_config, arg):
138
139 # figure out section to handle
140 start_span = match.span()
141
142 if tmpl[start_span[1]] == '\n':
143 start_span = (start_span[0], start_span[1] + 1)
144
145 end_str = '${foreach_end}\n'
146
147 end_at = tmpl.find(end_str, start_span[1])
148 if end_at < 0:
149 end_str = end_str[:-1]
150 end_at = tmpl.find(end_str, start_span[1])
151
152 if end_at < 0:
Neels Hofmeyr3c093452018-11-26 01:17:42 +0100153 raise Exception('%r: ${for_each()} expects %r in %r' % (tmpl_src, end_str, tmpl[start_span[1]:]))
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100154
155 end_span = (end_at, end_at + len(end_str))
156
157 before_block = tmpl[:start_span[0]]
158 foreach_block = tmpl[start_span[1]:end_span[0]]
159 after_block = tmpl[end_span[1]:]
160
161 # figure out what items matching the foreach(FOO<number>) there are
162 item_re = re.compile('(^%s([0-9]+))_.*' % arg)
163 items = set()
164 for item in local_config.keys():
165 item_m = item_re.match(item)
166 if not item_m:
167 continue
168 items.add((item_m.group(1), item_m.group(2)))
169
170 items = sorted(list(items))
171
172 expanded = [before_block]
173 for item, nr in items:
174 expanded_block = foreach_block
175
176 while True:
177 expanded_block_was = expanded_block
178
179 expanded_block = expanded_block.replace('${%sn_' % arg, '${%s_' % item)
180 expanded_block = expanded_block.replace('${%sn}' % arg, nr)
181 expanded_block = replace_vars(expanded_block, tmpl_dir, tmpl_src, local_config)
182
183 if expanded_block_was == expanded_block:
184 break
185
186 expanded.append(expanded_block)
187
188 expanded.extend(after_block)
189 return ''.join(expanded)
190
191def handle_commands(tmpl, tmpl_dir, tmpl_src, local_config):
192 handled = 0
Neels Hofmeyre02e0ae2018-09-26 14:57:57 +0200193 for m in command_re.finditer(tmpl):
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100194 handled += 1
Neels Hofmeyre02e0ae2018-09-26 14:57:57 +0200195 cmd = m.group(1)
196 arg = m.group(2)
197 if cmd == 'include':
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100198 tmpl = insert_includes(tmpl, tmpl_dir, tmpl_src, local_config, arg)
199 elif cmd == 'foreach':
200 tmpl = insert_foreach(tmpl, tmpl_dir, tmpl_src, m, local_config, arg)
Neels Hofmeyre02e0ae2018-09-26 14:57:57 +0200201 else:
202 print('Error: unknown command: %r in %r' % (cmd, tmpl_src))
203 exit(1)
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100204
Neels Hofmeyre02e0ae2018-09-26 14:57:57 +0200205 return tmpl
206
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200207for tmpl_name in sorted(os.listdir(tmpl_dir)):
Neels Hofmeyr57e42882018-08-23 15:19:35 +0200208
209 # omit "hidden" files
210 if tmpl_name.startswith('.'):
211 continue
212
Neels Hofmeyr4f061222018-11-16 23:59:51 +0100213 # omit files to be included by other files
214 if tmpl_name.startswith('common_'):
215 continue
216
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200217 tmpl_src = os.path.join(tmpl_dir, tmpl_name)
218 dst = tmpl_name
219
Neels Hofmeyr57e42882018-08-23 15:19:35 +0200220 if args.check_stale:
221 check_stale(local_config_file, dst)
222 check_stale(tmpl_src, dst)
223
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200224 local_config['_fname'] = tmpl_name
225 local_config['_name'] = os.path.splitext(tmpl_name)[0]
226 local_config['_idx0'] = str(idx)
227 idx += 1
228 local_config['_idx1'] = str(idx)
229
230 try:
231 result = open(tmpl_src).read()
232 except:
233 print('Error in %r' % tmpl_src)
234 raise
235
236 while True:
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100237 result_was = result
238 result = handle_commands(result, tmpl_dir, tmpl_src, local_config)
239 result = replace_vars(result, tmpl_dir, tmpl_src, local_config)
240 if result_was == result:
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200241 break
242
Neels Hofmeyr57e42882018-08-23 15:19:35 +0200243 if not args.check_stale:
244 with open(dst, 'w') as dst_file:
245 dst_file.write(result)
246 shutil.copymode(tmpl_src, dst)
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200247
248# vim: ts=2 sw=2 expandtab