From ca804955927dddb6ae5a846dbc0248a932be9a4e Mon Sep 17 00:00:00 2001 From: Bo Bayles Date: Wed, 29 May 2019 03:06:12 -0500 Subject: bpo-22454: Add shlex.join() (the opposite of shlex.split()) (GH-7605) --- Doc/library/shlex.rst | 15 +++++++++++++++ Doc/whatsnew/3.8.rst | 5 +++++ Lib/shlex.py | 7 ++++++- Lib/test/test_shlex.py | 20 ++++++++++++++++++++ .../Library/2018-06-10-17-48-07.bpo-22454.qeiy_X.rst | 2 ++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2018-06-10-17-48-07.bpo-22454.qeiy_X.rst diff --git a/Doc/library/shlex.rst b/Doc/library/shlex.rst index fb335c6..8c5b023 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 d1305dc..f704b47 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 2c9786c..fb1130d 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 fd35788..a432610 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 0000000..2f30e5c --- /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. -- cgit v0.12