Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Doc/library/shlex.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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']))
Comment thread
bbayles marked this conversation as resolved.
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
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,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
------
Expand Down
7 changes: 6 additions & 1 deletion Lib/shlex.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from io import StringIO

__all__ = ["shlex", "split", "quote"]
__all__ = ["shlex", "split", "quote", "join"]

class shlex:
"A lexical analyzer class for simple shell-like syntaxes."
Expand Down Expand Up @@ -305,6 +305,11 @@ def split(s, comments=False, posix=True):
return list(lex)


def join(split_command):
Comment thread
bbayles marked this conversation as resolved.
"""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):
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_shlex.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,26 @@ def testQuote(self):
self.assertEqual(shlex.quote("test%s'name'" % u),
"'test%s'\"'\"'name'\"'\"''" % u)

def testJoin(self):
Comment thread
bbayles marked this conversation as resolved.
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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The :mod:`shlex` module now exposes :func:`shlex.join`, the inverse of
:func:`shlex.split`. Patch by Bo Bayles.