blob: a9c67002df569d3750f468c1416623f4ebe8a92e [file] [log] [blame]
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A module for parsing TF command files.
This module is a python port of TF CommandFileParser.
"""
import logging
import os.path
import re
import shlex
# A regex to match macros in command lines.
_MACRO_PATTERN = re.compile(r"([a-z][a-z0-9_-]*)\(\)", re.I)
def _ShouldParseLine(line):
"""Checks whether a line should be parsed."""
if not line or line.startswith("#"):
return False
return True
def _IsMacro(command):
"""Checks whether a command is a macro."""
if len(command) >= 4 and command[0] == "MACRO" and command[2] == "=":
return True
return False
def _IsLongMacro(command):
"""Checks whether a command is a long macro."""
if len(command) == 3 and command[0] == "LONG" and command[1] == "MACRO":
return True
return False
def _IsIncludeDirective(command):
"""Checke where a command is an include directive."""
if len(command) == 2 and command[0] == "INCLUDE":
return True
return False
class Parser(object):
"""A class for parsing command files."""
def __init__(self):
self._filenames = []
self._commands = []
self._macros = {}
self._long_macros = {}
def Parse(self, filename):
"""Reads and parses a command file.
Args:
filename: the name of a command file including necessary path.
"""
self._ScanFile(filename)
self._ExpandMacros()
def _ScanFile(self, filename):
"""Scans a file and parse lines."""
logging.info("Scanning %s", filename)
if filename in self._filenames:
return
self._filenames.append(filename)
with open(filename, "r") as f:
while True:
line = f.readline()
if not line:
break
line = line.strip()
if not _ShouldParseLine(line):
continue
command = shlex.split(line)
if _IsMacro(command):
name = command[1]
macro = command[3:]
if name in self._macros:
logging.warning(
"Overwrote short macro '%s' while parsing file %s",
name,
filename
)
logging.warning(
"value '%s' replaced previous value '%s'",
self._macros[name],
macro
)
self._macros[name] = macro
elif _IsLongMacro(command):
name = command[2]
macro = []
while True:
line = f.readline()
if not line:
break
line = line.strip()
if not _ShouldParseLine(line):
continue
if line == "END MACRO":
break
command = shlex.split(line)
macro.append(command)
if name in self._long_macros:
logging.warning(
"Overwrote long macro '%s' while parsing file %s",
name,
filename
)
logging.warning(
"%d-line definition replaced previous %d-line definition",
len(self._long_macros[name]),
len(macro)
)
self._long_macros[name] = macro
elif _IsIncludeDirective(command):
include_filename = command[1]
if not os.path.isabs(include_filename):
include_filename = os.path.normpath(
os.path.join(os.path.dirname(filename), include_filename))
logging.debug("Including %s", include_filename)
self._ScanFile(include_filename)
else:
self._commands.append(command)
def _ExpandMacros(self):
"""Expands macros in commands."""
has_macro = True
iteration = 0
while has_macro:
logging.debug("Macro expansion iteration %d", iteration)
has_macro = False
index = 0
while index < len(self._commands):
command = self._commands[index]
if self._ExpandShortMacro(command):
has_macro = True
new_commands = self._ExpandLongMacro(command, not has_macro)
if new_commands:
has_macro = True
del self._commands[index]
for new_command in new_commands:
self._commands.insert(index, new_command)
index += 1
else:
index += 1
iteration += 1
def _ExpandShortMacro(self, command):
"""Expands short macros in a command."""
has_macro = False
index = 0
while index < len(command):
token = command[index]
match = _MACRO_PATTERN.match(token)
if match and match.group(1) in self._macros:
has_macro = True
name = match.group(1)
del command[index]
for macro_token in self._macros[name]:
command.insert(index, macro_token)
index += 1
else:
index += 1
return has_macro
def _ExpandLongMacro(self, command, check_unknown_macro):
"""Expands the first long macro in a command."""
index = 0
while index < len(command):
token = command[index]
match = _MACRO_PATTERN.match(token)
if match:
name = match.group(1)
if name not in self._long_macros:
if check_unknown_macro:
raise Exception(
"Macro call '%s' does not match any macro definitions" % name
)
else:
return []
new_commands = []
prefix = command[0:index]
suffix = command[index:]
del suffix[0]
for macro_line in self._long_macros[name]:
new_command = []
new_command.extend(prefix)
new_command.extend(macro_line)
new_command.extend(suffix)
new_commands.append(new_command)
return new_commands
index += 1
return []
def GetCommands(self):
"""Returns parsed commands.
Returns:
a list of commands.
"""
return self._commands