Fix commands with spaces in them (Bug: 589281 and 589285). (Anthony Roach)
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sun, 4 Aug 2002 23:55:21 +0000 (23:55 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sun, 4 Aug 2002 23:55:21 +0000 (23:55 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@425 fdb21ef1-2011-0410-befe-b5e4ea1792b1

doc/man/scons.1
src/engine/SCons/Action.py
src/engine/SCons/Util.py
src/engine/SCons/UtilTests.py
test/spaces.py [new file with mode: 0644]

index f574fb3cfab794586c5a176ae7dc87fc3f00c492..c0c995b1ede14971dfd0a77983f7f47c6022f497 100644 (file)
@@ -2603,6 +2603,27 @@ but the command signature added to any target files would be:
 echo Last build occurred  . > $TARGET
 .EE
 
+SCons uses the following rules when converting construction variables into
+command lines:
+
+.IP String
+When the value is a string it is interpreted as a space delimited list of
+command line arguments. 
+
+.IP List
+When the value is a list it is interpreted as a list of command line
+arguments. Each element of the list is converted to a string.
+
+.IP Other
+Anything that is not a list or string is converted to a string and
+interpreted as a single command line argument.
+
+.IP Newline
+Newline characters (\\n) delimit lines. The newline parsing is done after
+all other parsing, so it is not possible for arguments (e.g. file names) to
+contain embedded newline characters. This limitation will likely go away in
+a future version of SCons.
+
 .SS Scanner Objects
 
 You can use the
index 1ddecdfdcfdd83ef5dbf4812aa8e92ffe04e1e1e..2a07fceb8722cb8ae2166107715b85dbe0390f2d 100644 (file)
@@ -50,6 +50,12 @@ exitvalmap = {
 
 default_ENV = None
 
+def quote(x):
+    if ' ' in x or '\t' in x:
+        return '"'+x+'"'
+    else:
+        return x
+
 if os.name == 'posix':
 
     def defaultSpawn(cmd, args, env):
@@ -57,11 +63,7 @@ if os.name == 'posix':
         if not pid:
             # Child process.
             exitval = 127
-            args = [ 'sh', '-c' ] + \
-                   [ string.join(map(lambda x: string.replace(str(x),
-                                                              ' ',
-                                                              r'\ '),
-                                     args)) ]
+            args = ['sh', '-c', string.join(map(quote, args))]
             try:
                 os.execvpe('sh', args, env)
             except OSError, e:
@@ -145,13 +147,8 @@ elif os.name == 'nt':
             return 127
         else:
             try:
-
-                a = [ cmd_interp, '/C', args[0] ]
-                for arg in args[1:]:
-                    if ' ' in arg or '\t' in arg:
-                        arg = '"' + arg + '"'
-                    a.append(arg)
-                ret = os.spawnve(os.P_WAIT, cmd_interp, a, env)
+                args = [cmd_interp, '/C', quote(string.join(map(quote, args)))]
+                ret = os.spawnve(os.P_WAIT, cmd_interp, args, env)
             except OSError, e:
                 ret = exitvalmap[e[0]]
                 sys.stderr.write("scons: %s: %s\n" % (cmd, e[1]))
index 89871a3111a3ab5a9110cbfe90278142425e26ee..51789566e5db9d333ffc4ae6a75268aa1db87a1a 100644 (file)
@@ -194,64 +194,69 @@ _space_sep = re.compile(r'[\t ]+(?![^{]*})')
 
 def scons_subst_list(strSubst, globals, locals, remove=None):
     """
-    This function is similar to scons_subst(), but with
-    one important difference.  Instead of returning a single
-    string, this function returns a list of lists.
+    This function serves the same purpose as scons_subst(), except
+    this function returns the interpolated list as a list of lines, where
+    each line is a list of command line arguments. In other words:
     The first (outer) list is a list of lines, where the
     substituted stirng has been broken along newline characters.
     The inner lists are lists of command line arguments, i.e.,
     the argv array that should be passed to a spawn or exec
     function.
 
-    Also, this method can accept a list of strings as input
-    to strSubst, which explicitly denotes the command line
-    arguments.  This is useful if you want to pass in
-    command line arguments with spaces or newlines in them.
-    Otheriwise, if you just passed in a string, they would
-    get split along the spaces and newlines.
-    
-    One important thing this guy does is preserve environment
-    variables that are lists.  For instance, if you have
-    an environment variable that is a Python list (or UserList-
-    derived class) that contains path names with spaces in them,
-    then the entire path will be returned as a single argument.
-    This is the only way to know where the 'split' between arguments
-    is for executing a command line."""
+    There are a few simple rules this function follows in order to
+    determine how to parse strSubst and consruction variables into lines
+    and arguments:
+
+    1) A string is interpreted as a space delimited list of arguments.
+    2) A list is interpreted as a list of arguments. This allows arguments
+       with spaces in them to be expressed easily.
+    4) Anything that is not a list or string (e.g. a Node instance) is
+       interpreted as a single argument, and is converted to a string.
+    3) Newline (\n) characters delimit lines. The newline parsing is done
+       after all the other parsing, so it is not possible for arguments
+       (e.g. file names) to contain embedded newline characters.
+    """
 
-    def repl(m, globals=globals, locals=locals):
+    def convert(x):
+        """This function is used to convert construction variable
+        values or the value of strSubst to a string for interpolation.
+        This function follows the rules outlined in the documentaion
+        for scons_subst_list()"""
+        if x is None:
+            return ''
+        elif is_String(x):
+            return _space_sep.sub('\0', x)
+        elif is_List(x):
+            return string.join(map(to_String, x), '\0')
+        else:
+            return to_String(x)
+
+    def repl(m, globals=globals, locals=locals, convert=convert):
         key = m.group(1)
         if key[0] == '{':
             key = key[1:-1]
         try:
             e = eval(key, globals, locals)
-            if e is None:
-                s = ''
-            elif is_List(e):
-                s = string.join(map(to_String, e), '\0')
-            else:
-                s = _space_sep.sub('\0', to_String(e))
+            return convert(e)
         except NameError:
-            s = ''
-        return s
+            return ''
 
-    if is_List(strSubst):
-        # This looks like our input is a list of strings,
-        # as explained in the docstring above.  Munge
-        # it into a tokenized string by concatenating
-        # the list with nulls.
-        strSubst = string.join(strSubst, '\0')
-    else:
-        # Tokenize the original string...
-        strSubst = _space_sep.sub('\0', to_String(strSubst))
+    # Convert the argument to a string:
+    strSubst = convert(strSubst)
 
-    # Now, do the substitution
+    # Do the interpolation:
     n = 1
     while n != 0:
         strSubst, n = _cv.subn(repl, strSubst)
-    # Now parse the whole list into tokens.
+        
+    # Convert the interpolated string to a list of lines:
     listLines = string.split(strSubst, '\n')
+
+    # Remove the patterns that match the remove argument: 
     if remove:
         listLines = map(lambda x,re=remove: re.sub('', x), listLines)
+
+    # Finally split each line up into a list of arguments:
     return map(lambda x: filter(lambda y: y, string.split(x, '\0')),
                listLines)
 
index 1f712cf525e1f8a2d2f102639b59cb1d9f795e42..cad5670b81d2b67e422c81a130c4188d5f3cc31a 100644 (file)
@@ -136,6 +136,13 @@ class UtilTestCase(unittest.TestCase):
 
     def test_subst_list(self):
         """Testing the scons_subst_list() method..."""
+
+        class Node:
+            def __init__(self, name):
+                self.name = name
+            def __str__(self):
+                return self.name
+        
         loc = {}
         loc['TARGETS'] = PathList(map(os.path.normpath, [ "./foo/bar.exe",
                                                           "/bar/baz with spaces.obj",
@@ -147,6 +154,11 @@ class UtilTestCase(unittest.TestCase):
         loc['xxx'] = None
         loc['NEWLINE'] = 'before\nafter'
 
+        loc['DO'] = Node('do something')
+        loc['FOO'] = Node('foo.in')
+        loc['BAR'] = Node('bar with spaces.out')
+        loc['CRAZY'] = Node('crazy\nfile.in')
+
         if os.sep == '/':
             def cvt(str):
                 return str
@@ -167,6 +179,27 @@ class UtilTestCase(unittest.TestCase):
         assert cmd_list[1][0] == 'after', cmd_list[1][0]
         assert cmd_list[0][2] == cvt('../foo/ack.cbefore'), cmd_list[0][2]
 
+        cmd_list = scons_subst_list("$DO --in=$FOO --out=$BAR", loc, {})
+        assert len(cmd_list) == 1, cmd_list
+        assert len(cmd_list[0]) == 3, cmd_list
+        assert cmd_list[0][0] == 'do something', cmd_list[0][0]
+        assert cmd_list[0][1] == '--in=foo.in', cmd_list[0][1]
+        assert cmd_list[0][2] == '--out=bar with spaces.out', cmd_list[0][2]
+
+        # XXX: The newline in crazy really should be interpreted as
+        #      part of the file name, and not as delimiting a new command
+        #      line
+        #      In other words the following test fragment is illustrating
+        #      a bug in variable interpolation.
+        cmd_list = scons_subst_list("$DO --in=$CRAZY --out=$BAR", loc, {})
+        assert len(cmd_list) == 2, cmd_list
+        assert len(cmd_list[0]) == 2, cmd_list
+        assert len(cmd_list[1]) == 2, cmd_list
+        assert cmd_list[0][0] == 'do something', cmd_list[0][0]
+        assert cmd_list[0][1] == '--in=crazy', cmd_list[0][1]
+        assert cmd_list[1][0] == 'file.in', cmd_list[1][0]
+        assert cmd_list[1][1] == '--out=bar with spaces.out', cmd_list[1][1]
+        
         # Test inputting a list to scons_subst_list()
         cmd_list = scons_subst_list([ "$SOURCES$NEWLINE", "$TARGETS",
                                         "This is a test" ],
diff --git a/test/spaces.py b/test/spaces.py
new file mode 100644 (file)
index 0000000..f588cc6
--- /dev/null
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2001, 2002 Steven Knight
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import TestSCons
+import sys
+import os
+
+test = TestSCons.TestSCons()
+
+if sys.platform == 'win32':
+    test.write('duplicate a file.bat', 'copy foo.in foo.out\n')
+    copy = test.workpath('duplicate a file.bat')
+else:
+    test.write('duplicate a file.sh', 'cp foo.in foo.out\n')
+    copy = test.workpath('duplicate a file.sh')
+    os.chmod(test.workpath('duplicate a file.sh'), 0777)
+    
+
+test.write('SConstruct', r'''
+env=Environment()
+env.Command("foo.out", "foo.in", [[r"%s", "$SOURCE", "$TARGET"]])
+'''%copy)
+
+test.write('foo.in', 'foo.in 1 \n')
+
+test.run(arguments='foo.out')
+
+test.write('SConstruct', r'''
+env=Environment()
+env["COPY"] = File(r"%s")
+env["ENV"]
+env.Command("foo.out", "foo.in", [["./$COPY", "$SOURCE", "$TARGET"]])
+'''%copy)
+
+test.write('foo.in', 'foo.in 2 \n')
+
+test.run(arguments='foo.out')
+
+test.pass_test()
+