blob: d68f50cd791723affad4b185d5f601b29d4df455 [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
Neels Hofmeyre0570c22020-05-20 22:40:33 +020092replace_re = re.compile('\$\{([A-Z_][A-Za-z0-9_]*)\}')
Neels Hofmeyr96a12a12019-12-04 03:43:12 +010093command_re = re.compile('\$\{([a-z][A-Za-z0-9_]*)\(([^)]*)\)\}')
Neels Hofmeyr697a6172018-08-22 17:32:21 +020094
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):
Oliver Smith24ddf9c2019-01-30 16:48:18 +010099 print()
Neels Hofmeyr57e42882018-08-23 15:19:35 +0200100 print('Stale: %r is newer than %r' % (src_path, target_path))
101 exit(1)
102
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100103def replace_vars(tmpl, tmpl_dir, tmpl_src, local_config, strict=True):
104 used_vars = set()
105 for m in replace_re.finditer(tmpl):
106 name = m.group(1)
107 if not name in local_config:
108 if strict:
109 print('Error: undefined var %r in %r' % (name, tmpl_src))
110 exit(1)
111 else:
112 continue
113 used_vars.add(name)
114
115 for var in used_vars:
116 tmpl = tmpl.replace('${%s}' % var, local_config.get(var))
117
118 return tmpl
119
120def insert_includes(tmpl, tmpl_dir, tmpl_src, local_config, arg):
121 include_path = os.path.join(tmpl_dir, arg)
122 if not os.path.isfile(include_path):
123 print('Error: included file does not exist: %r in %r' % (include_path, tmpl_src))
124 exit(1)
125 try:
126 incl = open(include_path).read()
127 except:
128 print('Cannot read %r for %r' % (include_path, tmpl_src))
129 raise
130 if args.check_stale:
131 check_stale(include_path, dst)
132
133 # recurse, to follow the paths that the included bits come from
134 incl = handle_commands(incl, os.path.dirname(include_path), include_path, local_config)
135
136 return tmpl.replace('${include(%s)}' % arg, incl)
137
138def insert_foreach(tmpl, tmpl_dir, tmpl_src, match, local_config, arg):
139
140 # figure out section to handle
141 start_span = match.span()
142
143 if tmpl[start_span[1]] == '\n':
144 start_span = (start_span[0], start_span[1] + 1)
145
146 end_str = '${foreach_end}\n'
147
148 end_at = tmpl.find(end_str, start_span[1])
149 if end_at < 0:
150 end_str = end_str[:-1]
151 end_at = tmpl.find(end_str, start_span[1])
152
153 if end_at < 0:
Neels Hofmeyr3c093452018-11-26 01:17:42 +0100154 raise Exception('%r: ${for_each()} expects %r in %r' % (tmpl_src, end_str, tmpl[start_span[1]:]))
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100155
156 end_span = (end_at, end_at + len(end_str))
157
158 before_block = tmpl[:start_span[0]]
159 foreach_block = tmpl[start_span[1]:end_span[0]]
160 after_block = tmpl[end_span[1]:]
161
162 # figure out what items matching the foreach(FOO<number>) there are
163 item_re = re.compile('(^%s([0-9]+))_.*' % arg)
164 items = set()
165 for item in local_config.keys():
166 item_m = item_re.match(item)
167 if not item_m:
168 continue
Neels Hofmeyrc65901c2019-08-21 04:03:42 +0200169 items.add((int(item_m.group(2)), item_m.group(1)))
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100170
171 items = sorted(list(items))
172
173 expanded = [before_block]
Neels Hofmeyrc65901c2019-08-21 04:03:42 +0200174 for nr, item in items:
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100175 expanded_block = foreach_block
176
177 while True:
178 expanded_block_was = expanded_block
179
180 expanded_block = expanded_block.replace('${%sn_' % arg, '${%s_' % item)
Neels Hofmeyrc65901c2019-08-21 04:03:42 +0200181 expanded_block = expanded_block.replace('${%sn}' % arg, str(nr))
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100182 expanded_block = replace_vars(expanded_block, tmpl_dir, tmpl_src, local_config)
183
184 if expanded_block_was == expanded_block:
185 break
186
187 expanded.append(expanded_block)
188
189 expanded.extend(after_block)
190 return ''.join(expanded)
191
192def handle_commands(tmpl, tmpl_dir, tmpl_src, local_config):
Neels Hofmeyre7cb4a52018-11-26 01:18:08 +0100193 while True:
194 # make sure to re-run the regex after each expansion to get proper string
195 # offsets each time
196 m = command_re.search(tmpl)
197 if not m:
198 break;
Neels Hofmeyre02e0ae2018-09-26 14:57:57 +0200199 cmd = m.group(1)
200 arg = m.group(2)
Neels Hofmeyr96a12a12019-12-04 03:43:12 +0100201 expanded = False
Neels Hofmeyre02e0ae2018-09-26 14:57:57 +0200202 if cmd == 'include':
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100203 tmpl = insert_includes(tmpl, tmpl_dir, tmpl_src, local_config, arg)
Neels Hofmeyr96a12a12019-12-04 03:43:12 +0100204 expanded = True
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100205 elif cmd == 'foreach':
206 tmpl = insert_foreach(tmpl, tmpl_dir, tmpl_src, m, local_config, arg)
Neels Hofmeyr96a12a12019-12-04 03:43:12 +0100207 expanded = True
208 elif cmd == 'strftime':
209 pass
Neels Hofmeyre02e0ae2018-09-26 14:57:57 +0200210 else:
211 print('Error: unknown command: %r in %r' % (cmd, tmpl_src))
Neels Hofmeyr96a12a12019-12-04 03:43:12 +0100212 break
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100213
Neels Hofmeyr96a12a12019-12-04 03:43:12 +0100214 if not expanded:
215 break
Neels Hofmeyre7cb4a52018-11-26 01:18:08 +0100216
Neels Hofmeyre02e0ae2018-09-26 14:57:57 +0200217 return tmpl
218
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200219for tmpl_name in sorted(os.listdir(tmpl_dir)):
Neels Hofmeyr57e42882018-08-23 15:19:35 +0200220
221 # omit "hidden" files
222 if tmpl_name.startswith('.'):
223 continue
224
Neels Hofmeyr4f061222018-11-16 23:59:51 +0100225 # omit files to be included by other files
226 if tmpl_name.startswith('common_'):
227 continue
228
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200229 tmpl_src = os.path.join(tmpl_dir, tmpl_name)
230 dst = tmpl_name
231
Oliver Smith87dae192019-11-08 12:28:15 +0100232 # subdirectories: must not contain config files, just copy them
233 if os.path.isdir(tmpl_src):
234 if os.path.exists(dst) and os.path.isdir(dst):
235 shutil.rmtree(dst)
Neels Hofmeyr96a12a12019-12-04 03:43:12 +0100236 shutil.copytree(tmpl_src, dst, symlinks=True)
Oliver Smith87dae192019-11-08 12:28:15 +0100237 continue
238
Neels Hofmeyr57e42882018-08-23 15:19:35 +0200239 if args.check_stale:
240 check_stale(local_config_file, dst)
241 check_stale(tmpl_src, dst)
242
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200243 local_config['_fname'] = tmpl_name
244 local_config['_name'] = os.path.splitext(tmpl_name)[0]
245 local_config['_idx0'] = str(idx)
246 idx += 1
247 local_config['_idx1'] = str(idx)
248
Neels Hofmeyr3cf904a2019-03-13 02:43:41 +0100249 # If there are ${FOOn} in the value of a variable called FOO23_SOMETHING,
250 # then replace that n by 23. This happens automatically in ${foreach} blocks,
251 # but doing this also allows expanding the n outside of ${foreach}.
252 for key, val in local_config.items():
253 foo_n_re = re.compile('\$\{([A-Za-z0-9_]*)n[_}]')
254 for m in foo_n_re.finditer(val):
255 name = m.group(1)
256 item_re = re.compile('^%s([0-9]+)_.*' % name)
257 item_m = item_re.match(key)
258 if not item_m:
259 continue
260 nr_in_key = item_m.group(1)
261 val = val.replace('${%sn}' % name, nr_in_key)
262 val = val.replace('${%sn_' % name, '${%s%s_' % (name, nr_in_key))
263 local_config[key] = val
264
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200265 try:
266 result = open(tmpl_src).read()
267 except:
268 print('Error in %r' % tmpl_src)
269 raise
270
271 while True:
Neels Hofmeyr5549ffb2018-11-16 18:39:57 +0100272 result_was = result
273 result = handle_commands(result, tmpl_dir, tmpl_src, local_config)
274 result = replace_vars(result, tmpl_dir, tmpl_src, local_config)
275 if result_was == result:
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200276 break
277
Neels Hofmeyr57e42882018-08-23 15:19:35 +0200278 if not args.check_stale:
279 with open(dst, 'w') as dst_file:
280 dst_file.write(result)
281 shutil.copymode(tmpl_src, dst)
Neels Hofmeyr697a6172018-08-22 17:32:21 +0200282
283# vim: ts=2 sw=2 expandtab