]>
Commit | Line | Data |
---|---|---|
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 | |
11 | you use it, to process conditional statements, variables, iteration | |
12 | statements, 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 | ||
65 | import os | |
66 | import platform | |
67 | import re | |
68 | import subprocess | |
69 | import sys | |
70 | from typing import Optional | |
71 | from xml.dom import minidom | |
72 | ||
73 | ||
74 | class 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 | ||
264 | def 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 | ||
272 | def 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 | ||
278 | def 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 | ||
292 | if __name__ == "__main__": | |
293 | main() |