Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 407 lines (321 sloc) 9.96 KB
#!/usr/bin/python
#
# goatee -- simple file includer and macroish file processor
#
# )_)
# ___|oo) File inclusion and macroish processing--
# '| |\_| --for goats!
# |||| #
# ````
#
# Description:
#
# Goatee processes an input file, which might include other files, or
# execute Python code within special delimiters, and produces an output
# file.
#
# The original intended purpose was to be used in HTML/JS/CSS build
# pipelines, where the CSS or HTML might need to be processed with file
# inclusion or variable substitution
#
# Goatee is small, simple, and standalone, to be easily included with
# other build-related sources, e.g. Makefiles.
#
# License (MIT):
#
# Copyright (c) 2013 Brian "Beej Jorgensen" Hall <beej@beej.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
import sys
import re
import mimetypes
import getopt
import os
import os.path
# regexes for goatee expressions by input file MIME type:
_goatee_patterns = {
'text/html': {
'start': r'\[\[',
'end': r'\](?P<flags>\w*?)\]',
'comment': '<!-- %s -->'
},
'text/css': {
'start': r'\[\[',
'end': r'\](?P<flags>\w*?)\]',
'comment': '/* %s */'
},
'application/javascript': {
'start': r'\[\[',
'end': r'\](?P<flags>\w*?)\]',
'comment': '/* %s */'
},
'text/javascript': {
'start': r'\[\[',
'end': r'\](?P<flags>\w*?)\]',
'comment': '/* %s */'
},
}
# comment to be added to the top of the output file with -w:
_warn_comment = "This is a computer-generated file! Do not edit!"
#====================================================================
_SCRIPT = 'goatee'
_ac = None # global app context
_output = True # true if we should be producing output
#====================================================================
def _write_output(s):
"""Write output conditional on the output() flag."""
if _output:
return _ac.fout.write(str(s))
#====================================================================
def ifndef(s, v=None):
"""Enable output if an environment variable is not defined."""
if v == None:
output(not envdef(s))
else:
output(env(s) != str(v))
def ifdef(s, v=None):
"""Enable output if an environment variable is defined."""
if v == None:
output(envdef(s))
else:
output(env(s) == str(v))
def endif():
"""Enable output after an ifdef() or ifndef()."""
output(True)
#====================================================================
def envdef(s):
"""Return True if an environment variable is defined."""
return s in os.environ
def env(s):
"""Return value of an environment variable, or empty string if
undefined."""
if envdef(s):
return os.environ[s]
return ''
#====================================================================
def prints(s):
"""Print a string to the output file (python2 can't call it
print())."""
return _write_output(str(s))
def ps(s):
"""Alias for prints()."""
return _write_output(str(s))
#====================================================================
def output(b):
"""Set whether or not goatee should produce output. Expressions are
still processed."""
global _output
_output = b
#====================================================================
def source(filename):
"""Function to execute a Python file in the global namespace."""
fp = open(filename)
d = fp.read()
fp.close()
try:
exec(d, globals())
except Exception as a:
sys.stderr.write("%s: %s\n" % (filename, a))
#====================================================================
def _runExpression(expr, filename, lineno, strip):
"""Compile and run a Python expression, catching errors."""
try:
if strip:
expr = expr.strip()
expr_code = compile(expr, filename, 'exec')
exec(expr_code, globals())
except Exception as e:
sys.stderr.write('%s: line %d: %s\n' % \
(filename, lineno, e))
#====================================================================
def include(filename, evaluate=True, typeoverride=None, optional=False, _root=False):
"""Function to include a file in the output stream."""
global _ac
global _goatee_patterns
if filename == None or filename == '-':
fin = sys.stdin
filetype = typeoverride
else:
if typeoverride == None:
filetype = mimetypes.guess_type(filename)[0]
if filetype == None:
if _ac.outfilename != None:
# no? try to get it from the outfile:
#sys.stderr.write("%s\n" % _ac.outfilename)
filetype = mimetypes.guess_type(_ac.outfilename)[0]
else:
filetype = typeoverride
try:
fin = open(filename)
except FileNotFoundError as e:
if optional:
#sys.stderr.write("Missing optional file: %s\n" % filename);
return
else:
raise e
if filetype == None:
_usage_exit('%s: file type unknown' % _printable_filename(filename))
if filetype not in _goatee_patterns:
_usage_exit('%s: no eval patterns found for file type %s' % \
(_printable_filename(filename), filetype))
# find the entry for this filetype
ent = _goatee_patterns[filetype]
start_regex = ent['start']
end_regex = ent['end']
# output a comment warning
if _root and _ac.addcomment:
_write_output(ent['comment'] % _warn_comment)
_write_output('\n\n')
inMatch = False # true if we're inside the delimiters
copyBuf = '' # buffer to accumulate text
execBuf = '' # buffer to accumulate commands
dnl = False
startLine = -1
endLine = -2
if evaluate:
lineno = 0
for l in fin:
lineno += 1
while len(l) > 0:
if inMatch:
mo = re.search(end_regex, l);
else: # else we're out of match
mo = re.search(start_regex, l);
# do processing that looks for flags and handles
# also accumulate buffers
if mo:
beforeMatch = l[:mo.start()]
afterMatch = l[mo.end():]
if inMatch:
# must have just got an out-of-match tag ]]
flags = mo.group('flags')
dnl = 'd' in flags
execBuf += beforeMatch
else:
# must have just got an in-match tag [[
copyBuf += beforeMatch
if dnl:
l = ''
dnl = False
else:
l = afterMatch
else:
if inMatch:
execBuf += l
else:
copyBuf += l
l = ''
# look for state changes
# run commands, generate other output
if mo:
if inMatch:
# switching to out of match
endLine = lineno
# strip whitespace off things that start and end
# on the same line
strip = startLine == endLine
# output buffer
_write_output(copyBuf)
# eval buffer
_runExpression(execBuf, filename, lineno, strip)
startLine = -1
copyBuf = execBuf = ''
else:
# switching to in match
startLine = lineno
inMatch = not inMatch
# output last buffer
_write_output(copyBuf)
copyBuf = ''
else:
# evaluate flag false, so just write 'em as we get 'em
for l in fin:
_write_output(l)
fin.close()
#====================================================================
class AppContext(object):
"""Holds information about the application."""
def __init__(self, argv):
global _SCRIPT
_SCRIPT = os.path.basename(argv.pop(0))
try:
opts, args = getopt.gnu_getopt(argv, 'ho:t:w',
['help', 'output=', 'type=', 'warn-comment'])
except getopt.GetoptError:
_usage_exit()
self.outfilename = None
self.typeoverride = None
self.addcomment = False
for o, a in opts:
if o in ('-h', '--help'):
_usage_exit(None, 0)
elif o in ('-o', '--output'):
self.outfilename = a
elif o in ('-t', '--type'):
self.typeoverride = a
elif o in ('-w', '--warn-comment'):
self.addcomment = True
else:
_usage_exit()
if len(args) > 1:
_usage_exit()
elif len(args) == 1:
self.infilename = args[0]
else:
self.infilename = '-'
# go ahead and open the output file at this point
if self.outfilename == None:
self.fout = sys.stdout
else:
self.fout = open(self.outfilename, 'w');
#====================================================================
def _usage_exit(msg=None, status=1):
"""Exit with usage or an error message."""
e = sys.stderr
if msg == None:
e.write('usage: %s [options] [file]\n\n' % (_SCRIPT,))
e.write(' -h --help this help\n')
e.write(' -o file --output=file send output to file (default stdout)\n')
e.write(' -t type --type=type input file type override\n')
e.write(' -w --warn-comment add a computer-generated warning comment\n')
e.write('\n')
else:
e.write('%s: %s\n' % (_SCRIPT, msg))
sys.exit(status)
#====================================================================
def _printable_filename(filename, is_input=True):
if filename == None or filename == '-':
if is_input:
return '<stdin>'
else:
return '<stdout>'
return filename
#====================================================================
def _main(argv):
"""Main routine"""
global _ac
_ac = AppContext(argv)
mimetypes.init()
# this line does the work:
include(_ac.infilename, evaluate=True, typeoverride=_ac.typeoverride, _root=True)
return 0
if __name__ == "__main__": sys.exit(_main(sys.argv))