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