import optparse
import os
import re
import sys
_MACRO_RE = re.compile(r'(?P<name>\w+)\((?P<args>.*?)\)')
_MACRO_ARG_NAME_RE = re.compile(r'^\w+$')
_GENERATED_CODE_LINE = (
'// This block of code is generated, do not edit it directly.'
)
def _MacroRefRe(macro_names):
return re.compile(r'\b(?P<macro_ref>(?P<name>(%s))\((?P<args>.*?)\))' %
'|'.join(macro_names))
def _MacroArgRefRe(macro_arg_names):
return re.compile(r'\b(?P<name>(%s))(\$(?P<option>.))?\b' %
'|'.join(macro_arg_names))
class PDDMError(Exception):
def __init__(self, message="Error"):
self.message = message
super().__init__(self.message)
class MacroCollection(object):
def __init__(self, a_file=None):
self._macros = dict()
if a_file:
self.ParseInput(a_file)
class MacroDefinition(object):
def __init__(self, name, arg_names):
self._name = name
self._args = tuple(arg_names)
self._body = ''
self._needNewLine = False
def AppendLine(self, line):
if self._needNewLine:
self._body += '\n'
self._body += line
self._needNewLine = not line.endswith('\n')
@property
def name(self):
return self._name
@property
def args(self):
return self._args
@property
def body(self):
return self._body
def ParseInput(self, a_file):
input_lines = a_file.read().splitlines()
self.ParseLines(input_lines)
def ParseLines(self, input_lines):
current_macro = None
for line in input_lines:
if line.startswith('PDDM-'):
directive = line.split(' ', 1)[0]
if directive == 'PDDM-DEFINE':
name, args = self._ParseDefineLine(line)
if self._macros.get(name):
raise PDDMError('Attempt to redefine macro: "%s"' % line)
current_macro = self.MacroDefinition(name, args)
self._macros[name] = current_macro
continue
if directive == 'PDDM-DEFINE-END':
if not current_macro:
raise PDDMError('Got DEFINE-END directive without an active macro:'
' "%s"' % line)
current_macro = None
continue
raise PDDMError('Hit a line with an unknown directive: "%s"' % line)
if current_macro:
current_macro.AppendLine(line)
continue
if line.strip() == '':
continue
raise PDDMError('Hit a line that wasn\'t a directive and no open macro'
' definition: "%s"' % line)
def _ParseDefineLine(self, input_line):
assert input_line.startswith('PDDM-DEFINE')
line = input_line[12:].strip()
match = _MACRO_RE.match(line)
if match is None or match.group(0) != line:
raise PDDMError('Failed to parse macro definition: "%s"' % input_line)
name = match.group('name')
args_str = match.group('args').strip()
args = []
if args_str:
for part in args_str.split(','):
arg = part.strip()
if arg == '':
raise PDDMError('Empty arg name in macro definition: "%s"'
% input_line)
if not _MACRO_ARG_NAME_RE.match(arg):
raise PDDMError('Invalid arg name "%s" in macro definition: "%s"'
% (arg, input_line))
if arg in args:
raise PDDMError('Arg name "%s" used more than once in macro'
' definition: "%s"' % (arg, input_line))
args.append(arg)
return (name, tuple(args))
def Expand(self, macro_ref_str):
match = _MACRO_RE.match(macro_ref_str)
if match is None or match.group(0) != macro_ref_str:
raise PDDMError('Failed to parse macro reference: "%s"' % macro_ref_str)
if match.group('name') not in self._macros:
raise PDDMError('No macro named "%s".' % match.group('name'))
return self._Expand(match, [], macro_ref_str)
def _FormatStack(self, macro_ref_stack):
result = ''
for _, macro_ref in reversed(macro_ref_stack):
result += '\n...while expanding "%s".' % macro_ref
return result
def _Expand(self, macro_ref_match, macro_stack, macro_ref_str=None):
if macro_ref_str is None:
macro_ref_str = macro_ref_match.group('macro_ref')
name = macro_ref_match.group('name')
for prev_name, prev_macro_ref in macro_stack:
if name == prev_name:
raise PDDMError('Found macro recursion, invoking "%s":%s' %
(macro_ref_str, self._FormatStack(macro_stack)))
macro = self._macros[name]
args_str = macro_ref_match.group('args').strip()
args = []
if args_str or len(macro.args):
args = [x.strip() for x in args_str.split(',')]
if len(args) != len(macro.args):
raise PDDMError('Expected %d args, got: "%s".%s' %
(len(macro.args), macro_ref_str,
self._FormatStack(macro_stack)))
result = self._ReplaceArgValues(macro, args, macro_ref_str, macro_stack)
new_macro_stack = macro_stack + [(name, macro_ref_str)]
while True:
eval_result = self._EvalMacrosRefs(result, new_macro_stack)
eval_result = eval_result.replace('##', '')
if eval_result == result:
break
result = eval_result
return result
def _ReplaceArgValues(self,
macro, arg_values, macro_ref_to_report, macro_stack):
if len(arg_values) == 0:
return macro.body
assert len(arg_values) == len(macro.args)
args = dict(list(zip(macro.args, arg_values)))
def _lookupArg(match):
val = args[match.group('name')]
opt = match.group('option')
if opt:
if opt == 'S': return ' ' * len(val)
elif opt == 'l': if val:
return val[0].lower() + val[1:]
else:
return val
elif opt == 'L': return val.lower()
elif opt == 'u': if val:
return val[0].upper() + val[1:]
else:
return val
elif opt == 'U': return val.upper()
else:
raise PDDMError('Unknown arg option "%s$%s" while expanding "%s".%s'
% (match.group('name'), match.group('option'),
macro_ref_to_report,
self._FormatStack(macro_stack)))
return val
macro_arg_ref_re = _MacroArgRefRe(macro.args)
return macro_arg_ref_re.sub(_lookupArg, macro.body)
def _EvalMacrosRefs(self, text, macro_stack):
macro_ref_re = _MacroRefRe(list(self._macros.keys()))
def _resolveMacro(match):
return self._Expand(match, macro_stack)
return macro_ref_re.sub(_resolveMacro, text)
class SourceFile(object):
def __init__(self, a_file, import_resolver=None):
self._sections = []
self._original_content = a_file.read()
self._import_resolver = import_resolver
self._processed_content = None
class SectionBase(object):
def __init__(self, first_line_num):
self._lines = []
self._first_line_num = first_line_num
def TryAppend(self, line, line_num):
assert False, "subclass should have overridden"
return (False, False)
def HitEOF(self):
pass
def BindMacroCollection(self, macro_collection):
pass
def Append(self, line):
self._lines.append(line)
@property
def lines(self):
return self._lines
@property
def num_lines_captured(self):
return len(self._lines)
@property
def first_line_num(self):
return self._first_line_num
@property
def first_line(self):
if not self._lines:
return ''
return self._lines[0]
@property
def text(self):
return '\n'.join(self.lines) + '\n'
class TextSection(SectionBase):
def TryAppend(self, line, line_num):
if line.startswith('//%PDDM'):
return (False, False)
self.Append(line)
return (True, True)
class ExpansionSection(SectionBase):
def __init__(self, first_line_num):
SourceFile.SectionBase.__init__(self, first_line_num)
self._macro_collection = None
def TryAppend(self, line, line_num):
if line.startswith('//%PDDM'):
directive = line.split(' ', 1)[0]
if directive == '//%PDDM-EXPAND':
self.Append(line)
return (True, True)
if directive == '//%PDDM-EXPAND-END':
assert self.num_lines_captured > 0
return (True, False)
raise PDDMError('Ran into directive ("%s", line %d) while in "%s".' %
(directive, line_num, self.first_line))
return (True, True)
def HitEOF(self):
raise PDDMError('Hit the end of the file while in "%s".' %
self.first_line)
def BindMacroCollection(self, macro_collection):
self._macro_collection = macro_collection
@property
def lines(self):
captured_lines = SourceFile.SectionBase.lines.fget(self)
directive_len = len('//%PDDM-EXPAND')
result = []
for line in captured_lines:
result.append(line)
if self._macro_collection:
result.extend([_GENERATED_CODE_LINE, ''])
macro = line[directive_len:].strip()
try:
expand_result = self._macro_collection.Expand(macro)
lines = [x.rstrip() for x in expand_result.split('\n')]
result.append('\n'.join(lines))
except PDDMError as e:
raise PDDMError('%s\n...while expanding "%s" from the section'
' that started:\n Line %d: %s' %
(e.message, macro,
self.first_line_num, self.first_line))
if len(captured_lines) == 1:
result.append('//%%PDDM-EXPAND-END %s' %
captured_lines[0][directive_len:].strip())
else:
result.append('//%%PDDM-EXPAND-END (%s expansions)' %
len(captured_lines))
return result
class DefinitionSection(SectionBase):
def TryAppend(self, line, line_num):
if not line.startswith('//%'):
return (False, False)
if line.startswith('//%PDDM'):
directive = line.split(' ', 1)[0]
if directive == "//%PDDM-EXPAND":
return False, False
if directive not in ('//%PDDM-DEFINE', '//%PDDM-DEFINE-END'):
raise PDDMError('Ran into directive ("%s", line %d) while in "%s".' %
(directive, line_num, self.first_line))
self.Append(line)
return (True, True)
def BindMacroCollection(self, macro_collection):
if macro_collection:
try:
macro_collection.ParseLines([x[3:] for x in self.lines])
except PDDMError as e:
raise PDDMError('%s\n...while parsing section that started:\n'
' Line %d: %s' %
(e.message, self.first_line_num, self.first_line))
class ImportDefinesSection(SectionBase):
def __init__(self, first_line_num, import_resolver):
SourceFile.SectionBase.__init__(self, first_line_num)
self._import_resolver = import_resolver
def TryAppend(self, line, line_num):
if not line.startswith('//%PDDM-IMPORT-DEFINES '):
return (False, False)
assert self.num_lines_captured == 0
self.Append(line)
return (True, False)
def BindMacroCollection(self, macro_colletion):
if not macro_colletion:
return
if self._import_resolver is None:
raise PDDMError('Got an IMPORT-DEFINES without a resolver (line %d):'
' "%s".' % (self.first_line_num, self.first_line))
import_name = self.first_line.split(' ', 1)[1].strip()
imported_file = self._import_resolver(import_name)
if imported_file is None:
raise PDDMError('Resolver failed to find "%s" (line %d):'
' "%s".' %
(import_name, self.first_line_num, self.first_line))
try:
imported_src_file = SourceFile(imported_file, self._import_resolver)
imported_src_file._ParseFile()
for section in imported_src_file._sections:
section.BindMacroCollection(macro_colletion)
except PDDMError as e:
raise PDDMError('%s\n...while importing defines:\n'
' Line %d: %s' %
(e.message, self.first_line_num, self.first_line))
def _ParseFile(self):
self._sections = []
lines = self._original_content.splitlines()
cur_section = None
for line_num, line in enumerate(lines, 1):
if not cur_section:
cur_section = self._MakeSection(line, line_num)
was_added, accept_more = cur_section.TryAppend(line, line_num)
if not was_added:
cur_section = self._MakeSection(line, line_num)
was_added, accept_more = cur_section.TryAppend(line, line_num)
assert was_added
if not accept_more:
cur_section = None
if cur_section:
cur_section.HitEOF()
def _MakeSection(self, line, line_num):
if not line.startswith('//%PDDM'):
section = self.TextSection(line_num)
else:
directive = line.split(' ', 1)[0]
if directive == '//%PDDM-EXPAND':
section = self.ExpansionSection(line_num)
elif directive == '//%PDDM-DEFINE':
section = self.DefinitionSection(line_num)
elif directive == '//%PDDM-IMPORT-DEFINES':
section = self.ImportDefinesSection(line_num, self._import_resolver)
else:
raise PDDMError('Unexpected line %d: "%s".' % (line_num, line))
self._sections.append(section)
return section
def ProcessContent(self, strip_expansion=False):
self._ParseFile()
if strip_expansion:
collection = None
else:
collection = MacroCollection()
for section in self._sections:
section.BindMacroCollection(collection)
result = ''
for section in self._sections:
result += section.text
self._processed_content = result
@property
def original_content(self):
return self._original_content
@property
def processed_content(self):
return self._processed_content
def main(args):
usage = '%prog [OPTIONS] PATH ...'
description = (
'Processes PDDM directives in the given paths and write them back out.'
)
parser = optparse.OptionParser(usage=usage, description=description)
parser.add_option('--dry-run',
default=False, action='store_true',
help='Don\'t write back to the file(s), just report if the'
' contents needs an update and exit with a value of 1.')
parser.add_option('--verbose',
default=False, action='store_true',
help='Reports is a file is already current.')
parser.add_option('--collapse',
default=False, action='store_true',
help='Removes all the generated code.')
opts, extra_args = parser.parse_args(args)
if not extra_args:
parser.error('Need at least one file to process')
result = 0
for a_path in extra_args:
if not os.path.exists(a_path):
sys.stderr.write('ERROR: File not found: %s\n' % a_path)
return 100
def _ImportResolver(name):
a_dir = os.path.dirname(a_path)
import_path = os.path.join(a_dir, name)
if not os.path.exists(import_path):
return None
return open(import_path, 'r')
with open(a_path, 'r') as f:
src_file = SourceFile(f, _ImportResolver)
try:
src_file.ProcessContent(strip_expansion=opts.collapse)
except PDDMError as e:
sys.stderr.write('ERROR: %s\n...While processing "%s"\n' %
(e.message, a_path))
return 101
if src_file.processed_content != src_file.original_content:
if not opts.dry_run:
print('Updating for "%s".' % a_path)
with open(a_path, 'w') as f:
f.write(src_file.processed_content)
else:
print('Update needed for "%s".' % a_path)
result = 1
elif opts.verbose:
print('No update for "%s".' % a_path)
return result
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))