Yet another python script. As the name suggests this script can print macros. You pass it a template and some arguments and it will print the template with the arguments filled in.

Templates can be provided on the commandline or read from a file. By default pythons string.format function is used. If the template is passed on the commandline \ escapes will be interpreted. An other option is to use mako templates but this requires the mako library to be installed.

Arguments can be passed in several forms as well. They can be provided on the command line as a list of arguments or as a json list, from a file as a json list or from a csv file. Arguments can also be read from a database if you have a python database driver installed for your database type. The python standard library only provides a driver for sqlite3, but it should work with any driver that adheres to PEP-0249. To do this you need to pass the name of the driver, a connect string and an sql query. The results of the query will then be used as arguments.

How the arguments are mapped to the parameters in the template depends on the way the arguments are provided. If the arguments are provided as a list you need to use numerical indices in your template, but if they are passed as dictionaries (like json objects) you need to use their names. If a csv file is used the template is printed once for every line in the file and the arguments can be used by referring to their column number starting with 0. If the parameters are read from a database the arguments can be accessed by the column names in the sql query. Note that some database drivers may alter the column names, for instance cx_Oracle will convert them to uppercase.

#!/usr/bin/env python3
#
#  Copyright 2015 Hans Alves <halves@localhost>
#  This work is free. You can redistribute it and/or modify it under the
#  terms of the Do What The Fuck You Want To Public License, Version 2,
#  as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
#

import re
import codecs
import csv
import argparse
import json
import sys
import importlib

# taken from https://stackoverflow.com/a/24519338
escape_sequence_exp = re.compile(r'''
    ( \\U[0-9A-Fa-f]{8} # 8-digit hex escapes
    | \\u[0-9A-Fa-f]{4} # 4-digit hex escapes
    | \\x[0-9A-Fa-f]{2} # 2-digit hex escapes
    | \\[0-7]{1,3}      # Octal escapes
    | \\N\{[^}]+\}      # Unicode characters by name
    | \\[\\'"abfnrtv]   # Single-character escapes
    )''', re.UNICODE | re.VERBOSE)

def decode_escapes(s):
    return escape_sequence_exp.sub(
        lambda match : codecs.decode(match.group(0), 'unicode-escape'),
        s)


def get_params_iter(args):
    if args.args is not None:
        return iter(args.args)

    if args.json is not None:
        return iter(json.loads(args.json))

    if args.jsonfile is not None:
        with open(args.jsonfile, 'r') as f:
            return iter(json.loads(f.read()))

    if args.csv is not None:
        return iter(csv.reader(open(args.csv, newline='')))

    if args.sql is not None:
        modulename, db_string, query = args.sql
        mod = importlib.import_module(modulename)

        with mod.connect(db_string) as connection:
            cursor = connection.cursor()
            cursor.execute(query)
            colum_names = list(c[0] for c in cursor.description)
            return iter(dict(zip(colum_names, row)) for row in cursor)


def get_macro_closure(args):
    if args.template is not None:
        args.template = decode_escapes(args.template)

    if args.infile is not None:
        with open(args.infile, 'r') as f:
            args.template = f.read()

    if args.template is not None:
        def format(outfile, params):
            if isinstance(params, dict):
                print(args.template.format(**params), file=outfile)
            elif isinstance(params, list):
                print(args.template.format(*params), file=outfile)
            else:
                print(args.template.format(params), file=outfile)

        return format

    elif args.mako is not None:
        import mako.template
        with open(args.mako, 'r') as f:
            template = mako.template.Template(f.read())
        def render(outfile, params):
            if isinstance(params, list):
                params = dict(('p' + str(i), params[i])
                               for i in range(len(params)))
            print(template.render(**params), file=outfile)

        return render


def main():
    parser = argparse.ArgumentParser(description='macro.py takes a list '
                            'of arguments and fills them in a template')
    tgroup = parser.add_argument_group('template')
    templategroup = tgroup.add_mutually_exclusive_group(required=True)
    templategroup.add_argument('-t', '--template', metavar='TEMPLATE',
                       help='use the template specified on the command line')
    templategroup.add_argument('-i', '--infile', metavar='FILE',
                       help='use contents of FILE as template')
    templategroup.add_argument('-m', '--mako', metavar='FILE',
                       help='use contents of FILE as a mako template '
                            '(requires the mako module)')

    agroup = parser.add_argument_group('arguments')
    argumentgroup = agroup.add_mutually_exclusive_group(required=True)
    argumentgroup.add_argument('-a', '--args', nargs='+',
                       help='use the arguments specified on the command line')
    argumentgroup.add_argument('-j', '--json', metavar='STRING',
                       help='load the arguments as a json list from STRING')
    argumentgroup.add_argument('-f', '--jsonfile', metavar='FILE',
                       help='load the arguments as a json list from FILE')
    argumentgroup.add_argument('-c', '--csv', metavar='FILE',
                       help='load the arguments from a csv file.')
    argumentgroup.add_argument('-s', '--sql',
                               metavar=('MODULE', 'DB_STRING', 'QUERY'),
                               nargs=3,
                       help='load the arguments from a database using a '
                            'sql query')

    ogroup = parser.add_argument_group('output')
    outputgroup = ogroup.add_mutually_exclusive_group(required=False)
    outputgroup.add_argument('-o', '--output', metavar='FILE', default='-',
                       help='print the output to FILE, use - for stdout '
                            '(default)')
    outputgroup.add_argument('-p', '--output-param', metavar='PARAM',
                       help='print each macro to a separate file, where '
                            'the filename is the value of PARAM.')

    args = parser.parse_args()

    if args.output != '-' and args.output_param is None:
        outfile = open(args.output, 'w')
    else:
        outfile = sys.stdout

    exec_macro = get_macro_closure(args)
    get_params = get_params_iter(args)

    for params in get_params:
        if args.output_param and args.output_param in params:
            outfile.close()
            outfile = open(params[args.output_param], 'w')
        exec_macro(outfile, params)

    outfile.close()

if __name__ == '__main__':
    sys.exit(main())