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 | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 102 | def insert_includes(tmpl, tmpl_dir, tmpl_src): |
| 103 | for m in command_re.finditer(tmpl): |
| 104 | cmd = m.group(1) |
| 105 | arg = m.group(2) |
| 106 | if cmd == 'include': |
| 107 | include_path = os.path.join(tmpl_dir, arg) |
| 108 | if not os.path.isfile(include_path): |
| 109 | print('Error: included file does not exist: %r in %r' % (include_path, tmpl_src)) |
| 110 | exit(1) |
| 111 | try: |
| 112 | incl = open(include_path).read() |
| 113 | except: |
| 114 | print('Cannot read %r for %r' % (include_path, tmpl_src)) |
| 115 | raise |
| 116 | if args.check_stale: |
| 117 | check_stale(include_path, dst) |
| 118 | |
| 119 | # recurse, to follow the paths that the included bits come from |
| 120 | incl = insert_includes(incl, os.path.dirname(include_path), include_path) |
| 121 | |
| 122 | tmpl = tmpl.replace('${%s(%s)}' % (cmd, arg), incl) |
| 123 | else: |
| 124 | print('Error: unknown command: %r in %r' % (cmd, tmpl_src)) |
| 125 | exit(1) |
| 126 | return tmpl |
| 127 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 128 | for tmpl_name in sorted(os.listdir(tmpl_dir)): |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 129 | |
| 130 | # omit "hidden" files |
| 131 | if tmpl_name.startswith('.'): |
| 132 | continue |
| 133 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 134 | tmpl_src = os.path.join(tmpl_dir, tmpl_name) |
| 135 | dst = tmpl_name |
| 136 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 137 | if args.check_stale: |
| 138 | check_stale(local_config_file, dst) |
| 139 | check_stale(tmpl_src, dst) |
| 140 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 141 | local_config['_fname'] = tmpl_name |
| 142 | local_config['_name'] = os.path.splitext(tmpl_name)[0] |
| 143 | local_config['_idx0'] = str(idx) |
| 144 | idx += 1 |
| 145 | local_config['_idx1'] = str(idx) |
| 146 | |
| 147 | try: |
| 148 | result = open(tmpl_src).read() |
| 149 | except: |
| 150 | print('Error in %r' % tmpl_src) |
| 151 | raise |
| 152 | |
| 153 | while True: |
| 154 | used_vars = set() |
Neels Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 155 | |
| 156 | result = insert_includes(result, tmpl_dir, tmpl_src) |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 157 | |
| 158 | for m in replace_re.finditer(result): |
| 159 | name = m.group(1) |
| 160 | if not name in local_config: |
| 161 | print('Error: undefined var %r in %r' % (name, tmpl_src)) |
| 162 | exit(1) |
| 163 | used_vars.add(name) |
| 164 | |
| 165 | if not used_vars: |
| 166 | break |
| 167 | |
| 168 | for var in used_vars: |
| 169 | result = result.replace('${%s}' % var, local_config.get(var)) |
| 170 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 171 | if not args.check_stale: |
| 172 | with open(dst, 'w') as dst_file: |
| 173 | dst_file.write(result) |
| 174 | shutil.copymode(tmpl_src, dst) |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 175 | |
| 176 | # vim: ts=2 sw=2 expandtab |