From 6a93e6cf443418740bf6e23b0d261ac8db07ac1e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 28 Nov 2018 19:22:47 +0800 Subject: [PATCH] Quote arguments with carets for cmd.exe Carets introduce a difficult situation since they are essentially "lossy" when parses. 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. --- news/3307.bugfix.rst | 1 + pipenv/cmdparse.py | 19 +++++++++++++++++-- tests/unit/test_cmdparse.py | 9 +++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 news/3307.bugfix.rst 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