1#!/usr/bin/env python3 2# 3# Copyright (c) 2017-2019 Tony Su 4# Copyright (c) 2023 Red Hat, Inc. 5# 6# SPDX-License-Identifier: MIT 7# 8# Adapted from https://github.com/peitaosu/XML-Preprocessor 9# 10"""This is a XML Preprocessor which can be used to process your XML file before 11you use it, to process conditional statements, variables, iteration 12statements, error/warning, execute command, etc. 13 14## XML Schema 15 16### Include Files 17``` 18<?include path/to/file ?> 19``` 20 21### Variables 22``` 23$(env.EnvironmentVariable) 24 25$(sys.SystemVariable) 26 27$(var.CustomVariable) 28``` 29 30### Conditional Statements 31``` 32<?if ?> 33 34<?ifdef ?> 35 36<?ifndef ?> 37 38<?else?> 39 40<?elseif ?> 41 42<?endif?> 43``` 44 45### Iteration Statements 46``` 47<?foreach VARNAME in 1;2;3?> 48 $(var.VARNAME) 49<?endforeach?> 50``` 51 52### Errors and Warnings 53``` 54<?error "This is error message!" ?> 55 56<?warning "This is warning message!" ?> 57``` 58 59### Commands 60``` 61<? cmd "echo hello world" ?> 62``` 63""" 64 65import os 66import platform 67import re 68import subprocess 69import sys 70from typing import Optional 71from xml.dom import minidom 72 73 74class Preprocessor(): 75 """This class holds the XML preprocessing state""" 76 77 def __init__(self): 78 self.sys_vars = { 79 "ARCH": platform.architecture()[0], 80 "SOURCE": os.path.abspath(__file__), 81 "CURRENT": os.getcwd(), 82 } 83 self.cus_vars = {} 84 85 def _pp_include(self, xml_str: str) -> str: 86 include_regex = r"(<\?include([\w\s\\/.:_-]+)\s*\?>)" 87 matches = re.findall(include_regex, xml_str) 88 for group_inc, group_xml in matches: 89 inc_file_path = group_xml.strip() 90 with open(inc_file_path, "r", encoding="utf-8") as inc_file: 91 inc_file_content = inc_file.read() 92 xml_str = xml_str.replace(group_inc, inc_file_content) 93 return xml_str 94 95 def _pp_env_var(self, xml_str: str) -> str: 96 envvar_regex = r"(\$\(env\.(\w+)\))" 97 matches = re.findall(envvar_regex, xml_str) 98 for group_env, group_var in matches: 99 xml_str = xml_str.replace(group_env, os.environ[group_var]) 100 return xml_str 101 102 def _pp_sys_var(self, xml_str: str) -> str: 103 sysvar_regex = r"(\$\(sys\.(\w+)\))" 104 matches = re.findall(sysvar_regex, xml_str) 105 for group_sys, group_var in matches: 106 xml_str = xml_str.replace(group_sys, self.sys_vars[group_var]) 107 return xml_str 108 109 def _pp_cus_var(self, xml_str: str) -> str: 110 define_regex = r"(<\?define\s*(\w+)\s*=\s*([\w\s\"]+)\s*\?>)" 111 matches = re.findall(define_regex, xml_str) 112 for group_def, group_name, group_var in matches: 113 group_name = group_name.strip() 114 group_var = group_var.strip().strip("\"") 115 self.cus_vars[group_name] = group_var 116 xml_str = xml_str.replace(group_def, "") 117 cusvar_regex = r"(\$\(var\.(\w+)\))" 118 matches = re.findall(cusvar_regex, xml_str) 119 for group_cus, group_var in matches: 120 xml_str = xml_str.replace( 121 group_cus, 122 self.cus_vars.get(group_var, "") 123 ) 124 return xml_str 125 126 def _pp_foreach(self, xml_str: str) -> str: 127 foreach_regex = r"(<\?foreach\s+(\w+)\s+in\s+([\w;]+)\s*\?>(.*)<\?endforeach\?>)" 128 matches = re.findall(foreach_regex, xml_str) 129 for group_for, group_name, group_vars, group_text in matches: 130 group_texts = "" 131 for var in group_vars.split(";"): 132 self.cus_vars[group_name] = var 133 group_texts += self._pp_cus_var(group_text) 134 xml_str = xml_str.replace(group_for, group_texts) 135 return xml_str 136 137 def _pp_error_warning(self, xml_str: str) -> str: 138 error_regex = r"<\?error\s*\"([^\"]+)\"\s*\?>" 139 matches = re.findall(error_regex, xml_str) 140 for group_var in matches: 141 raise RuntimeError("[Error]: " + group_var) 142 warning_regex = r"(<\?warning\s*\"([^\"]+)\"\s*\?>)" 143 matches = re.findall(warning_regex, xml_str) 144 for group_wrn, group_var in matches: 145 print("[Warning]: " + group_var) 146 xml_str = xml_str.replace(group_wrn, "") 147 return xml_str 148 149 def _pp_if_eval(self, xml_str: str) -> str: 150 ifelif_regex = ( 151 r"(<\?(if|elseif)\s*([^\"\s=<>!]+)\s*([!=<>]+)\s*\"*([^\"=<>!]+)\"*\s*\?>)" 152 ) 153 matches = re.findall(ifelif_regex, xml_str) 154 for ifelif, tag, left, operator, right in matches: 155 if "<" in operator or ">" in operator: 156 result = eval(f"{left} {operator} {right}") 157 else: 158 result = eval(f'"{left}" {operator} "{right}"') 159 xml_str = xml_str.replace(ifelif, f"<?{tag} {result}?>") 160 return xml_str 161 162 def _pp_ifdef_ifndef(self, xml_str: str) -> str: 163 ifndef_regex = r"(<\?(ifdef|ifndef)\s*([\w]+)\s*\?>)" 164 matches = re.findall(ifndef_regex, xml_str) 165 for group_ifndef, group_tag, group_var in matches: 166 if group_tag == "ifdef": 167 result = group_var in self.cus_vars 168 else: 169 result = group_var not in self.cus_vars 170 xml_str = xml_str.replace(group_ifndef, f"<?if {result}?>") 171 return xml_str 172 173 def _pp_if_elseif(self, xml_str: str) -> str: 174 if_elif_else_regex = ( 175 r"(<\?if\s(True|False)\?>" 176 r"(.*?)" 177 r"<\?elseif\s(True|False)\?>" 178 r"(.*?)" 179 r"<\?else\?>" 180 r"(.*?)" 181 r"<\?endif\?>)" 182 ) 183 if_else_regex = ( 184 r"(<\?if\s(True|False)\?>" 185 r"(.*?)" 186 r"<\?else\?>" 187 r"(.*?)" 188 r"<\?endif\?>)" 189 ) 190 if_regex = r"(<\?if\s(True|False)\?>(.*?)<\?endif\?>)" 191 matches = re.findall(if_elif_else_regex, xml_str, re.DOTALL) 192 for (group_full, group_if, group_if_elif, group_elif, 193 group_elif_else, group_else) in matches: 194 result = "" 195 if group_if == "True": 196 result = group_if_elif 197 elif group_elif == "True": 198 result = group_elif_else 199 else: 200 result = group_else 201 xml_str = xml_str.replace(group_full, result) 202 matches = re.findall(if_else_regex, xml_str, re.DOTALL) 203 for group_full, group_if, group_if_else, group_else in matches: 204 result = "" 205 if group_if == "True": 206 result = group_if_else 207 else: 208 result = group_else 209 xml_str = xml_str.replace(group_full, result) 210 matches = re.findall(if_regex, xml_str, re.DOTALL) 211 for group_full, group_if, group_text in matches: 212 result = "" 213 if group_if == "True": 214 result = group_text 215 xml_str = xml_str.replace(group_full, result) 216 return xml_str 217 218 def _pp_command(self, xml_str: str) -> str: 219 cmd_regex = r"(<\?cmd\s*\"([^\"]+)\"\s*\?>)" 220 matches = re.findall(cmd_regex, xml_str) 221 for group_cmd, group_exec in matches: 222 output = subprocess.check_output( 223 group_exec, shell=True, 224 text=True, stderr=subprocess.STDOUT 225 ) 226 xml_str = xml_str.replace(group_cmd, output) 227 return xml_str 228 229 def _pp_blanks(self, xml_str: str) -> str: 230 right_blank_regex = r">[\n\s\t\r]*" 231 left_blank_regex = r"[\n\s\t\r]*<" 232 xml_str = re.sub(right_blank_regex, ">", xml_str) 233 xml_str = re.sub(left_blank_regex, "<", xml_str) 234 return xml_str 235 236 def preprocess(self, xml_str: str) -> str: 237 fns = [ 238 self._pp_blanks, 239 self._pp_include, 240 self._pp_foreach, 241 self._pp_env_var, 242 self._pp_sys_var, 243 self._pp_cus_var, 244 self._pp_if_eval, 245 self._pp_ifdef_ifndef, 246 self._pp_if_elseif, 247 self._pp_command, 248 self._pp_error_warning, 249 ] 250 251 while True: 252 changed = False 253 for func in fns: 254 out_xml = func(xml_str) 255 if not changed and out_xml != xml_str: 256 changed = True 257 xml_str = out_xml 258 if not changed: 259 break 260 261 return xml_str 262 263 264def preprocess_xml(path: str) -> str: 265 with open(path, "r", encoding="utf-8") as original_file: 266 input_xml = original_file.read() 267 268 proc = Preprocessor() 269 return proc.preprocess(input_xml) 270 271 272def save_xml(xml_str: str, path: Optional[str]): 273 xml = minidom.parseString(xml_str) 274 with open(path, "w", encoding="utf-8") if path else sys.stdout as output_file: 275 output_file.write(xml.toprettyxml()) 276 277 278def main(): 279 if len(sys.argv) < 2: 280 print("Usage: xml-preprocessor input.xml [output.xml]") 281 sys.exit(1) 282 283 output_file = None 284 if len(sys.argv) == 3: 285 output_file = sys.argv[2] 286 287 input_file = sys.argv[1] 288 output_xml = preprocess_xml(input_file) 289 save_xml(output_xml, output_file) 290 291 292if __name__ == "__main__": 293 main() 294