Skip to content
20 changes: 19 additions & 1 deletion Doc/library/shlex.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ The :mod:`!shlex` module defines the following functions:
.. versionadded:: 3.8


.. function:: quote(s)
.. function:: quote(s, *, force=False)

Return a shell-escaped version of the string *s*. The returned value is a
string that can safely be used as one token in a shell command line, for
cases where you cannot use a list.

If *force* is :const:`True` then *s* will be quoted even if it is already
safe for a shell without being quoted.

.. _shlex-quote-warning:

.. warning::
Expand Down Expand Up @@ -91,8 +94,23 @@ The :mod:`!shlex` module defines the following functions:
>>> command
['ls', '-l', 'somefile; rm -rf ~']

The *force* keyword can be used to produce consistent behavior when
escaping multiple strings:

>>> from shlex import quote
>>> filenames = ['my first file', 'file2', 'file 3']
>>> filenames_some_escaped = [quote(f, force=False) for f in filenames]
>>> filenames_some_escaped
["'my first file'", 'file2', "'file 3'"]
>>> filenames_all_escaped = [quote(f, force=True) for f in filenames]
>>> filenames_all_escaped
["'my first file'", "'file2'", "'file 3'"]

.. versionadded:: 3.3

.. versionchanged:: next
The *force* keyword was added.

The :mod:`!shlex` module defines the following class:


Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,15 @@ New deprecations
Hugo van Kemenade in :gh:`148100`.)


* :mod:`shlex`:

* :func:`shlex.quote` has a new keyword-only parameter *force* that ensures
a string will always be quoted, even if it is already safe for a shell
without being quoted.

(Contributed by Jay Berry in :gh:`148846`.)


* :mod:`struct`:

* Calling the ``Struct.__new__()`` without required argument now is
Expand Down
14 changes: 10 additions & 4 deletions Lib/shlex.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,12 @@ def join(split_command):
return ' '.join(quote(arg) for arg in split_command)


def quote(s):
"""Return a shell-escaped version of the string *s*."""
def quote(s, *, force=False):
"""Return a shell-escaped version of the string *s*.

If *force* is *True* then *s* will be quoted even if it is
already safe for a shell without being quoted.
"""
if not s:
return "''"

Expand All @@ -329,8 +333,10 @@ def quote(s):
safe_chars = (b'%+,-./0123456789:=@'
b'ABCDEFGHIJKLMNOPQRSTUVWXYZ_'
b'abcdefghijklmnopqrstuvwxyz')
# No quoting is needed if `s` is an ASCII string consisting only of `safe_chars`
if s.isascii() and not s.encode().translate(None, delete=safe_chars):
if (not force
and s.isascii() and not s.encode().translate(None, delete=safe_chars)):
# No quoting is needed if we're not forcing quoting
# and `s` is an ASCII string consisting only of `safe_chars`
return s

# use single quotes, and put single quotes into double quotes
Expand Down
7 changes: 7 additions & 0 deletions Lib/test/test_shlex.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,13 @@ def testQuote(self):
self.assertRaises(TypeError, shlex.quote, 42)
self.assertRaises(TypeError, shlex.quote, b"abc")

def testForceQuote(self):
self.assertEqual(shlex.quote("spam"), "spam")
self.assertEqual(shlex.quote("spam", force=False), "spam")
self.assertEqual(shlex.quote("spam", force=True), "'spam'")
self.assertEqual(shlex.quote("spam eggs", force=False), "'spam eggs'")
self.assertEqual(shlex.quote("spam eggs", force=True), "'spam eggs'")

def testJoin(self):
for split_command, command in [
(['a ', 'b'], "'a ' b"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add *force* keyword only argument to :func:`shlex.quote` to always quote the
string passed to it, even if it is already safe for a shell without being
quoted.
Loading