diff --git a/news/3158.bugfix.rst b/news/3158.bugfix.rst new file mode 100644 index 00000000..661fb09e --- /dev/null +++ b/news/3158.bugfix.rst @@ -0,0 +1 @@ +Fix package installation when the virtual environment path contains parentheses. diff --git a/pipenv/cmdparse.py b/pipenv/cmdparse.py index 087a95b1..cec19273 100644 --- a/pipenv/cmdparse.py +++ b/pipenv/cmdparse.py @@ -1,3 +1,4 @@ +import itertools import re import shlex @@ -8,6 +9,12 @@ class ScriptEmptyError(ValueError): pass +def _quote_if_contains(value, pattern): + if next(re.finditer(pattern, value), None): + return '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', value)) + return value + + class Script(object): """Parse a script line (in Pipfile's [scripts] section). @@ -56,17 +63,21 @@ class Script(object): The result is then quoted into a pair of double quotes to be grouped. An argument is intentionally not quoted if it does not contain - whitespaces. This is done to be compatible with Windows built-in + foul characters. This is done to be compatible with Windows built-in commands that don't work well with quotes, e.g. everything with `echo`, and DOS-style (forward slash) switches. + Foul characters include: + + * Whitespaces. + * Parentheses in the command. (pypa/pipenv#3168) + The intended use of this function is to pre-process an argument list before passing it into ``subprocess.Popen(..., shell=True)``. See also: https://docs.python.org/3/library/subprocess.html#converting-argument-sequence """ - return " ".join( - arg if not next(re.finditer(r'\s', arg), None) - else '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', arg)) - for arg in self._parts - ) + return " ".join(itertools.chain( + [_quote_if_contains(self.command, r'[\s()]')], + (_quote_if_contains(arg, r'\s') for arg in self.args), + )) diff --git a/tests/unit/test_cmdparse.py b/tests/unit/test_cmdparse.py index 06012d07..1b329a53 100644 --- a/tests/unit/test_cmdparse.py +++ b/tests/unit/test_cmdparse.py @@ -47,3 +47,20 @@ def test_cmdify_complex(): '-c', """ "print(\'Double quote: \\\"\')" """.strip(), ]), script + + +@pytest.mark.run +@pytest.mark.script +def test_cmdify_quote_if_paren_in_command(): + """Ensure ONLY the command is quoted if it contains parentheses. + """ + script = Script.parse(' '.join([ + '"C:\\Python36(x86)\\python.exe"', + '-c', + "print(123)", + ])) + assert script.cmdify() == ' '.join([ + '"C:\\Python36(x86)\\python.exe"', + '-c', + "print(123)", + ]), script