Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 2 | '''Take values from a config file and fill them into a set of templates. |
| 3 | Write the result to the current directory.''' |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 4 | |
| 5 | import os, sys, re, shutil |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 6 | import argparse |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 7 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 8 | def file_newer(path_a, than_path_b): |
| 9 | return os.path.getmtime(path_a) > os.path.getmtime(than_path_b) |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 10 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 11 | LAST_LOCAL_CONFIG_FILE = '.last_config' |
| 12 | LAST_TMPL_DIR = '.last_templates' |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 13 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 14 | parser = argparse.ArgumentParser(description=__doc__, |
| 15 | formatter_class=argparse.RawDescriptionHelpFormatter) |
| 16 | parser.add_argument('sources', metavar='SRC', nargs='*', |
| 17 | help='Pass both a template directory and a config file.') |
| 18 | parser.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 | |
| 22 | args = parser.parse_args() |
| 23 | |
| 24 | local_config_file = None |
| 25 | tmpl_dir = None |
| 26 | |
| 27 | for 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 | |
| 37 | if 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 | |
| 40 | if tmpl_dir is None and os.path.isfile(LAST_TMPL_DIR): |
| 41 | tmpl_dir = open(LAST_TMPL_DIR).read().strip() |
| 42 | |
| 43 | if not tmpl_dir or not os.path.isdir(tmpl_dir): |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 44 | print("Template dir does not exist: %r" % tmpl_dir) |
| 45 | exit(1) |
| 46 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 47 | if not local_config_file or not os.path.isfile(local_config_file): |
Neels Hofmeyr | d924e33 | 2018-09-10 00:05:29 +0200 | [diff] [blame] | 48 | print("No such config file: %r" % local_config_file) |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 49 | exit(1) |
| 50 | |
| 51 | local_config_file = os.path.realpath(local_config_file) |
| 52 | tmpl_dir = os.path.realpath(tmpl_dir) |
Oliver Smith | fd7446f | 2018-10-22 11:59:55 +0200 | [diff] [blame] | 53 | net_dir = os.path.realpath(".") |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 54 | |
Oliver Smith | fd7446f | 2018-10-22 11:59:55 +0200 | [diff] [blame] | 55 | print('using config file %r\non templates %r\nwith NET_DIR %r' % (local_config_file, tmpl_dir, net_dir)) |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 56 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 57 | with open(LAST_LOCAL_CONFIG_FILE, 'w') as last_file: |
| 58 | last_file.write(local_config_file) |
| 59 | with open(LAST_TMPL_DIR, 'w') as last_file: |
| 60 | last_file.write(tmpl_dir) |
| 61 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 62 | # read in variable values from config file |
Oliver Smith | fd7446f | 2018-10-22 11:59:55 +0200 | [diff] [blame] | 63 | # NET_DIR is the folder where fill_config.py was started |
| 64 | local_config = {"NET_DIR": net_dir} |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 65 | |
| 66 | line_nr = 0 |
| 67 | for line in open(local_config_file): |
| 68 | line_nr += 1 |
| 69 | line = line.strip('\n') |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 70 | |
| 71 | if line.startswith('#'): |
| 72 | continue |
| 73 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 74 | 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 Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 91 | # replace variable names with above values recursively |
Neels Hofmeyr | e0570c2 | 2020-05-20 22:40:33 +0200 | [diff] [blame] | 92 | replace_re = re.compile('\$\{([A-Z_][A-Za-z0-9_]*)\}') |
Neels Hofmeyr | 96a12a1 | 2019-12-04 03:43:12 +0100 | [diff] [blame] | 93 | command_re = re.compile('\$\{([a-z][A-Za-z0-9_]*)\(([^)]*)\)\}') |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 94 | |
| 95 | idx = 0 |
| 96 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 97 | def check_stale(src_path, target_path): |
| 98 | if file_newer(src_path, target_path): |
Oliver Smith | 24ddf9c | 2019-01-30 16:48:18 +0100 | [diff] [blame] | 99 | print() |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 100 | print('Stale: %r is newer than %r' % (src_path, target_path)) |
| 101 | exit(1) |
| 102 | |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 103 | def 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 | |
| 120 | def 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 | |
| 138 | def 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 Hofmeyr | 3c09345 | 2018-11-26 01:17:42 +0100 | [diff] [blame] | 154 | raise Exception('%r: ${for_each()} expects %r in %r' % (tmpl_src, end_str, tmpl[start_span[1]:])) |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 155 | |
| 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 Hofmeyr | c65901c | 2019-08-21 04:03:42 +0200 | [diff] [blame] | 169 | items.add((int(item_m.group(2)), item_m.group(1))) |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 170 | |
| 171 | items = sorted(list(items)) |
| 172 | |
| 173 | expanded = [before_block] |
Neels Hofmeyr | c65901c | 2019-08-21 04:03:42 +0200 | [diff] [blame] | 174 | for nr, item in items: |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 175 | 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 Hofmeyr | c65901c | 2019-08-21 04:03:42 +0200 | [diff] [blame] | 181 | expanded_block = expanded_block.replace('${%sn}' % arg, str(nr)) |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 182 | 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 | |
| 192 | def handle_commands(tmpl, tmpl_dir, tmpl_src, local_config): |
Neels Hofmeyr | e7cb4a5 | 2018-11-26 01:18:08 +0100 | [diff] [blame] | 193 | 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 Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 199 | cmd = m.group(1) |
| 200 | arg = m.group(2) |
Neels Hofmeyr | 96a12a1 | 2019-12-04 03:43:12 +0100 | [diff] [blame] | 201 | expanded = False |
Neels Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 202 | if cmd == 'include': |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 203 | tmpl = insert_includes(tmpl, tmpl_dir, tmpl_src, local_config, arg) |
Neels Hofmeyr | 96a12a1 | 2019-12-04 03:43:12 +0100 | [diff] [blame] | 204 | expanded = True |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 205 | elif cmd == 'foreach': |
| 206 | tmpl = insert_foreach(tmpl, tmpl_dir, tmpl_src, m, local_config, arg) |
Neels Hofmeyr | 96a12a1 | 2019-12-04 03:43:12 +0100 | [diff] [blame] | 207 | expanded = True |
| 208 | elif cmd == 'strftime': |
| 209 | pass |
Neels Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 210 | else: |
| 211 | print('Error: unknown command: %r in %r' % (cmd, tmpl_src)) |
Neels Hofmeyr | 96a12a1 | 2019-12-04 03:43:12 +0100 | [diff] [blame] | 212 | break |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 213 | |
Neels Hofmeyr | 96a12a1 | 2019-12-04 03:43:12 +0100 | [diff] [blame] | 214 | if not expanded: |
| 215 | break |
Neels Hofmeyr | e7cb4a5 | 2018-11-26 01:18:08 +0100 | [diff] [blame] | 216 | |
Neels Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 217 | return tmpl |
| 218 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 219 | for tmpl_name in sorted(os.listdir(tmpl_dir)): |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 220 | |
| 221 | # omit "hidden" files |
| 222 | if tmpl_name.startswith('.'): |
| 223 | continue |
| 224 | |
Neels Hofmeyr | 4f06122 | 2018-11-16 23:59:51 +0100 | [diff] [blame] | 225 | # omit files to be included by other files |
| 226 | if tmpl_name.startswith('common_'): |
| 227 | continue |
| 228 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 229 | tmpl_src = os.path.join(tmpl_dir, tmpl_name) |
| 230 | dst = tmpl_name |
| 231 | |
Oliver Smith | 87dae19 | 2019-11-08 12:28:15 +0100 | [diff] [blame] | 232 | # 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 Hofmeyr | 96a12a1 | 2019-12-04 03:43:12 +0100 | [diff] [blame] | 236 | shutil.copytree(tmpl_src, dst, symlinks=True) |
Oliver Smith | 87dae19 | 2019-11-08 12:28:15 +0100 | [diff] [blame] | 237 | continue |
| 238 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 239 | if args.check_stale: |
| 240 | check_stale(local_config_file, dst) |
| 241 | check_stale(tmpl_src, dst) |
| 242 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 243 | 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 Hofmeyr | 3cf904a | 2019-03-13 02:43:41 +0100 | [diff] [blame] | 249 | # 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 Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 265 | try: |
| 266 | result = open(tmpl_src).read() |
| 267 | except: |
| 268 | print('Error in %r' % tmpl_src) |
| 269 | raise |
| 270 | |
| 271 | while True: |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 272 | 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 Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 276 | break |
| 277 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 278 | 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 Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 282 | |
| 283 | # vim: ts=2 sw=2 expandtab |