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) |
| 53 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 54 | print('using config file %r\non templates %r' % (local_config_file, tmpl_dir)) |
| 55 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 56 | with open(LAST_LOCAL_CONFIG_FILE, 'w') as last_file: |
| 57 | last_file.write(local_config_file) |
| 58 | with open(LAST_TMPL_DIR, 'w') as last_file: |
| 59 | last_file.write(tmpl_dir) |
| 60 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 61 | # read in variable values from config file |
| 62 | local_config = {} |
| 63 | |
| 64 | line_nr = 0 |
| 65 | for line in open(local_config_file): |
| 66 | line_nr += 1 |
| 67 | line = line.strip('\n') |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 68 | |
| 69 | if line.startswith('#'): |
| 70 | continue |
| 71 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 72 | if not '=' in line: |
| 73 | if line: |
| 74 | print("Error: %r line %d: %r" % (local_config_file, line_nr, line)) |
| 75 | exit(1) |
| 76 | continue |
| 77 | |
| 78 | split_pos = line.find('=') |
| 79 | name = line[:split_pos] |
| 80 | val = line[split_pos + 1:] |
| 81 | |
| 82 | if val.startswith('"') and val.endswith('"'): |
| 83 | val = val[1:-1] |
| 84 | |
| 85 | if name in local_config: |
| 86 | print("Error: duplicate identifier in %r line %d: %r" % (local_config_file, line_nr, line)) |
| 87 | local_config[name] = val |
| 88 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 89 | # replace variable names with above values recursively |
| 90 | replace_re = re.compile('\$\{([A-Za-z0-9_]*)\}') |
| 91 | command_re = re.compile('\$\{([A-Za-z0-9_]*)\(([^)]*)\)\}') |
| 92 | |
| 93 | idx = 0 |
| 94 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 95 | def check_stale(src_path, target_path): |
| 96 | if file_newer(src_path, target_path): |
| 97 | print('Stale: %r is newer than %r' % (src_path, target_path)) |
| 98 | exit(1) |
| 99 | |
Neels Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 100 | def insert_includes(tmpl, tmpl_dir, tmpl_src): |
| 101 | for m in command_re.finditer(tmpl): |
| 102 | cmd = m.group(1) |
| 103 | arg = m.group(2) |
| 104 | if cmd == 'include': |
| 105 | include_path = os.path.join(tmpl_dir, arg) |
| 106 | if not os.path.isfile(include_path): |
| 107 | print('Error: included file does not exist: %r in %r' % (include_path, tmpl_src)) |
| 108 | exit(1) |
| 109 | try: |
| 110 | incl = open(include_path).read() |
| 111 | except: |
| 112 | print('Cannot read %r for %r' % (include_path, tmpl_src)) |
| 113 | raise |
| 114 | if args.check_stale: |
| 115 | check_stale(include_path, dst) |
| 116 | |
| 117 | # recurse, to follow the paths that the included bits come from |
| 118 | incl = insert_includes(incl, os.path.dirname(include_path), include_path) |
| 119 | |
| 120 | tmpl = tmpl.replace('${%s(%s)}' % (cmd, arg), incl) |
| 121 | else: |
| 122 | print('Error: unknown command: %r in %r' % (cmd, tmpl_src)) |
| 123 | exit(1) |
| 124 | return tmpl |
| 125 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 126 | for tmpl_name in sorted(os.listdir(tmpl_dir)): |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 127 | |
| 128 | # omit "hidden" files |
| 129 | if tmpl_name.startswith('.'): |
| 130 | continue |
| 131 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 132 | tmpl_src = os.path.join(tmpl_dir, tmpl_name) |
| 133 | dst = tmpl_name |
| 134 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 135 | if args.check_stale: |
| 136 | check_stale(local_config_file, dst) |
| 137 | check_stale(tmpl_src, dst) |
| 138 | |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 139 | local_config['_fname'] = tmpl_name |
| 140 | local_config['_name'] = os.path.splitext(tmpl_name)[0] |
| 141 | local_config['_idx0'] = str(idx) |
| 142 | idx += 1 |
| 143 | local_config['_idx1'] = str(idx) |
| 144 | |
| 145 | try: |
| 146 | result = open(tmpl_src).read() |
| 147 | except: |
| 148 | print('Error in %r' % tmpl_src) |
| 149 | raise |
| 150 | |
| 151 | while True: |
| 152 | used_vars = set() |
Neels Hofmeyr | e02e0ae | 2018-09-26 14:57:57 +0200 | [diff] [blame] | 153 | |
| 154 | result = insert_includes(result, tmpl_dir, tmpl_src) |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 155 | |
| 156 | for m in replace_re.finditer(result): |
| 157 | name = m.group(1) |
| 158 | if not name in local_config: |
| 159 | print('Error: undefined var %r in %r' % (name, tmpl_src)) |
| 160 | exit(1) |
| 161 | used_vars.add(name) |
| 162 | |
| 163 | if not used_vars: |
| 164 | break |
| 165 | |
| 166 | for var in used_vars: |
| 167 | result = result.replace('${%s}' % var, local_config.get(var)) |
| 168 | |
Neels Hofmeyr | 57e4288 | 2018-08-23 15:19:35 +0200 | [diff] [blame] | 169 | if not args.check_stale: |
| 170 | with open(dst, 'w') as dst_file: |
| 171 | dst_file.write(result) |
| 172 | shutil.copymode(tmpl_src, dst) |
Neels Hofmeyr | 697a617 | 2018-08-22 17:32:21 +0200 | [diff] [blame] | 173 | |
| 174 | # vim: ts=2 sw=2 expandtab |