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 |
| 92 | replace_re = re.compile('\$\{([A-Za-z0-9_]*)\}') |
| 93 | command_re = re.compile('\$\{([A-Za-z0-9_]*)\(([^)]*)\)\}') |
| 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): |
| 99 | print('Stale: %r is newer than %r' % (src_path, target_path)) |
| 100 | exit(1) |
| 101 | |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 102 | def 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 | |
| 119 | def 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 | |
| 137 | def 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 Hofmeyr | 3c09345 | 2018-11-26 01:17:42 +0100 | [diff] [blame^] | 153 | 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] | 154 | |
| 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 | |
| 191 | def handle_commands(tmpl, tmpl_dir, tmpl_src, local_config): |
| 192 | handled = 0 |
Neels Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 193 | for m in command_re.finditer(tmpl): |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 194 | handled += 1 |
Neels Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 195 | cmd = m.group(1) |
| 196 | arg = m.group(2) |
| 197 | if cmd == 'include': |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 198 | 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 Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 201 | else: |
| 202 | print('Error: unknown command: %r in %r' % (cmd, tmpl_src)) |
| 203 | exit(1) |
Neels Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 204 | |
Neels Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 205 | return tmpl |
| 206 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 207 | for tmpl_name in sorted(os.listdir(tmpl_dir)): |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 208 | |
| 209 | # omit "hidden" files |
| 210 | if tmpl_name.startswith('.'): |
| 211 | continue |
| 212 | |
Neels Hofmeyr | 4f06122 | 2018-11-16 23:59:51 +0100 | [diff] [blame] | 213 | # omit files to be included by other files |
| 214 | if tmpl_name.startswith('common_'): |
| 215 | continue |
| 216 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 217 | tmpl_src = os.path.join(tmpl_dir, tmpl_name) |
| 218 | dst = tmpl_name |
| 219 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 220 | if args.check_stale: |
| 221 | check_stale(local_config_file, dst) |
| 222 | check_stale(tmpl_src, dst) |
| 223 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 224 | 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 Hofmeyr | 5549ffb | 2018-11-16 18:39:57 +0100 | [diff] [blame] | 237 | 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 Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 241 | break |
| 242 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 243 | 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 Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 247 | |
| 248 | # vim: ts=2 sw=2 expandtab |