diff --git a/news/3307.bugfix.rst b/news/3307.bugfix.rst new file mode 100644 index 00000000..0f095c1a --- /dev/null +++ b/news/3307.bugfix.rst @@ -0,0 +1 @@ +Quote command arguments with carets (``^``) on Windows to work around unintended shell escapes. diff --git a/pipenv/cmdparse.py b/pipenv/cmdparse.py index cec19273..21aba77e 100644 --- a/pipenv/cmdparse.py +++ b/pipenv/cmdparse.py @@ -70,14 +70,29 @@ class Script(object): Foul characters include: * Whitespaces. + * Carets (^). (pypa/pipenv#3307) * Parentheses in the command. (pypa/pipenv#3168) + Carets introduce a difficult situation since they are essentially + "lossy" when parsed. Consider this in cmd.exe:: + + > echo "foo^bar" + "foo^bar" + > echo foo^^bar + foo^bar + + The two commands produce different results, but are both parsed by the + shell as `foo^bar`, and there's essentially no sensible way to tell + what was actually passed in. This implementation assumes the quoted + variation (the first) since it is easier to implement, and arguably + the more common case. + 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(itertools.chain( - [_quote_if_contains(self.command, r'[\s()]')], - (_quote_if_contains(arg, r'\s') for arg in self.args), + [_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 1b329a53..912031e9 100644 --- a/tests/unit/test_cmdparse.py +++ b/tests/unit/test_cmdparse.py @@ -64,3 +64,12 @@ def test_cmdify_quote_if_paren_in_command(): '-c', "print(123)", ]), script + + +@pytest.mark.run +@pytest.mark.script +def test_cmdify_quote_if_carets(): + """Ensure arguments are quoted if they contain carets. + """ + script = Script('foo^bar', ['baz^rex']) + assert script.cmdify() == '"foo^bar" "baz^rex"', script