#!/usr/bin/python3 # "PlacehoLder Un- and Generator" import sys import re from enum import Enum, auto C = {} # ANSI colors def has_color(): import os return "color" in os.environ.get('TERM') def def_colors(): global C C = { 'b': '\033[34m', 'y': '\033[33m', 'g': '\033[32m', 'r': '\033[31m', 'm': '\033[35m', 'B': '\033[1m', 'R': '\033[7m', 'n': '\033[0m', } def undef_colors(): global C C = { 'b': '', 'y': '', 'g': '', 'r': '', 'B': '', 'n': '', 'R': '', 'm': '', } undef_colors() class Error(Enum): PARAM_MISS = auto() UNK_FLAG = auto() IO = auto() UNK_OPT = auto() UNTERM_DEF = auto() NO_FILES = auto() MULTI_OP = auto() NO_OP = auto() def usage(): print( '''{C[g]}{C[B]}{argv0}{C[n]} {C[B]}{C[y]}({C[b]}-g{C[y]}|{C[b]}-u{C[y]}) {C[B]}{C[y]}({C[b]}<option>{C[y]}|{C[b]}<file>{C[y]})+{C[n]} {C[y]}Options:{C[n]} {C[g]}{C[B]}-h{C[n]} {C[b]}{C[B]}{C[n]} : print help and exit {C[g]}{C[B]}--color{C[n]} {C[B]}{C[b]}never{C[y]}|{C[b]}auto{C[y]}|{C[b]}always{C[n]} : set output coloring option; default: auto {C[g]}{C[B]}-d{C[n]} {C[b]}{C[B]}<name> <value>{C[n]} : define placeholder on the cli {C[g]}{C[B]}-e{C[n]} {C[b]}{C[B]}<name> <file>{C[n]} : define placeholder as the contents of <file> {C[g]}{C[B]}-a{C[n]} : define all undefined placeholders as empty strings {C[g]}{C[B]}-u{C[n]} : ungenerate placeholders (ie. collapse) {C[g]}{C[B]}-g{C[n]} : generate placeholders (ie. expand) Every argument not starting with '-' is considered a file. If multiple files are specified, actions apply to all of them. Undefine placeholders are left intact. {C[y]}{C[B]}Placeholder syntax:{C[n]} {C[y]}#placeholder<{C[n]}<name>{C[y]}> COLLAPSED{C[n]} {C[m]}{C[R]}NOTE:{C[n]} text located on the same line and before the placeholder is preserved. This allows you to comment it out while embedding. {C[y]}Builtins:{C[n]} Builtin placeholder names start with '@', these names are reserved. This Plug version defines the following builtins: {C[b]}@gnu-tofile-*{C[n]} {C[y]}{C[B]}Example:{C[n]} {C[g]}$ cat example/ex1.txt{C[n]} original text #placeholder<hw> COLLAPSED some more original text {C[g]}$ plug -g -d hw 'hello world' example/ex1.txt{C[n]} {C[g]}$ cat example/ex1.txt{C[n]} original text #placeholder<hw> BEGIN hello world #placeholder<hw> END some more original text '''.format(**globals(), argv0 = sys.argv[0]), end='') destination_files = [] operation = "" is_all = False placeholders = {} placeholder = '#placeholder<{0}>' placeholder_collapsed = placeholder + ' COLLAPSED' placeholder_expanded_beginning = placeholder + ' BEGIN' placeholder_expanded_ending = placeholder + ' END' del placeholder re_placeholder_collapsed = re.compile('''^(.*){0}.*'''.format(placeholder_collapsed.format('''([a-zA-Z0-9_@-]+)''')), re.M) re_placeholder_expanded_beginning = re.compile('''^(.*){0}.*'''.format(placeholder_expanded_beginning.format('''([a-zA-Z0-9_@-]+)''')), re.M) re_placeholder_expanded_ending = re.compile('''^.*{0}.*'''.format(placeholder_expanded_ending.format('''([a-zA-Z0-9_@-]+)''')), re.M) builtins = [ ('''@gnu-tofile-(.*)''', '''payload_data=$(sed -n '/#placeholder<payload> START$/,/#placeholder<payload> END$/p' "$(realpath $0)") payload_data=$(echo "$payload_data" | grep -vE '#placeholder<payload> (START|END)') [ -z "$PAYLOADOUT" ] && PAYLOADOUT="out" echo "$payload_data" > "$PAYLOADOUT"''' ), ] def builtin_lookup(phl : str) -> str: for i in builtins: regex, value = i m = re.compile(regex).match(phl) if m: value = value.format(m.groups()[1:]) return value return '' def gen(s : str, phls : [str]) -> str: global is_all s = ungen(s, phls) for phl in phls: buf = '' l = 0 for m in re_placeholder_collapsed.finditer(s): if (m.group(2) != phl): continue buf += s[l : m.start(0)] buf += m.group(1) + placeholder_expanded_beginning.format(phl) + '\n' buf += builtin_lookup(phl) if phl[0] == '@' else placeholders[phl] buf += '\n' + m.group(1) + placeholder_expanded_ending.format(phl) l = m.end(0) buf += s[l:] s = buf if is_all: buf = '' l = 0 for m in re_placeholder_collapsed.finditer(s): buf += s[l : m.start(0)] buf += m.group(1) + placeholder_expanded_beginning.format(m.group(2)) + '\n' buf += m.group(1) + placeholder_expanded_ending.format(m.group(2)) l = m.end(0) buf += s[l:] s = buf return s def ungen(s : str, phls : [str]) -> str: global is_all for phl in phls: buf = '' l = 0 for m in re_placeholder_expanded_beginning.finditer(s): if(m.group(2) != phl): continue buf += s[l : m.start(0)] buf += m.group(1) + placeholder_collapsed.format(phl) l = m.end(0) for me in re_placeholder_expanded_ending.finditer(s[m.end(0):]): if(me.group(1) != phl): continue l = m.end(0) + me.end(0) break buf += s[l:] s = buf if is_all: buf = '' l = 0 for m in re_placeholder_expanded_beginning.finditer(s): buf += s[l : m.start(0)] buf += m.group(1) + placeholder_collapsed.format(m.group(2)) l = m.end(0) for me in re_placeholder_expanded_ending.finditer(s[m.end(0):]): if(me.group(1) != m.group(2)): continue l = m.end(0) + me.end(0) break buf += s[l:] s = buf return s def error_and_quit(e : int, argv : [str]) -> None: message = { Error.PARAM_MISS : "Missing parameter to flag '{0}'.", Error.UNK_FLAG : "Unrecognized flag '{0}'.", Error.UNK_OPT : "Unknown option passed to {0}: '{1}'.", Error.IO : "I/O error encountered while interacting with '{0}'.", Error.UNTERM_DEF : "Unterminated definition ({0}).", Error.NO_FILES : "Flags were specified, but no files.", Error.MULTI_OP : "Multiple operations specified, '-g'/'-u' are mutually exclive.", Error.NO_OP : "No operation operation specified. Either '-g' or '-u' is required.", } formatted_message = message[e].format(*argv, **globals()) print("{C[r]}{msg}{C[n]}".format(**globals(), msg=formatted_message)) exit(e.value) # We need this function because getopt does not support a single flag taking 2 arguments def parse_args(argv : [str]) -> None: global destination_files, operation, is_all def get_param(argv : [str], i : int) -> str: try: param = argv[i] except: error_and_quit(Error.PARAM_MISS, [argv[i-1]]) return param for arg in argv: if arg == '-h' or arg == '--help': usage() exit(0) try: i = 0 while i < len(argv): # 0 parama opt if argv[i] == '-u' or argv[i] == '-g': if operation != '': error_and_quit(Error.MULTI_OP, []) operation = argv[i][1] i = i + 1 continue if argv[i] == '-a': is_all = True i = i + 1 continue # 1 param opt if argv[i-1] == '--color': p = get_param(argv, i) if p == 'always': def_colors() elif p == 'auto' and has_color: def_colors() elif p == 'never': undef_colors() else: error_and_quit(Error.UNK_OPT, ['--color', p]) i = i + 2 continue # 2 param opt if argv[i] == '-d': placeholders[argv[i+1]] = argv[i+2] i = i + 3 continue if argv[i] == '-e': with open(argv[i+2], "r") as f: placeholders[argv[i+1]] = f.read() i = i + 3 continue # catch all if argv[i][0] != '-': # file destination_files.append(argv[i]) i = i + 1 continue error_and_quit(Error.UNK_FLAG, [argv[i]]) except IndexError: error_and_quit(Error.PARAM_MISS, [argv[i]]) except FileNotFoundError as e: error_and_quit(Error.IO, [e.filename]) def plug(argv : [str]) -> int: global destination_files, operation if has_color: def_colors() if len(argv) < 2: usage() exit(1) parse_args(argv) if destination_files == []: error_and_quit(Error.NO_FILES, []) if operation == '': error_and_quit(Error.NO_OP, [argv[i-1]]) elif operation == 'g': gen_callback = gen elif operation == 'u': gen_callback = ungen for df in destination_files: try: with open(df, 'r') as f: s = gen_callback(f.read(), placeholders) with open(df, 'w') as f: f.write(s) except FileNotFoundError: error_and_quit(Error.IO, df) return 0 if __name__ == '__main__': raise SystemExit(plug(sys.argv[1:]))