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