1# MIT License
2#
3# Copyright The SCons Foundation
4#
5# Permission is hereby granted, free of charge, to any person obtaining
6# a copy of this software and associated documentation files (the
7# "Software"), to deal in the Software without restriction, including
8# without limitation the rights to use, copy, modify, merge, publish,
9# distribute, sublicense, and/or sell copies of the Software, and to
10# permit persons to whom the Software is furnished to do so, subject to
11# the following conditions:
12#
13# The above copyright notice and this permission notice shall be included
14# in all copies or substantial portions of the Software.
15#
16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24"""SCons string substitution."""
25
26import collections
27import re
28from inspect import signature, Parameter
29
30import SCons.Errors
31from SCons.Util import is_String, is_Sequence
32
33# Indexed by the SUBST_* constants below.
34_strconv = [
35    SCons.Util.to_String_for_subst,
36    SCons.Util.to_String_for_subst,
37    SCons.Util.to_String_for_signature,
38]
39
40AllowableExceptions = (IndexError, NameError)
41
42
43def SetAllowableExceptions(*excepts):
44    global AllowableExceptions
45    AllowableExceptions = [_f for _f in excepts if _f]
46
47
48def raise_exception(exception, target, s):
49    name = exception.__class__.__name__
50    msg = "%s `%s' trying to evaluate `%s'" % (name, exception, s)
51    if target:
52        raise SCons.Errors.BuildError(target[0], msg)
53    else:
54        raise SCons.Errors.UserError(msg)
55
56
57class Literal:
58    """A wrapper for a string.  If you use this object wrapped
59    around a string, then it will be interpreted as literal.
60    When passed to the command interpreter, all special
61    characters will be escaped."""
62    def __init__(self, lstr):
63        self.lstr = lstr
64
65    def __str__(self):
66        return self.lstr
67
68    def escape(self, escape_func):
69        return escape_func(self.lstr)
70
71    def for_signature(self):
72        return self.lstr
73
74    def is_literal(self):
75        return 1
76
77    def __eq__(self, other):
78        if not isinstance(other, Literal):
79            return False
80        return self.lstr == other.lstr
81
82    def __neq__(self, other):
83        return not self.__eq__(other)
84
85    def __hash__(self):
86        return hash(self.lstr)
87
88class SpecialAttrWrapper:
89    """This is a wrapper for what we call a 'Node special attribute.'
90    This is any of the attributes of a Node that we can reference from
91    Environment variable substitution, such as $TARGET.abspath or
92    $SOURCES[1].filebase.  We implement the same methods as Literal
93    so we can handle special characters, plus a for_signature method,
94    such that we can return some canonical string during signature
95    calculation to avoid unnecessary rebuilds."""
96
97    def __init__(self, lstr, for_signature=None):
98        """The for_signature parameter, if supplied, will be the
99        canonical string we return from for_signature().  Else
100        we will simply return lstr."""
101        self.lstr = lstr
102        if for_signature:
103            self.forsig = for_signature
104        else:
105            self.forsig = lstr
106
107    def __str__(self):
108        return self.lstr
109
110    def escape(self, escape_func):
111        return escape_func(self.lstr)
112
113    def for_signature(self):
114        return self.forsig
115
116    def is_literal(self):
117        return 1
118
119def quote_spaces(arg):
120    """Generic function for putting double quotes around any string that
121    has white space in it."""
122    if ' ' in arg or '\t' in arg:
123        return '"%s"' % arg
124    else:
125        return str(arg)
126
127class CmdStringHolder(collections.UserString):
128    """This is a special class used to hold strings generated by
129    scons_subst() and scons_subst_list().  It defines a special method
130    escape().  When passed a function with an escape algorithm for a
131    particular platform, it will return the contained string with the
132    proper escape sequences inserted.
133    """
134    def __init__(self, cmd, literal=None):
135        collections.UserString.__init__(self, cmd)
136        self.literal = literal
137
138    def is_literal(self):
139        return self.literal
140
141    def escape(self, escape_func, quote_func=quote_spaces):
142        """Escape the string with the supplied function.  The
143        function is expected to take an arbitrary string, then
144        return it with all special characters escaped and ready
145        for passing to the command interpreter.
146
147        After calling this function, the next call to str() will
148        return the escaped string.
149        """
150
151        if self.is_literal():
152            return escape_func(self.data)
153        elif ' ' in self.data or '\t' in self.data:
154            return quote_func(self.data)
155        else:
156            return self.data
157
158def escape_list(mylist, escape_func):
159    """Escape a list of arguments by running the specified escape_func
160    on every object in the list that has an escape() method."""
161    def escape(obj, escape_func=escape_func):
162        try:
163            e = obj.escape
164        except AttributeError:
165            return obj
166        else:
167            return e(escape_func)
168    return list(map(escape, mylist))
169
170class NLWrapper:
171    """A wrapper class that delays turning a list of sources or targets
172    into a NodeList until it's needed.  The specified function supplied
173    when the object is initialized is responsible for turning raw nodes
174    into proxies that implement the special attributes like .abspath,
175    .source, etc.  This way, we avoid creating those proxies just
176    "in case" someone is going to use $TARGET or the like, and only
177    go through the trouble if we really have to.
178
179    In practice, this might be a wash performance-wise, but it's a little
180    cleaner conceptually...
181    """
182
183    def __init__(self, list, func):
184        self.list = list
185        self.func = func
186    def _return_nodelist(self):
187        return self.nodelist
188    def _gen_nodelist(self):
189        mylist = self.list
190        if mylist is None:
191            mylist = []
192        elif not is_Sequence(mylist):
193            mylist = [mylist]
194        # The map(self.func) call is what actually turns
195        # a list into appropriate proxies.
196        self.nodelist = SCons.Util.NodeList(list(map(self.func, mylist)))
197        self._create_nodelist = self._return_nodelist
198        return self.nodelist
199    _create_nodelist = _gen_nodelist
200
201
202class Targets_or_Sources(collections.UserList):
203    """A class that implements $TARGETS or $SOURCES expansions by in turn
204    wrapping a NLWrapper.  This class handles the different methods used
205    to access the list, calling the NLWrapper to create proxies on demand.
206
207    Note that we subclass collections.UserList purely so that the
208    is_Sequence() function will identify an object of this class as
209    a list during variable expansion.  We're not really using any
210    collections.UserList methods in practice.
211    """
212    def __init__(self, nl):
213        self.nl = nl
214    def __getattr__(self, attr):
215        nl = self.nl._create_nodelist()
216        return getattr(nl, attr)
217    def __getitem__(self, i):
218        nl = self.nl._create_nodelist()
219        return nl[i]
220    def __str__(self):
221        nl = self.nl._create_nodelist()
222        return str(nl)
223    def __repr__(self):
224        nl = self.nl._create_nodelist()
225        return repr(nl)
226
227class Target_or_Source:
228    """A class that implements $TARGET or $SOURCE expansions by in turn
229    wrapping a NLWrapper.  This class handles the different methods used
230    to access an individual proxy Node, calling the NLWrapper to create
231    a proxy on demand.
232    """
233    def __init__(self, nl):
234        self.nl = nl
235    def __getattr__(self, attr):
236        nl = self.nl._create_nodelist()
237        try:
238            nl0 = nl[0]
239        except IndexError:
240            # If there is nothing in the list, then we have no attributes to
241            # pass through, so raise AttributeError for everything.
242            raise AttributeError("NodeList has no attribute: %s" % attr)
243        return getattr(nl0, attr)
244    def __str__(self):
245        nl = self.nl._create_nodelist()
246        if nl:
247            return str(nl[0])
248        return ''
249    def __repr__(self):
250        nl = self.nl._create_nodelist()
251        if nl:
252            return repr(nl[0])
253        return ''
254
255class NullNodeList(SCons.Util.NullSeq):
256  def __call__(self, *args, **kwargs): return ''
257  def __str__(self): return ''
258
259NullNodesList = NullNodeList()
260
261def subst_dict(target, source):
262    """Create a dictionary for substitution of special
263    construction variables.
264
265    This translates the following special arguments:
266
267    target - the target (object or array of objects),
268             used to generate the TARGET and TARGETS
269             construction variables
270
271    source - the source (object or array of objects),
272             used to generate the SOURCES and SOURCE
273             construction variables
274    """
275    dict = {}
276
277    if target:
278        def get_tgt_subst_proxy(thing):
279            try:
280                subst_proxy = thing.get_subst_proxy()
281            except AttributeError:
282                subst_proxy = thing # probably a string, just return it
283            return subst_proxy
284        tnl = NLWrapper(target, get_tgt_subst_proxy)
285        dict['TARGETS'] = Targets_or_Sources(tnl)
286        dict['TARGET'] = Target_or_Source(tnl)
287
288        # This is a total cheat, but hopefully this dictionary goes
289        # away soon anyway.  We just let these expand to $TARGETS
290        # because that's "good enough" for the use of ToolSurrogates
291        # (see test/ToolSurrogate.py) to generate documentation.
292        dict['CHANGED_TARGETS'] = '$TARGETS'
293        dict['UNCHANGED_TARGETS'] = '$TARGETS'
294    else:
295        dict['TARGETS'] = NullNodesList
296        dict['TARGET'] = NullNodesList
297
298    if source:
299        def get_src_subst_proxy(node):
300            try:
301                rfile = node.rfile
302            except AttributeError:
303                pass
304            else:
305                node = rfile()
306            try:
307                return node.get_subst_proxy()
308            except AttributeError:
309                return node     # probably a String, just return it
310        snl = NLWrapper(source, get_src_subst_proxy)
311        dict['SOURCES'] = Targets_or_Sources(snl)
312        dict['SOURCE'] = Target_or_Source(snl)
313
314        # This is a total cheat, but hopefully this dictionary goes
315        # away soon anyway.  We just let these expand to $TARGETS
316        # because that's "good enough" for the use of ToolSurrogates
317        # (see test/ToolSurrogate.py) to generate documentation.
318        dict['CHANGED_SOURCES'] = '$SOURCES'
319        dict['UNCHANGED_SOURCES'] = '$SOURCES'
320    else:
321        dict['SOURCES'] = NullNodesList
322        dict['SOURCE'] = NullNodesList
323
324    return dict
325
326
327_callable_args_set = {'target', 'source', 'env', 'for_signature'}
328
329class StringSubber:
330    """A class to construct the results of a scons_subst() call.
331
332    This binds a specific construction environment, mode, target and
333    source with two methods (substitute() and expand()) that handle
334    the expansion.
335    """
336
337
338    def __init__(self, env, mode, conv, gvars):
339        self.env = env
340        self.mode = mode
341        self.conv = conv
342        self.gvars = gvars
343
344    def expand(self, s, lvars):
345        """Expand a single "token" as necessary, returning an
346        appropriate string containing the expansion.
347
348        This handles expanding different types of things (strings,
349        lists, callables) appropriately.  It calls the wrapper
350        substitute() method to re-expand things as necessary, so that
351        the results of expansions of side-by-side strings still get
352        re-evaluated separately, not smushed together.
353        """
354        if is_String(s):
355            try:
356                s0, s1 = s[:2]
357            except (IndexError, ValueError):
358                return s
359            if s0 != '$':
360                return s
361            if s1 == '$':
362                # In this case keep the double $'s which we'll later
363                # swap for a single dollar sign as we need to retain
364                # this information to properly avoid matching "$("" when
365                # the actual text was "$$(""  (or "$)"" when "$$)"" )
366                return '$$'
367            elif s1 in '()':
368                return s
369            else:
370                key = s[1:]
371                if key[0] == '{' or '.' in key:
372                    if key[0] == '{':
373                        key = key[1:-1]
374
375                # Store for error messages if we fail to expand the
376                # value
377                old_s = s
378                s = None
379                if key in lvars:
380                     s = lvars[key]
381                elif key in self.gvars:
382                     s = self.gvars[key]
383                else:
384                     try:
385                          s = eval(key, self.gvars, lvars)
386                     except KeyboardInterrupt:
387                          raise
388                     except Exception as e:
389                          if e.__class__ in AllowableExceptions:
390                               return ''
391                          raise_exception(e, lvars['TARGETS'], old_s)
392
393                if s is None and NameError not in AllowableExceptions:
394                     raise_exception(NameError(key), lvars['TARGETS'], old_s)
395                elif s is None:
396                     return ''
397
398                # Before re-expanding the result, handle
399                # recursive expansion by copying the local
400                # variable dictionary and overwriting a null
401                # string for the value of the variable name
402                # we just expanded.
403                #
404                # This could potentially be optimized by only
405                # copying lvars when s contains more expansions,
406                # but lvars is usually supposed to be pretty
407                # small, and deeply nested variable expansions
408                # are probably more the exception than the norm,
409                # so it should be tolerable for now.
410                lv = lvars.copy()
411                var = key.split('.')[0]
412                lv[var] = ''
413                return self.substitute(s, lv)
414        elif is_Sequence(s):
415            def func(l, conv=self.conv, substitute=self.substitute, lvars=lvars):
416                return conv(substitute(l, lvars))
417            return list(map(func, s))
418        elif callable(s):
419            # SCons has the unusual Null class where any __getattr__ call returns it's self,
420            # which does not work the signature module, and the Null class returns an empty
421            # string if called on, so we make an exception in this condition for Null class
422            # Also allow callables where the only non default valued args match the expected defaults
423            # this should also allow functools.partial's to work.
424            if isinstance(s, SCons.Util.Null) or {k for k, v in signature(s).parameters.items() if
425                                                  k in _callable_args_set or v.default == Parameter.empty} == _callable_args_set:
426
427                s = s(target=lvars['TARGETS'],
428                     source=lvars['SOURCES'],
429                     env=self.env,
430                     for_signature=(self.mode != SUBST_CMD))
431            else:
432                # This probably indicates that it's a callable
433                # object that doesn't match our calling arguments
434                # (like an Action).
435                if self.mode == SUBST_RAW:
436                    return s
437                s = self.conv(s)
438            return self.substitute(s, lvars)
439        elif s is None:
440            return ''
441        else:
442            return s
443
444    def substitute(self, args, lvars):
445        """Substitute expansions in an argument or list of arguments.
446
447        This serves as a wrapper for splitting up a string into
448        separate tokens.
449        """
450        if is_String(args) and not isinstance(args, CmdStringHolder):
451            args = str(args)        # In case it's a UserString.
452            try:
453                def sub_match(match):
454                    return self.conv(self.expand(match.group(1), lvars))
455                result = _dollar_exps.sub(sub_match, args)
456            except TypeError:
457                # If the internal conversion routine doesn't return
458                # strings (it could be overridden to return Nodes, for
459                # example), then the 1.5.2 re module will throw this
460                # exception.  Back off to a slower, general-purpose
461                # algorithm that works for all data types.
462                args = _separate_args.findall(args)
463                result = []
464                for a in args:
465                    result.append(self.conv(self.expand(a, lvars)))
466                if len(result) == 1:
467                    result = result[0]
468                else:
469                    result = ''.join(map(str, result))
470            return result
471        else:
472            return self.expand(args, lvars)
473
474
475class ListSubber(collections.UserList):
476    """A class to construct the results of a scons_subst_list() call.
477
478    Like StringSubber, this class binds a specific construction
479    environment, mode, target and source with two methods
480    (substitute() and expand()) that handle the expansion.
481
482    In addition, however, this class is used to track the state of
483    the result(s) we're gathering so we can do the appropriate thing
484    whenever we have to append another word to the result--start a new
485    line, start a new word, append to the current word, etc.  We do
486    this by setting the "append" attribute to the right method so
487    that our wrapper methods only need ever call ListSubber.append(),
488    and the rest of the object takes care of doing the right thing
489    internally.
490    """
491    def __init__(self, env, mode, conv, gvars):
492        collections.UserList.__init__(self, [])
493        self.env = env
494        self.mode = mode
495        self.conv = conv
496        self.gvars = gvars
497
498        if self.mode == SUBST_RAW:
499            self.add_strip = lambda x: self.append(x)
500        else:
501            self.add_strip = lambda x: None
502        self.in_strip = None
503        self.next_line()
504
505    def expanded(self, s):
506        """Determines if the string s requires further expansion.
507
508        Due to the implementation of ListSubber expand will call
509        itself 2 additional times for an already expanded string. This
510        method is used to determine if a string is already fully
511        expanded and if so exit the loop early to prevent these
512        recursive calls.
513        """
514        if not is_String(s) or isinstance(s, CmdStringHolder):
515            return False
516
517        s = str(s)  # in case it's a UserString
518        return _separate_args.findall(s) is None
519
520    def expand(self, s, lvars, within_list):
521        """Expand a single "token" as necessary, appending the
522        expansion to the current result.
523
524        This handles expanding different types of things (strings,
525        lists, callables) appropriately.  It calls the wrapper
526        substitute() method to re-expand things as necessary, so that
527        the results of expansions of side-by-side strings still get
528        re-evaluated separately, not smushed together.
529        """
530
531        if is_String(s):
532            try:
533                s0, s1 = s[:2]
534            except (IndexError, ValueError):
535                self.append(s)
536                return
537            if s0 != '$':
538                self.append(s)
539                return
540            if s1 == '$':
541                self.append('$')
542            elif s1 == '(':
543                self.open_strip('$(')
544            elif s1 == ')':
545                self.close_strip('$)')
546            else:
547                key = s[1:]
548                if key[0] == '{' or key.find('.') >= 0:
549                    if key[0] == '{':
550                        key = key[1:-1]
551
552                # Store for error messages if we fail to expand the
553                # value
554                old_s = s
555                s = None
556                if key in lvars:
557                     s = lvars[key]
558                elif key in self.gvars:
559                     s = self.gvars[key]
560                else:
561                     try:
562                         s = eval(key, self.gvars, lvars)
563                     except KeyboardInterrupt:
564                         raise
565                     except Exception as e:
566                         if e.__class__ in AllowableExceptions:
567                             return
568                         raise_exception(e, lvars['TARGETS'], old_s)
569
570                if s is None and NameError not in AllowableExceptions:
571                     raise_exception(NameError(), lvars['TARGETS'], old_s)
572                elif s is None:
573                     return
574
575                # If the string is already full expanded there's no
576                # need to continue recursion.
577                if self.expanded(s):
578                    self.append(s)
579                    return
580
581                # Before re-expanding the result, handle
582                # recursive expansion by copying the local
583                # variable dictionary and overwriting a null
584                # string for the value of the variable name
585                # we just expanded.
586                lv = lvars.copy()
587                var = key.split('.')[0]
588                lv[var] = ''
589                self.substitute(s, lv, 0)
590                self.this_word()
591        elif is_Sequence(s):
592            for a in s:
593                self.substitute(a, lvars, 1)
594                self.next_word()
595        elif callable(s):
596            # SCons has the unusual Null class where any __getattr__ call returns it's self,
597            # which does not work the signature module, and the Null class returns an empty
598            # string if called on, so we make an exception in this condition for Null class
599            # Also allow callables where the only non default valued args match the expected defaults
600            # this should also allow functools.partial's to work.
601            if isinstance(s, SCons.Util.Null) or {k for k, v in signature(s).parameters.items() if
602                                                  k in _callable_args_set or v.default == Parameter.empty} == _callable_args_set:
603
604                s = s(target=lvars['TARGETS'],
605                     source=lvars['SOURCES'],
606                     env=self.env,
607                     for_signature=(self.mode != SUBST_CMD))
608            else:
609                # This probably indicates that it's a callable
610                # object that doesn't match our calling arguments
611                # (like an Action).
612                if self.mode == SUBST_RAW:
613                    self.append(s)
614                    return
615                s = self.conv(s)
616            self.substitute(s, lvars, within_list)
617        elif s is None:
618            self.this_word()
619        else:
620            self.append(s)
621
622    def substitute(self, args, lvars, within_list):
623        """Substitute expansions in an argument or list of arguments.
624
625        This serves as a wrapper for splitting up a string into
626        separate tokens.
627        """
628
629        if is_String(args) and not isinstance(args, CmdStringHolder):
630            args = str(args)        # In case it's a UserString.
631            args = _separate_args.findall(args)
632            for a in args:
633                if a[0] in ' \t\n\r\f\v':
634                    if '\n' in a:
635                        self.next_line()
636                    elif within_list:
637                        self.append(a)
638                    else:
639                        self.next_word()
640                else:
641                    self.expand(a, lvars, within_list)
642        else:
643            self.expand(args, lvars, within_list)
644
645    def next_line(self):
646        """Arrange for the next word to start a new line.  This
647        is like starting a new word, except that we have to append
648        another line to the result."""
649        collections.UserList.append(self, [])
650        self.next_word()
651
652    def this_word(self):
653        """Arrange for the next word to append to the end of the
654        current last word in the result."""
655        self.append = self.add_to_current_word
656
657    def next_word(self):
658        """Arrange for the next word to start a new word."""
659        self.append = self.add_new_word
660
661    def add_to_current_word(self, x):
662        """Append the string x to the end of the current last word
663        in the result.  If that is not possible, then just add
664        it as a new word.  Make sure the entire concatenated string
665        inherits the object attributes of x (in particular, the
666        escape function) by wrapping it as CmdStringHolder."""
667
668        if not self.in_strip or self.mode != SUBST_SIG:
669            try:
670                current_word = self[-1][-1]
671            except IndexError:
672                self.add_new_word(x)
673            else:
674                # All right, this is a hack and it should probably
675                # be refactored out of existence in the future.
676                # The issue is that we want to smoosh words together
677                # and make one file name that gets escaped if
678                # we're expanding something like foo$EXTENSION,
679                # but we don't want to smoosh them together if
680                # it's something like >$TARGET, because then we'll
681                # treat the '>' like it's part of the file name.
682                # So for now, just hard-code looking for the special
683                # command-line redirection characters...
684                try:
685                    last_char = str(current_word)[-1]
686                except IndexError:
687                    last_char = '\0'
688                if last_char in '<>|':
689                    self.add_new_word(x)
690                else:
691                    y = current_word + x
692
693                    # We used to treat a word appended to a literal
694                    # as a literal itself, but this caused problems
695                    # with interpreting quotes around space-separated
696                    # targets on command lines.  Removing this makes
697                    # none of the "substantive" end-to-end tests fail,
698                    # so we'll take this out but leave it commented
699                    # for now in case there's a problem not covered
700                    # by the test cases and we need to resurrect this.
701                    #literal1 = self.literal(self[-1][-1])
702                    #literal2 = self.literal(x)
703                    y = self.conv(y)
704                    if is_String(y):
705                        #y = CmdStringHolder(y, literal1 or literal2)
706                        y = CmdStringHolder(y, None)
707                    self[-1][-1] = y
708
709    def add_new_word(self, x):
710        if not self.in_strip or self.mode != SUBST_SIG:
711            literal = self.literal(x)
712            x = self.conv(x)
713            if is_String(x):
714                x = CmdStringHolder(x, literal)
715            self[-1].append(x)
716        self.append = self.add_to_current_word
717
718    def literal(self, x):
719        try:
720            l = x.is_literal
721        except AttributeError:
722            return None
723        else:
724            return l()
725
726    def open_strip(self, x):
727        """Handle the "open strip" $( token."""
728        self.add_strip(x)
729        self.in_strip = 1
730
731    def close_strip(self, x):
732        """Handle the "close strip" $) token."""
733        self.add_strip(x)
734        self.in_strip = None
735
736
737# Constants for the "mode" parameter to scons_subst_list() and
738# scons_subst().  SUBST_RAW gives the raw command line.  SUBST_CMD
739# gives a command line suitable for passing to a shell.  SUBST_SIG
740# gives a command line appropriate for calculating the signature
741# of a command line...if this changes, we should rebuild.
742SUBST_CMD = 0
743SUBST_RAW = 1
744SUBST_SIG = 2
745
746_rm = re.compile(r'\$[()]')
747
748# Note the pattern below only matches $( or $) when there is no
749# preceeding $. (Thus the (?<!\$))
750_rm_split = re.compile(r'(?<!\$)(\$[()])')
751
752# Indexed by the SUBST_* constants above.
753_regex_remove = [ _rm, None, _rm_split ]
754
755def _rm_list(list):
756    return [l for l in list if l not in ('$(', '$)')]
757
758def _remove_list(list):
759    result = []
760    depth = 0
761    for l in list:
762        if l == '$(':
763            depth += 1
764        elif l == '$)':
765            depth -= 1
766            if depth < 0:
767                break
768        elif depth == 0:
769            result.append(l)
770    if depth != 0:
771        return None
772    return result
773
774# Indexed by the SUBST_* constants above.
775_list_remove = [ _rm_list, None, _remove_list ]
776
777# Regular expressions for splitting strings and handling substitutions,
778# for use by the scons_subst() and scons_subst_list() functions:
779#
780# The first expression compiled matches all of the $-introduced tokens
781# that we need to process in some way, and is used for substitutions.
782# The expressions it matches are:
783#
784#       "$$"
785#       "$("
786#       "$)"
787#       "$variable"             [must begin with alphabetic or underscore]
788#       "${any stuff}"
789#
790# The second expression compiled is used for splitting strings into tokens
791# to be processed, and it matches all of the tokens listed above, plus
792# the following that affect how arguments do or don't get joined together:
793#
794#       "   "                   [white space]
795#       "non-white-space"       [without any dollar signs]
796#       "$"                     [single dollar sign]
797#
798_dollar_exps_str = r'\$[\$\(\)]|\$[_a-zA-Z][\.\w]*|\${[^}]*}'
799_dollar_exps = re.compile(r'(%s)' % _dollar_exps_str)
800_separate_args = re.compile(r'(%s|\s+|[^\s$]+|\$)' % _dollar_exps_str)
801
802# This regular expression is used to replace strings of multiple white
803# space characters in the string result from the scons_subst() function.
804_space_sep = re.compile(r'[\t ]+(?![^{]*})')
805
806def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None):
807    """Expand a string or list containing construction variable
808    substitutions.
809
810    This is the work-horse function for substitutions in file names
811    and the like.  The companion scons_subst_list() function (below)
812    handles separating command lines into lists of arguments, so see
813    that function if that's what you're looking for.
814    """
815    if (isinstance(strSubst, str) and '$' not in strSubst) or isinstance(strSubst, CmdStringHolder):
816        return strSubst
817
818    if conv is None:
819        conv = _strconv[mode]
820
821    # Doing this every time is a bit of a waste, since the Executor
822    # has typically already populated the OverrideEnvironment with
823    # $TARGET/$SOURCE variables.  We're keeping this (for now), though,
824    # because it supports existing behavior that allows us to call
825    # an Action directly with an arbitrary target+source pair, which
826    # we use in Tool/tex.py to handle calling $BIBTEX when necessary.
827    # If we dropped that behavior (or found another way to cover it),
828    # we could get rid of this call completely and just rely on the
829    # Executor setting the variables.
830    if 'TARGET' not in lvars:
831        d = subst_dict(target, source)
832        if d:
833            lvars = lvars.copy()
834            lvars.update(d)
835
836    # We're (most likely) going to eval() things.  If Python doesn't
837    # find a __builtins__ value in the global dictionary used for eval(),
838    # it copies the current global values for you.  Avoid this by
839    # setting it explicitly and then deleting, so we don't pollute the
840    # construction environment Dictionary(ies) that are typically used
841    # for expansion.
842    gvars['__builtins__'] = __builtins__
843
844    ss = StringSubber(env, mode, conv, gvars)
845    result = ss.substitute(strSubst, lvars)
846
847    try:
848        del gvars['__builtins__']
849    except KeyError:
850        pass
851
852    res = result
853    if is_String(result):
854        # Remove $(-$) pairs and any stuff in between,
855        # if that's appropriate.
856        remove = _regex_remove[mode]
857        if remove:
858            if mode == SUBST_SIG:
859                result = _list_remove[mode](remove.split(result))
860                if result is None:
861                    raise SCons.Errors.UserError("Unbalanced $(/$) in: " + res)
862                result = ' '.join(result)
863            else:
864                result = remove.sub('', result)
865        if mode != SUBST_RAW:
866            # Compress strings of white space characters into
867            # a single space.
868            result = _space_sep.sub(' ', result).strip()
869
870        # Now replace escaped $'s currently "$$"
871        # This is needed because we now retain $$ instead of
872        # replacing them during substition to avoid
873        # improperly trying to escape "$$(" as being "$("
874        result = result.replace('$$','$')
875    elif is_Sequence(result):
876        remove = _list_remove[mode]
877        if remove:
878            result = remove(result)
879            if result is None:
880                raise SCons.Errors.UserError("Unbalanced $(/$) in: " + str(res))
881
882    return result
883
884def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None):
885    """Substitute construction variables in a string (or list or other
886    object) and separate the arguments into a command list.
887
888    The companion scons_subst() function (above) handles basic
889    substitutions within strings, so see that function instead
890    if that's what you're looking for.
891    """
892    if conv is None:
893        conv = _strconv[mode]
894
895    # Doing this every time is a bit of a waste, since the Executor
896    # has typically already populated the OverrideEnvironment with
897    # $TARGET/$SOURCE variables.  We're keeping this (for now), though,
898    # because it supports existing behavior that allows us to call
899    # an Action directly with an arbitrary target+source pair, which
900    # we use in Tool/tex.py to handle calling $BIBTEX when necessary.
901    # If we dropped that behavior (or found another way to cover it),
902    # we could get rid of this call completely and just rely on the
903    # Executor setting the variables.
904    if 'TARGET' not in lvars:
905        d = subst_dict(target, source)
906        if d:
907            lvars = lvars.copy()
908            lvars.update(d)
909
910    # We're (most likely) going to eval() things.  If Python doesn't
911    # find a __builtins__ value in the global dictionary used for eval(),
912    # it copies the current global values for you.  Avoid this by
913    # setting it explicitly and then deleting, so we don't pollute the
914    # construction environment Dictionary(ies) that are typically used
915    # for expansion.
916    gvars['__builtins__'] = __builtins__
917
918    ls = ListSubber(env, mode, conv, gvars)
919    ls.substitute(strSubst, lvars, 0)
920
921    try:
922        del gvars['__builtins__']
923    except KeyError:
924        pass
925
926    return ls.data
927
928def scons_subst_once(strSubst, env, key):
929    """Perform single (non-recursive) substitution of a single
930    construction variable keyword.
931
932    This is used when setting a variable when copying or overriding values
933    in an Environment.  We want to capture (expand) the old value before
934    we override it, so people can do things like:
935
936        env2 = env.Clone(CCFLAGS = '$CCFLAGS -g')
937
938    We do this with some straightforward, brute-force code here...
939    """
940    if isinstance(strSubst, str) and strSubst.find('$') < 0:
941        return strSubst
942
943    matchlist = ['$' + key, '${' + key + '}']
944    val = env.get(key, '')
945    def sub_match(match, val=val, matchlist=matchlist):
946        a = match.group(1)
947        if a in matchlist:
948            a = val
949        if is_Sequence(a):
950            return ' '.join(map(str, a))
951        else:
952            return str(a)
953
954    if is_Sequence(strSubst):
955        result = []
956        for arg in strSubst:
957            if is_String(arg):
958                if arg in matchlist:
959                    arg = val
960                    if is_Sequence(arg):
961                        result.extend(arg)
962                    else:
963                        result.append(arg)
964                else:
965                    result.append(_dollar_exps.sub(sub_match, arg))
966            else:
967                result.append(arg)
968        return result
969    elif is_String(strSubst):
970        return _dollar_exps.sub(sub_match, strSubst)
971    else:
972        return strSubst
973
974# Local Variables:
975# tab-width:4
976# indent-tabs-mode:nil
977# End:
978# vim: set expandtab tabstop=4 shiftwidth=4:
979