Repository: tox-dev/tox
Pull request #385 (2016-10-25 10:50:09)

 fix issue310: the {posargs} substitution now properly preserves the…
… tox command line

  positional arguments. Positional arguments with spaces are now properly handled.
  NOTE: if your tox invocation previously used extra quoting for positional arguments to
  work around issue310, you need to remove the quoting. Example:
  tox -- "'some string'" can has to be written simply as "tox -- "some string".


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 560c0240..63bfa8d3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,10 +1,18 @@
 unreleased
 ----------
 
+- fix issue310: the {posargs} substitution now properly preserves the tox command line
+  positional arguments. Positional arguments with spaces are now properly handled.
+  NOTE: if your tox invocation previously used extra quoting for positional arguments to
+  work around issue310, you need to remove the quoting. Example:
+  tox -- "'some string'"  # can has to be written simply as 
+  tox -- "some string"
+
 - add support for py36 and py37 and add py36-dev and py37(nightly) to
   travis builds. Thanks John Vandenberg.
 
 
+
 2.4.1
 -----
 
diff --git a/tox/config.py b/tox/config.py
index f3fa3950..40f64e20 100644
--- a/tox/config.py
+++ b/tox/config.py
@@ -9,6 +9,7 @@ import string
 import pkg_resources
 import itertools
 import pluggy
+from subprocess import list2cmdline
 
 import tox.interpreters
 from tox import hookspecs
@@ -1146,7 +1147,8 @@ class _ArgvlistReader:
 
     @classmethod
     def processcommand(cls, reader, command):
-        posargs = getattr(reader, "posargs", None)
+        posargs = getattr(reader, "posargs", "")
+        posargs_string = list2cmdline([x for x in posargs if x])
 
         # Iterate through each word of the command substituting as
         # appropriate to construct the new command string. This
@@ -1155,12 +1157,11 @@ class _ArgvlistReader:
         newcommand = ""
         for word in CommandParser(command).words():
             if word == "{posargs}" or word == "[]":
-                if posargs:
-                    newcommand += " ".join(posargs)
+                newcommand += posargs_string
                 continue
             elif word.startswith("{posargs:") and word.endswith("}"):
                 if posargs:
-                    newcommand += " ".join(posargs)
+                    newcommand += posargs_string
                     continue
                 else:
                     word = word[9:-1]
@@ -1176,8 +1177,7 @@ class _ArgvlistReader:
         shlexer = shlex.shlex(newcommand, posix=True)
         shlexer.whitespace_split = True
         shlexer.escape = ''
-        argv = list(shlexer)
-        return argv
+        return list(shlexer)
 
 
 class CommandParser(object):


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index c26063eb..419d3321 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -598,6 +598,17 @@ class TestIniParser:
         assert argvlist[2] == ['cmd2', '-m', "something"] + posargs
         assert argvlist[3] == ['cmd3', 'something[]else']
 
+    def test_posargs_are_added_escaped_issue310(self, newconfig):
+        config = newconfig("""
+            [section]
+            key= cmd0 {posargs}
+        """)
+        reader = SectionReader("section", config._cfg)
+        posargs = ['hello world', '--x==y z', '--format=%(code)s: %(text)s']
+        reader.addsubstitutions(posargs)
+        argvlist = reader.getargvlist('key')
+        assert argvlist[0] == ['cmd0'] + posargs
+
     def test_substitution_with_multiple_words(self, newconfig):
         inisource = """
             [section]