| #!/usr/bin/env python3 | |
| # | |
| # Copyright (c) 2017-2019 Tony Su | |
| # Copyright (c) 2023 Red Hat, Inc. | |
| # | |
| # SPDX-License-Identifier: MIT | |
| # | |
| # Adapted from https://github.com/peitaosu/XML-Preprocessor | |
| # | |
| """This is a XML Preprocessor which can be used to process your XML file before | |
| you use it, to process conditional statements, variables, iteration | |
| statements, error/warning, execute command, etc. | |
| ## XML Schema | |
| ### Include Files | |
| ``` | |
| <?include path/to/file ?> | |
| ``` | |
| ### Variables | |
| ``` | |
| $(env.EnvironmentVariable) | |
| $(sys.SystemVariable) | |
| $(var.CustomVariable) | |
| ``` | |
| ### Conditional Statements | |
| ``` | |
| <?if ?> | |
| <?ifdef ?> | |
| <?ifndef ?> | |
| <?else?> | |
| <?elseif ?> | |
| <?endif?> | |
| ``` | |
| ### Iteration Statements | |
| ``` | |
| <?foreach VARNAME in 1;2;3?> | |
| $(var.VARNAME) | |
| <?endforeach?> | |
| ``` | |
| ### Errors and Warnings | |
| ``` | |
| <?error "This is error message!" ?> | |
| <?warning "This is warning message!" ?> | |
| ``` | |
| ### Commands | |
| ``` | |
| <? cmd "echo hello world" ?> | |
| ``` | |
| """ | |
| import os | |
| import platform | |
| import re | |
| import subprocess | |
| import sys | |
| from typing import Optional | |
| from xml.dom import minidom | |
| class Preprocessor(): | |
| """This class holds the XML preprocessing state""" | |
| def __init__(self): | |
| self.sys_vars = { | |
| "ARCH": platform.architecture()[0], | |
| "SOURCE": os.path.abspath(__file__), | |
| "CURRENT": os.getcwd(), | |
| } | |
| self.cus_vars = {} | |
| def _pp_include(self, xml_str: str) -> str: | |
| include_regex = r"(<\?include([\w\s\\/.:_-]+)\s*\?>)" | |
| matches = re.findall(include_regex, xml_str) | |
| for group_inc, group_xml in matches: | |
| inc_file_path = group_xml.strip() | |
| with open(inc_file_path, "r", encoding="utf-8") as inc_file: | |
| inc_file_content = inc_file.read() | |
| xml_str = xml_str.replace(group_inc, inc_file_content) | |
| return xml_str | |
| def _pp_env_var(self, xml_str: str) -> str: | |
| envvar_regex = r"(\$\(env\.(\w+)\))" | |
| matches = re.findall(envvar_regex, xml_str) | |
| for group_env, group_var in matches: | |
| xml_str = xml_str.replace(group_env, os.environ[group_var]) | |
| return xml_str | |
| def _pp_sys_var(self, xml_str: str) -> str: | |
| sysvar_regex = r"(\$\(sys\.(\w+)\))" | |
| matches = re.findall(sysvar_regex, xml_str) | |
| for group_sys, group_var in matches: | |
| xml_str = xml_str.replace(group_sys, self.sys_vars[group_var]) | |
| return xml_str | |
| def _pp_cus_var(self, xml_str: str) -> str: | |
| define_regex = r"(<\?define\s*(\w+)\s*=\s*([\w\s\"]+)\s*\?>)" | |
| matches = re.findall(define_regex, xml_str) | |
| for group_def, group_name, group_var in matches: | |
| group_name = group_name.strip() | |
| group_var = group_var.strip().strip("\"") | |
| self.cus_vars[group_name] = group_var | |
| xml_str = xml_str.replace(group_def, "") | |
| cusvar_regex = r"(\$\(var\.(\w+)\))" | |
| matches = re.findall(cusvar_regex, xml_str) | |
| for group_cus, group_var in matches: | |
| xml_str = xml_str.replace( | |
| group_cus, | |
| self.cus_vars.get(group_var, "") | |
| ) | |
| return xml_str | |
| def _pp_foreach(self, xml_str: str) -> str: | |
| foreach_regex = r"(<\?foreach\s+(\w+)\s+in\s+([\w;]+)\s*\?>(.*)<\?endforeach\?>)" | |
| matches = re.findall(foreach_regex, xml_str) | |
| for group_for, group_name, group_vars, group_text in matches: | |
| group_texts = "" | |
| for var in group_vars.split(";"): | |
| self.cus_vars[group_name] = var | |
| group_texts += self._pp_cus_var(group_text) | |
| xml_str = xml_str.replace(group_for, group_texts) | |
| return xml_str | |
| def _pp_error_warning(self, xml_str: str) -> str: | |
| error_regex = r"<\?error\s*\"([^\"]+)\"\s*\?>" | |
| matches = re.findall(error_regex, xml_str) | |
| for group_var in matches: | |
| raise RuntimeError("[Error]: " + group_var) | |
| warning_regex = r"(<\?warning\s*\"([^\"]+)\"\s*\?>)" | |
| matches = re.findall(warning_regex, xml_str) | |
| for group_wrn, group_var in matches: | |
| print("[Warning]: " + group_var) | |
| xml_str = xml_str.replace(group_wrn, "") | |
| return xml_str | |
| def _pp_if_eval(self, xml_str: str) -> str: | |
| ifelif_regex = ( | |
| r"(<\?(if|elseif)\s*([^\"\s=<>!]+)\s*([!=<>]+)\s*\"*([^\"=<>!]+)\"*\s*\?>)" | |
| ) | |
| matches = re.findall(ifelif_regex, xml_str) | |
| for ifelif, tag, left, operator, right in matches: | |
| if "<" in operator or ">" in operator: | |
| result = eval(f"{left} {operator} {right}") | |
| else: | |
| result = eval(f'"{left}" {operator} "{right}"') | |
| xml_str = xml_str.replace(ifelif, f"<?{tag} {result}?>") | |
| return xml_str | |
| def _pp_ifdef_ifndef(self, xml_str: str) -> str: | |
| ifndef_regex = r"(<\?(ifdef|ifndef)\s*([\w]+)\s*\?>)" | |
| matches = re.findall(ifndef_regex, xml_str) | |
| for group_ifndef, group_tag, group_var in matches: | |
| if group_tag == "ifdef": | |
| result = group_var in self.cus_vars | |
| else: | |
| result = group_var not in self.cus_vars | |
| xml_str = xml_str.replace(group_ifndef, f"<?if {result}?>") | |
| return xml_str | |
| def _pp_if_elseif(self, xml_str: str) -> str: | |
| if_elif_else_regex = ( | |
| r"(<\?if\s(True|False)\?>" | |
| r"(.*?)" | |
| r"<\?elseif\s(True|False)\?>" | |
| r"(.*?)" | |
| r"<\?else\?>" | |
| r"(.*?)" | |
| r"<\?endif\?>)" | |
| ) | |
| if_else_regex = ( | |
| r"(<\?if\s(True|False)\?>" | |
| r"(.*?)" | |
| r"<\?else\?>" | |
| r"(.*?)" | |
| r"<\?endif\?>)" | |
| ) | |
| if_regex = r"(<\?if\s(True|False)\?>(.*?)<\?endif\?>)" | |
| matches = re.findall(if_elif_else_regex, xml_str, re.DOTALL) | |
| for (group_full, group_if, group_if_elif, group_elif, | |
| group_elif_else, group_else) in matches: | |
| result = "" | |
| if group_if == "True": | |
| result = group_if_elif | |
| elif group_elif == "True": | |
| result = group_elif_else | |
| else: | |
| result = group_else | |
| xml_str = xml_str.replace(group_full, result) | |
| matches = re.findall(if_else_regex, xml_str, re.DOTALL) | |
| for group_full, group_if, group_if_else, group_else in matches: | |
| result = "" | |
| if group_if == "True": | |
| result = group_if_else | |
| else: | |
| result = group_else | |
| xml_str = xml_str.replace(group_full, result) | |
| matches = re.findall(if_regex, xml_str, re.DOTALL) | |
| for group_full, group_if, group_text in matches: | |
| result = "" | |
| if group_if == "True": | |
| result = group_text | |
| xml_str = xml_str.replace(group_full, result) | |
| return xml_str | |
| def _pp_command(self, xml_str: str) -> str: | |
| cmd_regex = r"(<\?cmd\s*\"([^\"]+)\"\s*\?>)" | |
| matches = re.findall(cmd_regex, xml_str) | |
| for group_cmd, group_exec in matches: | |
| output = subprocess.check_output( | |
| group_exec, shell=True, | |
| text=True, stderr=subprocess.STDOUT | |
| ) | |
| xml_str = xml_str.replace(group_cmd, output) | |
| return xml_str | |
| def _pp_blanks(self, xml_str: str) -> str: | |
| right_blank_regex = r">[\n\s\t\r]*" | |
| left_blank_regex = r"[\n\s\t\r]*<" | |
| xml_str = re.sub(right_blank_regex, ">", xml_str) | |
| xml_str = re.sub(left_blank_regex, "<", xml_str) | |
| return xml_str | |
| def preprocess(self, xml_str: str) -> str: | |
| fns = [ | |
| self._pp_blanks, | |
| self._pp_include, | |
| self._pp_foreach, | |
| self._pp_env_var, | |
| self._pp_sys_var, | |
| self._pp_cus_var, | |
| self._pp_if_eval, | |
| self._pp_ifdef_ifndef, | |
| self._pp_if_elseif, | |
| self._pp_command, | |
| self._pp_error_warning, | |
| ] | |
| while True: | |
| changed = False | |
| for func in fns: | |
| out_xml = func(xml_str) | |
| if not changed and out_xml != xml_str: | |
| changed = True | |
| xml_str = out_xml | |
| if not changed: | |
| break | |
| return xml_str | |
| def preprocess_xml(path: str) -> str: | |
| with open(path, "r", encoding="utf-8") as original_file: | |
| input_xml = original_file.read() | |
| proc = Preprocessor() | |
| return proc.preprocess(input_xml) | |
| def save_xml(xml_str: str, path: Optional[str]): | |
| xml = minidom.parseString(xml_str) | |
| with open(path, "w", encoding="utf-8") if path else sys.stdout as output_file: | |
| output_file.write(xml.toprettyxml()) | |
| def main(): | |
| if len(sys.argv) < 2: | |
| print("Usage: xml-preprocessor input.xml [output.xml]") | |
| sys.exit(1) | |
| output_file = None | |
| if len(sys.argv) == 3: | |
| output_file = sys.argv[2] | |
| input_file = sys.argv[1] | |
| output_xml = preprocess_xml(input_file) | |
| save_xml(output_xml, output_file) | |
| if __name__ == "__main__": | |
| main() |