summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBo Bayles <bbayles@gmail.com>2019-05-29 03:06:12 -0500
committerVinay Sajip <vinay_sajip@yahoo.co.uk>2019-05-29 09:06:11 +0100
commitca804955927dddb6ae5a846dbc0248a932be9a4e (patch)
treed9cb8069d21b991842d1210e1641a95ebddde912
parentf83d1dbd3bfbde940117c85f5c70de00e47b7e6e (diff)
downloadcpython-git-ca804955927dddb6ae5a846dbc0248a932be9a4e.tar.gz
bpo-22454: Add shlex.join() (the opposite of shlex.split()) (GH-7605)
-rw-r--r--Doc/library/shlex.rst15
-rw-r--r--Doc/whatsnew/3.8.rst5
-rw-r--r--Lib/shlex.py7
-rw-r--r--Lib/test/test_shlex.py20
-rw-r--r--Misc/NEWS.d/next/Library/2018-06-10-17-48-07.bpo-22454.qeiy_X.rst2
5 files changed, 48 insertions, 1 deletions
diff --git a/Doc/library/shlex.rst b/Doc/library/shlex.rst
index fb335c6900..8c5b0239d1 100644
--- a/Doc/library/shlex.rst
+++ b/Doc/library/shlex.rst
@@ -37,6 +37,21 @@ The :mod:`shlex` module defines the following functions:
standard input.
+.. function:: join(split_command)
+
+ Concatenate the tokens of the list *split_command* and return a string.
+ This function is the inverse of :func:`split`.
+
+ >>> from shlex import join
+ >>> print(join(['echo', '-n', 'Multiple words']))
+ echo -n 'Multiple words'
+
+ The returned value is shell-escaped to protect against injection
+ vulnerabilities (see :func:`quote`).
+
+ .. versionadded:: 3.8
+
+
.. function:: quote(s)
Return a shell-escaped version of the string *s*. The returned value is a
diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst
index d1305dc1e7..f704b47098 100644
--- a/Doc/whatsnew/3.8.rst
+++ b/Doc/whatsnew/3.8.rst
@@ -552,6 +552,11 @@ convenience functions to automate the necessary tasks usually involved when
creating a server socket, including accepting both IPv4 and IPv6 connections
on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.)
+shlex
+----------
+
+The new :func:`shlex.join` function acts as the inverse of :func:`shlex.split`.
+(Contributed by Bo Bayles in :issue:`32102`.)
shutil
------
diff --git a/Lib/shlex.py b/Lib/shlex.py
index 2c9786c517..fb1130d4ea 100644
--- a/Lib/shlex.py
+++ b/Lib/shlex.py
@@ -14,7 +14,7 @@ from collections import deque
from io import StringIO
-__all__ = ["shlex", "split", "quote"]
+__all__ = ["shlex", "split", "quote", "join"]
class shlex:
"A lexical analyzer class for simple shell-like syntaxes."
@@ -305,6 +305,11 @@ def split(s, comments=False, posix=True):
return list(lex)
+def join(split_command):
+ """Return a shell-escaped string from *split_command*."""
+ return ' '.join(quote(arg) for arg in split_command)
+
+
_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search
def quote(s):
diff --git a/Lib/test/test_shlex.py b/Lib/test/test_shlex.py
index fd35788e81..a432610d3a 100644
--- a/Lib/test/test_shlex.py
+++ b/Lib/test/test_shlex.py
@@ -308,6 +308,26 @@ class ShlexTest(unittest.TestCase):
self.assertEqual(shlex.quote("test%s'name'" % u),
"'test%s'\"'\"'name'\"'\"''" % u)
+ def testJoin(self):
+ for split_command, command in [
+ (['a ', 'b'], "'a ' b"),
+ (['a', ' b'], "a ' b'"),
+ (['a', ' ', 'b'], "a ' ' b"),
+ (['"a', 'b"'], '\'"a\' \'b"\''),
+ ]:
+ with self.subTest(command=command):
+ joined = shlex.join(split_command)
+ self.assertEqual(joined, command)
+
+ def testJoinRoundtrip(self):
+ all_data = self.data + self.posix_data
+ for command, *split_command in all_data:
+ with self.subTest(command=command):
+ joined = shlex.join(split_command)
+ resplit = shlex.split(joined)
+ self.assertEqual(split_command, resplit)
+
+
# Allow this test to be used with old shlex.py
if not getattr(shlex, "split", None):
for methname in dir(ShlexTest):
diff --git a/Misc/NEWS.d/next/Library/2018-06-10-17-48-07.bpo-22454.qeiy_X.rst b/Misc/NEWS.d/next/Library/2018-06-10-17-48-07.bpo-22454.qeiy_X.rst
new file mode 100644
index 0000000000..2f30e5c893
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-06-10-17-48-07.bpo-22454.qeiy_X.rst
@@ -0,0 +1,2 @@
+The :mod:`shlex` module now exposes :func:`shlex.join`, the inverse of
+:func:`shlex.split`. Patch by Bo Bayles.