1#!/usr/local/bin/python3.8 2#-------------------------------------------------------------------- 3# 4# preproc.py 5# 6# General purpose macro preprocessor 7# 8#-------------------------------------------------------------------- 9# Usage: 10# 11# preproc.py input_file [output_file] [-D<variable> ...] 12# 13# Where <variable> may be a keyword or a key=value pair 14# 15# Syntax: Basically like cpp. However, this preprocessor handles 16# only a limited set of keywords, so it does not otherwise mangle 17# the file in the belief that it must be C code. Handling of boolean 18# relations is important, so these are thoroughly defined (see below) 19# 20# #if defined(<variable>) [...] 21# #ifdef <variable> 22# #ifndef <variable> 23# #elseif <variable> 24# #else 25# #endif 26# 27# #define <variable> [...] 28# #undef <variable> 29# 30# #include <filename> 31# 32# <variable> may be 33# <keyword> 34# <keyword>=<value> 35# 36# <keyword> without '=' is effectively the same as <keyword>=1 37# Lack of a keyword is equivalent to <keyword>=0, in a conditional. 38# 39# Boolean operators (in order of precedence): 40# ! NOT 41# && AND 42# || OR 43# 44# Comments: 45# Most comments (C-like or Tcl-like) are output as-is. A 46# line beginning with "###" is treated as a preprocessor 47# comment and is not copied to the output. 48# 49# Examples; 50# #if defined(X) || defined(Y) 51# #else 52# #if defined(Z) 53# #endif 54#-------------------------------------------------------------------- 55 56import re 57import sys 58 59def solve_statement(condition): 60 61 defrex = re.compile('defined[ \t]*\(([^\)]+)\)') 62 orrex = re.compile('(.+)\|\|(.+)') 63 andrex = re.compile('(.+)&&(.+)') 64 notrex = re.compile('!([^&\|]+)') 65 parenrex = re.compile('\(([^\)]+)\)') 66 leadspacerex = re.compile('^[ \t]+(.*)') 67 endspacerex = re.compile('(.*)[ \t]+$') 68 69 matchfound = True 70 while matchfound: 71 matchfound = False 72 73 # Search for defined(K) (K must be a single keyword) 74 # If the keyword was defined, then it should have been replaced by 1 75 lmatch = defrex.search(condition) 76 if lmatch: 77 key = lmatch.group(1) 78 if key == 1 or key == '1' or key == True: 79 repl = 1 80 else: 81 repl = 0 82 83 condition = defrex.sub(str(repl), condition) 84 matchfound = True 85 86 # Search for (X) recursively 87 lmatch = parenrex.search(condition) 88 if lmatch: 89 repl = solve_statement(lmatch.group(1)) 90 condition = parenrex.sub(str(repl), condition) 91 matchfound = True 92 93 # Search for !X recursively 94 lmatch = notrex.search(condition) 95 if lmatch: 96 only = solve_statement(lmatch.group(1)) 97 if only == '1': 98 repl = '0' 99 else: 100 repl = '1' 101 condition = notrex.sub(str(repl), condition) 102 matchfound = True 103 104 # Search for A&&B recursively 105 lmatch = andrex.search(condition) 106 if lmatch: 107 first = solve_statement(lmatch.group(1)) 108 second = solve_statement(lmatch.group(2)) 109 if first == '1' and second == '1': 110 repl = '1' 111 else: 112 repl = '0' 113 condition = andrex.sub(str(repl), condition) 114 matchfound = True 115 116 # Search for A||B recursively 117 lmatch = orrex.search(condition) 118 if lmatch: 119 first = solve_statement(lmatch.group(1)) 120 second = solve_statement(lmatch.group(2)) 121 if first == '1' or second == '1': 122 repl = '1' 123 else: 124 repl = '0' 125 condition = orrex.sub(str(repl), condition) 126 matchfound = True 127 128 # Remove whitespace 129 lmatch = leadspacerex.match(condition) 130 if lmatch: 131 condition = lmatch.group(1) 132 lmatch = endspacerex.match(condition) 133 if lmatch: 134 condition = lmatch.group(1) 135 136 return condition 137 138def solve_condition(condition, keys, defines, keyrex): 139 # Do definition replacement on the conditional 140 for keyword in keys: 141 condition = keyrex[keyword].sub(defines[keyword], condition) 142 143 value = solve_statement(condition) 144 if value == '1': 145 return 1 146 else: 147 return 0 148 149def runpp(keys, keyrex, defines, ccomm, incdirs, inputfile, ofile): 150 151 includerex = re.compile('^[ \t]*#include[ \t]+"*([^ \t\n\r"]+)') 152 definerex = re.compile('^[ \t]*#define[ \t]+([^ \t]+)[ \t]+(.+)') 153 defrex = re.compile('^[ \t]*#define[ \t]+([^ \t\n\r]+)') 154 undefrex = re.compile('^[ \t]*#undef[ \t]+([^ \t\n\r]+)') 155 ifdefrex = re.compile('^[ \t]*#ifdef[ \t]+(.+)') 156 ifndefrex = re.compile('^[ \t]*#ifndef[ \t]+(.+)') 157 ifrex = re.compile('^[ \t]*#if[ \t]+(.+)') 158 elseifrex = re.compile('^[ \t]*#elseif[ \t]+(.+)') 159 elserex = re.compile('^[ \t]*#else') 160 endifrex = re.compile('^[ \t]*#endif') 161 commentrex = re.compile('^###[^#]*$') 162 ccstartrex = re.compile('/\*') # C-style comment start 163 ccendrex = re.compile('\*/') # C-style comment end 164 165 badifrex = re.compile('^[ \t]*#if[ \t]*.*') 166 badelserex = re.compile('^[ \t]*#else[ \t]*.*') 167 168 # This code is not designed to operate on huge files. Neither is it designed to be 169 # efficient. 170 171 # ifblock state: 172 # -1 : not in an if/else block 173 # 0 : no condition satisfied yet 174 # 1 : condition satisfied 175 # 2 : condition was handled, waiting for endif 176 177 ifile = False 178 try: 179 ifile = open(inputfile, 'r') 180 except FileNotFoundError: 181 for dir in incdirs: 182 try: 183 ifile = open(dir + '/' + inputfile, 'r') 184 except FileNotFoundError: 185 pass 186 else: 187 break 188 189 if not ifile: 190 print("Error: Cannot open file " + inputfile + " for reading.\n") 191 return 192 193 ccblock = -1 194 ifblock = -1 195 ifstack = [] 196 lineno = 0 197 198 filetext = ifile.readlines() 199 for line in filetext: 200 lineno += 1 201 202 # C-style comments override everything else 203 if ccomm: 204 if ccblock == -1: 205 pmatch = ccstartrex.search(line) 206 if pmatch: 207 ematch = ccendrex.search(line[pmatch.end(0):]) 208 if ematch: 209 line = line[0:pmatch.start(0)] + line[ematch.end(0)+2:] 210 else: 211 line = line[0:pmatch.start(0)] 212 ccblock = 1 213 elif ccblock == 1: 214 ematch = ccendrex.search(line) 215 if ematch: 216 line = line[ematch.end(0)+2:] 217 ccblock = -1 218 else: 219 continue 220 221 # Ignore lines beginning with "###" 222 pmatch = commentrex.match(line) 223 if pmatch: 224 continue 225 226 # Handle include. Note that this code does not expect or 227 # handle 'if' blocks that cross file boundaries. 228 pmatch = includerex.match(line) 229 if pmatch: 230 inclfile = pmatch.group(1) 231 runpp(keys, keyrex, defines, ccomm, incdirs, inclfile, ofile) 232 continue 233 234 # Handle define (with value) 235 pmatch = definerex.match(line) 236 if pmatch: 237 condition = pmatch.group(1) 238 value = pmatch.group(2) 239 defines[condition] = value 240 keyrex[condition] = re.compile(condition) 241 if condition not in keys: 242 keys.append(condition) 243 continue 244 245 # Handle define (simple case, no value) 246 pmatch = defrex.match(line) 247 if pmatch: 248 condition = pmatch.group(1) 249 print("Defrex condition is " + condition) 250 defines[condition] = '1' 251 keyrex[condition] = re.compile(condition) 252 if condition not in keys: 253 keys.append(condition) 254 print("Defrex value is " + defines[condition]) 255 continue 256 257 # Handle undef 258 pmatch = undefrex.match(line) 259 if pmatch: 260 condition = pmatch.group(1) 261 if condition in keys: 262 defines.pop(condition) 263 keyrex.pop(condition) 264 keys.remove(condition) 265 continue 266 267 # Handle ifdef 268 pmatch = ifdefrex.match(line) 269 if pmatch: 270 if ifblock != -1: 271 ifstack.append(ifblock) 272 273 if ifblock == 1 or ifblock == -1: 274 condition = pmatch.group(1) 275 ifblock = solve_condition(condition, keys, defines, keyrex) 276 else: 277 ifblock = 2 278 continue 279 280 # Handle ifndef 281 pmatch = ifndefrex.match(line) 282 if pmatch: 283 if ifblock != -1: 284 ifstack.append(ifblock) 285 286 if ifblock == 1 or ifblock == -1: 287 condition = pmatch.group(1) 288 ifblock = solve_condition(condition, keys, defines, keyrex) 289 ifblock = 1 if ifblock == 0 else 0 290 else: 291 ifblock = 2 292 continue 293 294 # Handle if 295 pmatch = ifrex.match(line) 296 if pmatch: 297 if ifblock != -1: 298 ifstack.append(ifblock) 299 300 if ifblock == 1 or ifblock == -1: 301 condition = pmatch.group(1) 302 ifblock = solve_condition(condition, keys, defines, keyrex) 303 else: 304 ifblock = 2 305 continue 306 307 # Handle elseif 308 pmatch = elseifrex.match(line) 309 if pmatch: 310 if ifblock == -1: 311 print("Error: #elseif without preceding #if at line " + str(lineno) + ".") 312 ifblock = 0 313 314 if ifblock == 1: 315 ifblock = 2 316 elif ifblock != 2: 317 condition = pmatch.group(1) 318 ifblock = solve_condition(condition, keys, defines, keyrex) 319 continue 320 321 # Handle else 322 pmatch = elserex.match(line) 323 if pmatch: 324 if ifblock == -1: 325 print("Error: #else without preceding #if at line " + str(lineno) + ".") 326 ifblock = 0 327 328 if ifblock == 1: 329 ifblock = 2 330 elif ifblock == 0: 331 ifblock = 1 332 continue 333 334 # Handle endif 335 pmatch = endifrex.match(line) 336 if pmatch: 337 if ifblock == -1: 338 print("Error: #endif outside of #if block at line " + str(lineno) + " (ignored)") 339 elif ifstack: 340 ifblock = ifstack.pop() 341 else: 342 ifblock = -1 343 continue 344 345 # Check for 'if' or 'else' that were not properly formed 346 pmatch = badifrex.match(line) 347 if pmatch: 348 print("Error: Badly formed #if statement at line " + str(lineno) + " (ignored)") 349 if ifblock != -1: 350 ifstack.append(ifblock) 351 352 if ifblock == 1 or ifblock == -1: 353 ifblock = 0 354 else: 355 ifblock = 2 356 continue 357 358 pmatch = badelserex.match(line) 359 if pmatch: 360 print("Error: Badly formed #else statement at line " + str(lineno) + " (ignored)") 361 ifblock = 2 362 continue 363 364 # Ignore all lines that are not satisfied by a conditional 365 if ifblock == 0 or ifblock == 2: 366 continue 367 368 # Now do definition replacement on what's left (if anything) 369 for keyword in keys: 370 line = keyrex[keyword].sub(defines[keyword], line) 371 372 # Output the line 373 print(line, file=ofile, end='') 374 375 if ifblock != -1 or ifstack != []: 376 print("Error: input file ended with an unterminated #if block.") 377 378 if ifile != sys.stdin: 379 ifile.close() 380 return 381 382def printusage(progname): 383 print('Usage: ' + progname + ' input_file [output_file] [-options]') 384 print(' Options are:') 385 print(' -help Print this help text.') 386 print(' -ccomm Remove C comments in /* ... */ delimiters.') 387 print(' -D<def> Define word <def> and set its value to 1.') 388 print(' -D<def>=<val> Define word <def> and set its value to <val>.') 389 print(' -I<dir> Add <dir> to search path for input files.') 390 return 391 392if __name__ == '__main__': 393 394 # Parse command line for options and arguments 395 options = [] 396 arguments = [] 397 for item in sys.argv[1:]: 398 if item.find('-', 0) == 0: 399 options.append(item) 400 else: 401 arguments.append(item) 402 403 if len(arguments) > 0: 404 inputfile = arguments[0] 405 if len(arguments) > 1: 406 outputfile = arguments[1] 407 else: 408 outputfile = [] 409 else: 410 printusage(sys.argv[0]) 411 sys.exit(0) 412 413 defines = {} 414 keyrex = {} 415 keys = [] 416 incdirs = [] 417 ccomm = False 418 for item in options: 419 result = item.split('=') 420 if result[0] == '-help': 421 printusage(sys.argv[0]) 422 sys.exit(0) 423 elif result[0] == '-ccomm': 424 ccomm = True 425 elif result[0][0:2] == '-I': 426 incdirs.append(result[0][2:]) 427 elif result[0][0:2] == '-D': 428 keyword = result[0][2:] 429 try: 430 value = result[1] 431 except: 432 value = '1' 433 defines[keyword] = value 434 keyrex[keyword] = re.compile(keyword) 435 keys.append(keyword) 436 else: 437 print('Bad option ' + item + ', options are -help, -ccomm, -D<def> -I<dir>\n') 438 sys.exit(1) 439 440 if outputfile: 441 ofile = open(outputfile, 'w') 442 else: 443 ofile = sys.stdout 444 445 if not ofile: 446 print("Error: Cannot open file " + output_file + " for writing.") 447 sys.exit(1) 448 449 runpp(keys, keyrex, defines, ccomm, incdirs, inputfile, ofile) 450 if ofile != sys.stdout: 451 ofile.close() 452 sys.exit(0) 453