From ddbe3a55a264e5f560b0aebdbfdcd1cf1b01f706 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 21 Apr 2026 04:41:38 +0100 Subject: [PATCH 1/6] shlex: Implement `force` parameter behavior for `shlex.quote` --- Lib/shlex.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Lib/shlex.py b/Lib/shlex.py index 5959f52dd12639..47abd496ccc160 100644 --- a/Lib/shlex.py +++ b/Lib/shlex.py @@ -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 "''" @@ -329,8 +333,11 @@ 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 From 0e722d6818f30bf5c411e4a1af601d5aca04ce9f Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 21 Apr 2026 04:43:22 +0100 Subject: [PATCH 2/6] shlex: Make `force` a keyword only argument in `shlex.quote` 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. --- Lib/shlex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/shlex.py b/Lib/shlex.py index 47abd496ccc160..6df6ae5819c860 100644 --- a/Lib/shlex.py +++ b/Lib/shlex.py @@ -317,7 +317,7 @@ def join(split_command): return ' '.join(quote(arg) for arg in split_command) -def quote(s, force=False): +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 From fd4af184f3254a1137c832fb85ed4aa71fe83ba9 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 21 Apr 2026 04:48:53 +0100 Subject: [PATCH 3/6] shlex tests: Add testForceQuote Test special cases of strings that don't need quoting, do need quoting, do use `force`, don't use `force` etc. I've tried to be exhaustive. --- Lib/test/test_shlex.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Lib/test/test_shlex.py b/Lib/test/test_shlex.py index 2a355abdeeb30f..2089206a0ec1a4 100644 --- a/Lib/test/test_shlex.py +++ b/Lib/test/test_shlex.py @@ -341,6 +341,28 @@ def testQuote(self): "'test%s'\"'\"'name'\"'\"''" % u) self.assertRaises(TypeError, shlex.quote, 42) self.assertRaises(TypeError, shlex.quote, b"abc") + # self.assertRaises(TypeError, shlex.quote, None) + + def testForceQuote(self): + # ensure default `force` behavior does not unnecessarily quote strings + self.assertEqual(shlex.quote("no-quotes-needed"), + "no-quotes-needed") + + # ensure `force=False` does not unnecessarily quote strings + self.assertEqual(shlex.quote("no-quotes-needed", force=False), + "no-quotes-needed") + + # ensure `force=True` does quote strings that + # would not be quoted if using `force=False` + self.assertEqual(shlex.quote("no-quotes-needed", force=True), + "'no-quotes-needed'") + + # ensure `force` does not affect outcome for strings that + # need quoting anyways + self.assertEqual(shlex.quote("quotes needed", force=False), + "'quotes needed'") + self.assertEqual(shlex.quote("quotes needed", force=True), + "'quotes needed'") def testJoin(self): for split_command, command in [ From 78b6f3b7d6bb2ad3a4fbd5807f083b464b916890 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 21 Apr 2026 06:26:41 +0100 Subject: [PATCH 4/6] shlex: Update documentation to mention `shlex.quote`'s `force` kwarg --- Doc/library/shlex.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Doc/library/shlex.rst b/Doc/library/shlex.rst index 2ab12f2f6f9169..24fe6e2ac18cd7 100644 --- a/Doc/library/shlex.rst +++ b/Doc/library/shlex.rst @@ -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:: @@ -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: From 762999d9e35f13f12f596c976fd4961aec5d88f1 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 21 Apr 2026 06:31:13 +0100 Subject: [PATCH 5/6] Add blurb entry for gh-119670: Add force keyword only argument to `shlex.force` --- .../Library/2026-04-21-06-30-59.gh-issue-119670.pMWZfY.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-21-06-30-59.gh-issue-119670.pMWZfY.rst 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 index 00000000000000..a0fa6c726cb66c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-21-06-30-59.gh-issue-119670.pMWZfY.rst @@ -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. From 2a301a5b3535bde40a94b5c956541d55a96af36c Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 21 Apr 2026 19:38:02 +0100 Subject: [PATCH 6/6] Add whatsnew entry for `shlex.quote` gaining `force` kwarg --- Doc/whatsnew/3.15.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index c4dac339be66af..70996b5425a792 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -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