1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4""" 5This is a very primitive line based preprocessor, for times when using 6a C preprocessor isn't an option. 7 8It currently supports the following grammar for expressions, whitespace is 9ignored: 10 11expression : 12 and_cond ( '||' expression ) ? ; 13and_cond: 14 test ( '&&' and_cond ) ? ; 15test: 16 unary ( ( '==' | '!=' ) unary ) ? ; 17unary : 18 '!'? value ; 19value : 20 [0-9]+ # integer 21 | 'defined(' \w+ ')' 22 | \w+ # string identifier or value; 23""" 24 25from __future__ import absolute_import, print_function, unicode_literals 26 27import errno 28import io 29from optparse import OptionParser 30import os 31import re 32import six 33import sys 34 35from mozbuild.makeutil import Makefile 36from mozpack.path import normsep 37 38# hack around win32 mangling our line endings 39# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65443 40if sys.platform == "win32": 41 import msvcrt 42 43 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) 44 os.linesep = "\n" 45 46 47__all__ = ["Context", "Expression", "Preprocessor", "preprocess"] 48 49 50def _to_text(a): 51 # We end up converting a lot of different types (text_type, binary_type, 52 # int, etc.) to Unicode in this script. This function handles all of those 53 # possibilities. 54 if isinstance(a, (six.text_type, six.binary_type)): 55 return six.ensure_text(a) 56 return six.text_type(a) 57 58 59def path_starts_with(path, prefix): 60 if os.altsep: 61 prefix = prefix.replace(os.altsep, os.sep) 62 path = path.replace(os.altsep, os.sep) 63 prefix = [os.path.normcase(p) for p in prefix.split(os.sep)] 64 path = [os.path.normcase(p) for p in path.split(os.sep)] 65 return path[: len(prefix)] == prefix 66 67 68class Expression: 69 def __init__(self, expression_string): 70 """ 71 Create a new expression with this string. 72 The expression will already be parsed into an Abstract Syntax Tree. 73 """ 74 self.content = expression_string 75 self.offset = 0 76 self.__ignore_whitespace() 77 self.e = self.__get_logical_or() 78 if self.content: 79 raise Expression.ParseError(self) 80 81 def __get_logical_or(self): 82 """ 83 Production: and_cond ( '||' expression ) ? 84 """ 85 if not len(self.content): 86 return None 87 rv = Expression.__AST("logical_op") 88 # test 89 rv.append(self.__get_logical_and()) 90 self.__ignore_whitespace() 91 if self.content[:2] != "||": 92 # no logical op needed, short cut to our prime element 93 return rv[0] 94 # append operator 95 rv.append(Expression.__ASTLeaf("op", self.content[:2])) 96 self.__strip(2) 97 self.__ignore_whitespace() 98 rv.append(self.__get_logical_or()) 99 self.__ignore_whitespace() 100 return rv 101 102 def __get_logical_and(self): 103 """ 104 Production: test ( '&&' and_cond ) ? 105 """ 106 if not len(self.content): 107 return None 108 rv = Expression.__AST("logical_op") 109 # test 110 rv.append(self.__get_equality()) 111 self.__ignore_whitespace() 112 if self.content[:2] != "&&": 113 # no logical op needed, short cut to our prime element 114 return rv[0] 115 # append operator 116 rv.append(Expression.__ASTLeaf("op", self.content[:2])) 117 self.__strip(2) 118 self.__ignore_whitespace() 119 rv.append(self.__get_logical_and()) 120 self.__ignore_whitespace() 121 return rv 122 123 def __get_equality(self): 124 """ 125 Production: unary ( ( '==' | '!=' ) unary ) ? 126 """ 127 if not len(self.content): 128 return None 129 rv = Expression.__AST("equality") 130 # unary 131 rv.append(self.__get_unary()) 132 self.__ignore_whitespace() 133 if not re.match("[=!]=", self.content): 134 # no equality needed, short cut to our prime unary 135 return rv[0] 136 # append operator 137 rv.append(Expression.__ASTLeaf("op", self.content[:2])) 138 self.__strip(2) 139 self.__ignore_whitespace() 140 rv.append(self.__get_unary()) 141 self.__ignore_whitespace() 142 return rv 143 144 def __get_unary(self): 145 """ 146 Production: '!'? value 147 """ 148 # eat whitespace right away, too 149 not_ws = re.match("!\s*", self.content) 150 if not not_ws: 151 return self.__get_value() 152 rv = Expression.__AST("not") 153 self.__strip(not_ws.end()) 154 rv.append(self.__get_value()) 155 self.__ignore_whitespace() 156 return rv 157 158 def __get_value(self): 159 """ 160 Production: ( [0-9]+ | 'defined(' \w+ ')' | \w+ ) 161 Note that the order is important, and the expression is kind-of 162 ambiguous as \w includes 0-9. One could make it unambiguous by 163 removing 0-9 from the first char of a string literal. 164 """ 165 rv = None 166 m = re.match("defined\s*\(\s*(\w+)\s*\)", self.content) 167 if m: 168 word_len = m.end() 169 rv = Expression.__ASTLeaf("defined", m.group(1)) 170 else: 171 word_len = re.match("[0-9]*", self.content).end() 172 if word_len: 173 value = int(self.content[:word_len]) 174 rv = Expression.__ASTLeaf("int", value) 175 else: 176 word_len = re.match("\w*", self.content).end() 177 if word_len: 178 rv = Expression.__ASTLeaf("string", self.content[:word_len]) 179 else: 180 raise Expression.ParseError(self) 181 self.__strip(word_len) 182 self.__ignore_whitespace() 183 return rv 184 185 def __ignore_whitespace(self): 186 ws_len = re.match("\s*", self.content).end() 187 self.__strip(ws_len) 188 return 189 190 def __strip(self, length): 191 """ 192 Remove a given amount of chars from the input and update 193 the offset. 194 """ 195 self.content = self.content[length:] 196 self.offset += length 197 198 def evaluate(self, context): 199 """ 200 Evaluate the expression with the given context 201 """ 202 203 # Helper function to evaluate __get_equality results 204 def eval_equality(tok): 205 left = opmap[tok[0].type](tok[0]) 206 right = opmap[tok[2].type](tok[2]) 207 rv = left == right 208 if tok[1].value == "!=": 209 rv = not rv 210 return rv 211 212 # Helper function to evaluate __get_logical_and and __get_logical_or results 213 def eval_logical_op(tok): 214 left = opmap[tok[0].type](tok[0]) 215 right = opmap[tok[2].type](tok[2]) 216 if tok[1].value == "&&": 217 return left and right 218 elif tok[1].value == "||": 219 return left or right 220 raise Expression.ParseError(self) 221 222 # Mapping from token types to evaluator functions 223 # Apart from (non-)equality, all these can be simple lambda forms. 224 opmap = { 225 "logical_op": eval_logical_op, 226 "equality": eval_equality, 227 "not": lambda tok: not opmap[tok[0].type](tok[0]), 228 "string": lambda tok: context[tok.value], 229 "defined": lambda tok: tok.value in context, 230 "int": lambda tok: tok.value, 231 } 232 233 return opmap[self.e.type](self.e) 234 235 class __AST(list): 236 """ 237 Internal class implementing Abstract Syntax Tree nodes 238 """ 239 240 def __init__(self, type): 241 self.type = type 242 super(self.__class__, self).__init__(self) 243 244 class __ASTLeaf: 245 """ 246 Internal class implementing Abstract Syntax Tree leafs 247 """ 248 249 def __init__(self, type, value): 250 self.value = value 251 self.type = type 252 253 def __str__(self): 254 return self.value.__str__() 255 256 def __repr__(self): 257 return self.value.__repr__() 258 259 class ParseError(Exception): 260 """ 261 Error raised when parsing fails. 262 It has two members, offset and content, which give the offset of the 263 error and the offending content. 264 """ 265 266 def __init__(self, expression): 267 self.offset = expression.offset 268 self.content = expression.content[:3] 269 270 def __str__(self): 271 return 'Unexpected content at offset {0}, "{1}"'.format( 272 self.offset, self.content 273 ) 274 275 276class Context(dict): 277 """ 278 This class holds variable values by subclassing dict, and while it 279 truthfully reports True and False on 280 281 name in context 282 283 it returns the variable name itself on 284 285 context["name"] 286 287 to reflect the ambiguity between string literals and preprocessor 288 variables. 289 """ 290 291 def __getitem__(self, key): 292 if key in self: 293 return super(self.__class__, self).__getitem__(key) 294 return key 295 296 297class Preprocessor: 298 """ 299 Class for preprocessing text files. 300 """ 301 302 class Error(RuntimeError): 303 def __init__(self, cpp, MSG, context): 304 self.file = cpp.context["FILE"] 305 self.line = cpp.context["LINE"] 306 self.key = MSG 307 RuntimeError.__init__(self, (self.file, self.line, self.key, context)) 308 309 def __init__(self, defines=None, marker="#"): 310 self.context = Context() 311 self.context.update({"FILE": "", "LINE": 0, "DIRECTORY": os.path.abspath(".")}) 312 try: 313 # Can import globally because of bootstrapping issues. 314 from buildconfig import topsrcdir, topobjdir 315 except ImportError: 316 # Allow this script to still work independently of a configured objdir. 317 topsrcdir = topobjdir = None 318 self.topsrcdir = topsrcdir 319 self.topobjdir = topobjdir 320 self.curdir = "." 321 self.actionLevel = 0 322 self.disableLevel = 0 323 # ifStates can be 324 # 0: hadTrue 325 # 1: wantsTrue 326 # 2: #else found 327 self.ifStates = [] 328 self.checkLineNumbers = False 329 330 # A list of (filter_name, filter_function) pairs. 331 self.filters = [] 332 333 self.cmds = {} 334 for cmd, level in ( 335 ("define", 0), 336 ("undef", 0), 337 ("if", sys.maxsize), 338 ("ifdef", sys.maxsize), 339 ("ifndef", sys.maxsize), 340 ("else", 1), 341 ("elif", 1), 342 ("elifdef", 1), 343 ("elifndef", 1), 344 ("endif", sys.maxsize), 345 ("expand", 0), 346 ("literal", 0), 347 ("filter", 0), 348 ("unfilter", 0), 349 ("include", 0), 350 ("includesubst", 0), 351 ("error", 0), 352 ): 353 self.cmds[cmd] = (level, getattr(self, "do_" + cmd)) 354 self.out = sys.stdout 355 self.setMarker(marker) 356 self.varsubst = re.compile("@(?P<VAR>\w+)@", re.U) 357 self.includes = set() 358 self.silenceMissingDirectiveWarnings = False 359 if defines: 360 self.context.update(defines) 361 362 def failUnused(self, file): 363 msg = None 364 if self.actionLevel == 0 and not self.silenceMissingDirectiveWarnings: 365 msg = "no preprocessor directives found" 366 elif self.actionLevel == 1: 367 msg = "no useful preprocessor directives found" 368 if msg: 369 370 class Fake(object): 371 pass 372 373 fake = Fake() 374 fake.context = { 375 "FILE": file, 376 "LINE": None, 377 } 378 raise Preprocessor.Error(fake, msg, None) 379 380 def setMarker(self, aMarker): 381 """ 382 Set the marker to be used for processing directives. 383 Used for handling CSS files, with pp.setMarker('%'), for example. 384 The given marker may be None, in which case no markers are processed. 385 """ 386 self.marker = aMarker 387 if aMarker: 388 self.instruction = re.compile( 389 "\s*{0}(?P<cmd>[a-z]+)(?:\s+(?P<args>.*?))?\s*$".format(aMarker) 390 ) 391 self.comment = re.compile(aMarker, re.U) 392 else: 393 394 class NoMatch(object): 395 def match(self, *args): 396 return False 397 398 self.instruction = self.comment = NoMatch() 399 400 def setSilenceDirectiveWarnings(self, value): 401 """ 402 Sets whether missing directive warnings are silenced, according to 403 ``value``. The default behavior of the preprocessor is to emit 404 such warnings. 405 """ 406 self.silenceMissingDirectiveWarnings = value 407 408 def addDefines(self, defines): 409 """ 410 Adds the specified defines to the preprocessor. 411 ``defines`` may be a dictionary object or an iterable of key/value pairs 412 (as tuples or other iterables of length two) 413 """ 414 self.context.update(defines) 415 416 def clone(self): 417 """ 418 Create a clone of the current processor, including line ending 419 settings, marker, variable definitions, output stream. 420 """ 421 rv = Preprocessor() 422 rv.context.update(self.context) 423 rv.setMarker(self.marker) 424 rv.out = self.out 425 return rv 426 427 def processFile(self, input, output, depfile=None): 428 """ 429 Preprocesses the contents of the ``input`` stream and writes the result 430 to the ``output`` stream. If ``depfile`` is set, the dependencies of 431 ``output`` file are written to ``depfile`` in Makefile format. 432 """ 433 self.out = output 434 435 self.do_include(input, False) 436 self.failUnused(input.name) 437 438 if depfile: 439 mk = Makefile() 440 mk.create_rule([output.name]).add_dependencies(self.includes) 441 mk.dump(depfile) 442 443 def computeDependencies(self, input): 444 """ 445 Reads the ``input`` stream, and computes the dependencies for that input. 446 """ 447 try: 448 old_out = self.out 449 self.out = None 450 self.do_include(input, False) 451 452 return self.includes 453 finally: 454 self.out = old_out 455 456 def applyFilters(self, aLine): 457 for f in self.filters: 458 aLine = f[1](aLine) 459 return aLine 460 461 def noteLineInfo(self): 462 # Record the current line and file. Called once before transitioning 463 # into or out of an included file and after writing each line. 464 self.line_info = self.context["FILE"], self.context["LINE"] 465 466 def write(self, aLine): 467 """ 468 Internal method for handling output. 469 """ 470 if not self.out: 471 return 472 473 next_line, next_file = self.context["LINE"], self.context["FILE"] 474 if self.checkLineNumbers: 475 expected_file, expected_line = self.line_info 476 expected_line += 1 477 if ( 478 expected_line != next_line 479 or expected_file 480 and expected_file != next_file 481 ): 482 self.out.write( 483 '//@line {line} "{file}"\n'.format(line=next_line, file=next_file) 484 ) 485 self.noteLineInfo() 486 487 filteredLine = self.applyFilters(aLine) 488 if filteredLine != aLine: 489 self.actionLevel = 2 490 self.out.write(filteredLine) 491 492 def handleCommandLine(self, args, defaultToStdin=False): 493 """ 494 Parse a commandline into this parser. 495 Uses OptionParser internally, no args mean sys.argv[1:]. 496 """ 497 498 def get_output_file(path, encoding=None): 499 if encoding is None: 500 encoding = "utf-8" 501 dir = os.path.dirname(path) 502 if dir: 503 try: 504 os.makedirs(dir) 505 except OSError as error: 506 if error.errno != errno.EEXIST: 507 raise 508 return io.open(path, "w", encoding=encoding, newline="\n") 509 510 p = self.getCommandLineParser() 511 options, args = p.parse_args(args=args) 512 out = self.out 513 depfile = None 514 515 if options.output: 516 out = get_output_file(options.output, options.output_encoding) 517 elif options.output_encoding: 518 raise Preprocessor.Error( 519 self, "--output-encoding doesn't work without --output", None 520 ) 521 if defaultToStdin and len(args) == 0: 522 args = [sys.stdin] 523 if options.depend: 524 raise Preprocessor.Error(self, "--depend doesn't work with stdin", None) 525 if options.depend: 526 if not options.output: 527 raise Preprocessor.Error( 528 self, "--depend doesn't work with stdout", None 529 ) 530 depfile = get_output_file(options.depend) 531 532 if args: 533 for f in args: 534 with io.open(f, "rU", encoding="utf-8") as input: 535 self.processFile(input=input, output=out) 536 if depfile: 537 mk = Makefile() 538 mk.create_rule([six.ensure_text(options.output)]).add_dependencies( 539 self.includes 540 ) 541 mk.dump(depfile) 542 depfile.close() 543 544 if options.output: 545 out.close() 546 547 def getCommandLineParser(self, unescapeDefines=False): 548 escapedValue = re.compile('".*"$') 549 numberValue = re.compile("\d+$") 550 551 def handleD(option, opt, value, parser): 552 vals = value.split("=", 1) 553 if len(vals) == 1: 554 vals.append(1) 555 elif unescapeDefines and escapedValue.match(vals[1]): 556 # strip escaped string values 557 vals[1] = vals[1][1:-1] 558 elif numberValue.match(vals[1]): 559 vals[1] = int(vals[1]) 560 self.context[vals[0]] = vals[1] 561 562 def handleU(option, opt, value, parser): 563 del self.context[value] 564 565 def handleF(option, opt, value, parser): 566 self.do_filter(value) 567 568 def handleMarker(option, opt, value, parser): 569 self.setMarker(value) 570 571 def handleSilenceDirectiveWarnings(option, opt, value, parse): 572 self.setSilenceDirectiveWarnings(True) 573 574 p = OptionParser() 575 p.add_option( 576 "-D", 577 action="callback", 578 callback=handleD, 579 type="string", 580 metavar="VAR[=VAL]", 581 help="Define a variable", 582 ) 583 p.add_option( 584 "-U", 585 action="callback", 586 callback=handleU, 587 type="string", 588 metavar="VAR", 589 help="Undefine a variable", 590 ) 591 p.add_option( 592 "-F", 593 action="callback", 594 callback=handleF, 595 type="string", 596 metavar="FILTER", 597 help="Enable the specified filter", 598 ) 599 p.add_option( 600 "-o", 601 "--output", 602 type="string", 603 default=None, 604 metavar="FILENAME", 605 help="Output to the specified file instead of stdout", 606 ) 607 p.add_option( 608 "--depend", 609 type="string", 610 default=None, 611 metavar="FILENAME", 612 help="Generate dependencies in the given file", 613 ) 614 p.add_option( 615 "--marker", 616 action="callback", 617 callback=handleMarker, 618 type="string", 619 help="Use the specified marker instead of #", 620 ) 621 p.add_option( 622 "--silence-missing-directive-warnings", 623 action="callback", 624 callback=handleSilenceDirectiveWarnings, 625 help="Don't emit warnings about missing directives", 626 ) 627 p.add_option( 628 "--output-encoding", 629 type="string", 630 default=None, 631 metavar="ENCODING", 632 help="Encoding to use for the output", 633 ) 634 return p 635 636 def handleLine(self, aLine): 637 """ 638 Handle a single line of input (internal). 639 """ 640 if self.actionLevel == 0 and self.comment.match(aLine): 641 self.actionLevel = 1 642 m = self.instruction.match(aLine) 643 if m: 644 args = None 645 cmd = m.group("cmd") 646 try: 647 args = m.group("args") 648 except IndexError: 649 pass 650 if cmd not in self.cmds: 651 raise Preprocessor.Error(self, "INVALID_CMD", aLine) 652 level, cmd = self.cmds[cmd] 653 if level >= self.disableLevel: 654 cmd(args) 655 if cmd != "literal": 656 self.actionLevel = 2 657 elif self.disableLevel == 0 and not self.comment.match(aLine): 658 self.write(aLine) 659 660 # Instruction handlers 661 # These are named do_'instruction name' and take one argument 662 663 # Variables 664 def do_define(self, args): 665 m = re.match("(?P<name>\w+)(?:\s(?P<value>.*))?", args, re.U) 666 if not m: 667 raise Preprocessor.Error(self, "SYNTAX_DEF", args) 668 val = "" 669 if m.group("value"): 670 val = self.applyFilters(m.group("value")) 671 try: 672 val = int(val) 673 except Exception: 674 pass 675 self.context[m.group("name")] = val 676 677 def do_undef(self, args): 678 m = re.match("(?P<name>\w+)$", args, re.U) 679 if not m: 680 raise Preprocessor.Error(self, "SYNTAX_DEF", args) 681 if args in self.context: 682 del self.context[args] 683 684 # Logic 685 def ensure_not_else(self): 686 if len(self.ifStates) == 0 or self.ifStates[-1] == 2: 687 sys.stderr.write( 688 "WARNING: bad nesting of #else in %s\n" % self.context["FILE"] 689 ) 690 691 def do_if(self, args, replace=False): 692 if self.disableLevel and not replace: 693 self.disableLevel += 1 694 return 695 val = None 696 try: 697 e = Expression(args) 698 val = e.evaluate(self.context) 699 except Exception: 700 # XXX do real error reporting 701 raise Preprocessor.Error(self, "SYNTAX_ERR", args) 702 if isinstance(val, six.text_type) or isinstance(val, six.binary_type): 703 # we're looking for a number value, strings are false 704 val = False 705 if not val: 706 self.disableLevel = 1 707 if replace: 708 if val: 709 self.disableLevel = 0 710 self.ifStates[-1] = self.disableLevel 711 else: 712 self.ifStates.append(self.disableLevel) 713 714 def do_ifdef(self, args, replace=False): 715 if self.disableLevel and not replace: 716 self.disableLevel += 1 717 return 718 if re.search("\W", args, re.U): 719 raise Preprocessor.Error(self, "INVALID_VAR", args) 720 if args not in self.context: 721 self.disableLevel = 1 722 if replace: 723 if args in self.context: 724 self.disableLevel = 0 725 self.ifStates[-1] = self.disableLevel 726 else: 727 self.ifStates.append(self.disableLevel) 728 729 def do_ifndef(self, args, replace=False): 730 if self.disableLevel and not replace: 731 self.disableLevel += 1 732 return 733 if re.search("\W", args, re.U): 734 raise Preprocessor.Error(self, "INVALID_VAR", args) 735 if args in self.context: 736 self.disableLevel = 1 737 if replace: 738 if args not in self.context: 739 self.disableLevel = 0 740 self.ifStates[-1] = self.disableLevel 741 else: 742 self.ifStates.append(self.disableLevel) 743 744 def do_else(self, args, ifState=2): 745 self.ensure_not_else() 746 hadTrue = self.ifStates[-1] == 0 747 self.ifStates[-1] = ifState # in-else 748 if hadTrue: 749 self.disableLevel = 1 750 return 751 self.disableLevel = 0 752 753 def do_elif(self, args): 754 if self.disableLevel == 1: 755 if self.ifStates[-1] == 1: 756 self.do_if(args, replace=True) 757 else: 758 self.do_else(None, self.ifStates[-1]) 759 760 def do_elifdef(self, args): 761 if self.disableLevel == 1: 762 if self.ifStates[-1] == 1: 763 self.do_ifdef(args, replace=True) 764 else: 765 self.do_else(None, self.ifStates[-1]) 766 767 def do_elifndef(self, args): 768 if self.disableLevel == 1: 769 if self.ifStates[-1] == 1: 770 self.do_ifndef(args, replace=True) 771 else: 772 self.do_else(None, self.ifStates[-1]) 773 774 def do_endif(self, args): 775 if self.disableLevel > 0: 776 self.disableLevel -= 1 777 if self.disableLevel == 0: 778 self.ifStates.pop() 779 780 # output processing 781 def do_expand(self, args): 782 lst = re.split("__(\w+)__", args, re.U) 783 784 def vsubst(v): 785 if v in self.context: 786 return _to_text(self.context[v]) 787 return "" 788 789 for i in range(1, len(lst), 2): 790 lst[i] = vsubst(lst[i]) 791 lst.append("\n") # add back the newline 792 self.write(six.moves.reduce(lambda x, y: x + y, lst, "")) 793 794 def do_literal(self, args): 795 self.write(args + "\n") 796 797 def do_filter(self, args): 798 filters = [f for f in args.split(" ") if hasattr(self, "filter_" + f)] 799 if len(filters) == 0: 800 return 801 current = dict(self.filters) 802 for f in filters: 803 current[f] = getattr(self, "filter_" + f) 804 self.filters = [(fn, current[fn]) for fn in sorted(current.keys())] 805 return 806 807 def do_unfilter(self, args): 808 filters = args.split(" ") 809 current = dict(self.filters) 810 for f in filters: 811 if f in current: 812 del current[f] 813 self.filters = [(fn, current[fn]) for fn in sorted(current.keys())] 814 return 815 816 # Filters 817 # 818 # emptyLines: Strips blank lines from the output. 819 def filter_emptyLines(self, aLine): 820 if aLine == "\n": 821 return "" 822 return aLine 823 824 # dumbComments: Empties out lines that consists of optional whitespace 825 # followed by a `//`. 826 def filter_dumbComments(self, aLine): 827 return re.sub("^\s*//.*", "", aLine) 828 829 # substitution: variables wrapped in @ are replaced with their value. 830 def filter_substitution(self, aLine, fatal=True): 831 def repl(matchobj): 832 varname = matchobj.group("VAR") 833 if varname in self.context: 834 return _to_text(self.context[varname]) 835 if fatal: 836 raise Preprocessor.Error(self, "UNDEFINED_VAR", varname) 837 return matchobj.group(0) 838 839 return self.varsubst.sub(repl, aLine) 840 841 # attemptSubstitution: variables wrapped in @ are replaced with their 842 # value, or an empty string if the variable is not defined. 843 def filter_attemptSubstitution(self, aLine): 844 return self.filter_substitution(aLine, fatal=False) 845 846 # File ops 847 def do_include(self, args, filters=True): 848 """ 849 Preprocess a given file. 850 args can either be a file name, or a file-like object. 851 Files should be opened, and will be closed after processing. 852 """ 853 isName = isinstance(args, six.string_types) 854 oldCheckLineNumbers = self.checkLineNumbers 855 self.checkLineNumbers = False 856 if isName: 857 try: 858 args = _to_text(args) 859 if filters: 860 args = self.applyFilters(args) 861 if not os.path.isabs(args): 862 args = os.path.join(self.curdir, args) 863 args = io.open(args, "rU", encoding="utf-8") 864 except Preprocessor.Error: 865 raise 866 except Exception: 867 raise Preprocessor.Error(self, "FILE_NOT_FOUND", _to_text(args)) 868 self.checkLineNumbers = bool( 869 re.search("\.(js|jsm|java|webidl)(?:\.in)?$", args.name) 870 ) 871 oldFile = self.context["FILE"] 872 oldLine = self.context["LINE"] 873 oldDir = self.context["DIRECTORY"] 874 oldCurdir = self.curdir 875 self.noteLineInfo() 876 877 if args.isatty(): 878 # we're stdin, use '-' and '' for file and dir 879 self.context["FILE"] = "-" 880 self.context["DIRECTORY"] = "" 881 self.curdir = "." 882 else: 883 abspath = os.path.abspath(args.name) 884 self.curdir = os.path.dirname(abspath) 885 self.includes.add(six.ensure_text(abspath)) 886 if self.topobjdir and path_starts_with(abspath, self.topobjdir): 887 abspath = "$OBJDIR" + normsep(abspath[len(self.topobjdir) :]) 888 elif self.topsrcdir and path_starts_with(abspath, self.topsrcdir): 889 abspath = "$SRCDIR" + normsep(abspath[len(self.topsrcdir) :]) 890 self.context["FILE"] = abspath 891 self.context["DIRECTORY"] = os.path.dirname(abspath) 892 self.context["LINE"] = 0 893 894 for l in args: 895 self.context["LINE"] += 1 896 self.handleLine(l) 897 if isName: 898 args.close() 899 900 self.context["FILE"] = oldFile 901 self.checkLineNumbers = oldCheckLineNumbers 902 self.context["LINE"] = oldLine 903 self.context["DIRECTORY"] = oldDir 904 self.curdir = oldCurdir 905 906 def do_includesubst(self, args): 907 args = self.filter_substitution(args) 908 self.do_include(args) 909 910 def do_error(self, args): 911 raise Preprocessor.Error(self, "Error: ", _to_text(args)) 912 913 914def preprocess(includes=[sys.stdin], defines={}, output=sys.stdout, marker="#"): 915 pp = Preprocessor(defines=defines, marker=marker) 916 for f in includes: 917 with io.open(f, "rU", encoding="utf-8") as input: 918 pp.processFile(input=input, output=output) 919 return pp.includes 920 921 922# Keep this module independently executable. 923if __name__ == "__main__": 924 pp = Preprocessor() 925 pp.handleCommandLine(None, True) 926