]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-119670: Add `force` keyword only argument to `shlex.quote` (#148846)
authorjb2170 <email@jb2170.com>
Thu, 4 Jun 2026 09:06:02 +0000 (10:06 +0100)
committerGitHub <noreply@github.com>
Thu, 4 Jun 2026 09:06:02 +0000 (11:06 +0200)
There are propositions to add a single-quote-double-quote switch
(gh-90630), so to avoid hiccups of people passing `force` as a
positional and it being used for the single-double switch, we make
kwargs kwargs-only.

Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
Doc/library/shlex.rst
Doc/whatsnew/3.16.rst
Lib/shlex.py
Lib/test/test_shlex.py
Misc/NEWS.d/next/Library/2026-04-21-06-30-59.gh-issue-119670.pMWZfY.rst [new file with mode: 0644]

index 2ab12f2f6f9169e9b0dd9c638e15d1799be511d0..2dfb0246d5d90c026d032116f23447f172048891 100644 (file)
@@ -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* is unconditionally quoted,
+   even if it is already safe for a shell without being quoted.
+
    .. _shlex-quote-warning:
 
    .. warning::
@@ -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) 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:
 
 
index 0aff48dba61449c3d5bd1381514a33604c260cdc..a055113dec0494cec35dc1771dc0aefc9d041923 100644 (file)
@@ -109,6 +109,13 @@ os
   process via a pidfd.  Available on Linux 5.6+.
   (Contributed by Maurycy Pawłowski-Wieroński in :gh:`149464`.)
 
+shlex
+-----
+
+* Add keyword-only parameter *force* to :func:`shlex.quote` to force quoting
+  a string, even if it is already safe for a shell without being quoted.
+  (Contributed by Jay Berry in :gh:`148846`.)
+
 xml
 ---
 
index 5959f52dd12639de2afd6f2f1a6bc312ab794163..c7ffc918d53961c1ac0b3fd48a0453692ca16489 100644 (file)
@@ -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* is unconditionally quoted,
+    even if it is already safe for a shell without being quoted.
+    """
     if not s:
         return "''"
 
@@ -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):
+    # No quoting is needed if we are not forcing quoting
+    # and `s` is an ASCII string consisting only of `safe_chars`.
+    if (not force
+        and s.isascii() and not s.encode().translate(None, delete=safe_chars)):
         return s
 
     # use single quotes, and put single quotes into double quotes
index 2a355abdeeb30fbcf27a2ae6316c11def3b286dd..2adaee81b063085d05809e620a131dfc77780469 100644 (file)
@@ -342,6 +342,14 @@ class ShlexTest(unittest.TestCase):
         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'")
+        self.assertEqual(shlex.quote("two's-complement", force=False), "'two'\"'\"'s-complement'")
+
     def testJoin(self):
         for split_command, command in [
             (['a ', 'b'], "'a ' b"),
diff --git a/Misc/NEWS.d/next/Library/2026-04-21-06-30-59.gh-issue-119670.pMWZfY.rst b/Misc/NEWS.d/next/Library/2026-04-21-06-30-59.gh-issue-119670.pMWZfY.rst
new file mode 100644 (file)
index 0000000..fc1941b
--- /dev/null
@@ -0,0 +1,2 @@
+Add keyword-only parameter *force* to :func:`shlex.quote` to force quoting
+a string, even if it is already safe for a shell without being quoted.