Skip to content

Commit ae507e3

Browse files
jb2170johnslavik
andauthored
gh-119670: Add force keyword only argument to shlex.quote (#148846)
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>
1 parent 6b24376 commit ae507e3

5 files changed

Lines changed: 46 additions & 5 deletions

File tree

Doc/library/shlex.rst

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,15 @@ The :mod:`!shlex` module defines the following functions:
4444
.. versionadded:: 3.8
4545

4646

47-
.. function:: quote(s)
47+
.. function:: quote(s, *, force=False)
4848

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

53+
If *force* is :const:`True`, then *s* is unconditionally quoted,
54+
even if it is already safe for a shell without being quoted.
55+
5356
.. _shlex-quote-warning:
5457

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

97+
The *force* keyword can be used to produce consistent behavior when
98+
escaping multiple strings:
99+
100+
>>> from shlex import quote
101+
>>> filenames = ['my first file', 'file2', 'file 3']
102+
>>> filenames_some_escaped = [quote(f) for f in filenames]
103+
>>> filenames_some_escaped
104+
["'my first file'", 'file2', "'file 3'"]
105+
>>> filenames_all_escaped = [quote(f, force=True) for f in filenames]
106+
>>> filenames_all_escaped
107+
["'my first file'", "'file2'", "'file 3'"]
108+
94109
.. versionadded:: 3.3
95110

111+
.. versionchanged:: next
112+
The *force* keyword was added.
113+
96114
The :mod:`!shlex` module defines the following class:
97115

98116

Doc/whatsnew/3.16.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ os
109109
process via a pidfd. Available on Linux 5.6+.
110110
(Contributed by Maurycy Pawłowski-Wieroński in :gh:`149464`.)
111111

112+
shlex
113+
-----
114+
115+
* Add keyword-only parameter *force* to :func:`shlex.quote` to force quoting
116+
a string, even if it is already safe for a shell without being quoted.
117+
(Contributed by Jay Berry in :gh:`148846`.)
118+
112119
xml
113120
---
114121

Lib/shlex.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,12 @@ def join(split_command):
317317
return ' '.join(quote(arg) for arg in split_command)
318318

319319

320-
def quote(s):
321-
"""Return a shell-escaped version of the string *s*."""
320+
def quote(s, *, force=False):
321+
"""Return a shell-escaped version of the string *s*.
322+
323+
If *force* is *True*, then *s* is unconditionally quoted,
324+
even if it is already safe for a shell without being quoted.
325+
"""
322326
if not s:
323327
return "''"
324328

@@ -329,8 +333,10 @@ def quote(s):
329333
safe_chars = (b'%+,-./0123456789:=@'
330334
b'ABCDEFGHIJKLMNOPQRSTUVWXYZ_'
331335
b'abcdefghijklmnopqrstuvwxyz')
332-
# No quoting is needed if `s` is an ASCII string consisting only of `safe_chars`
333-
if s.isascii() and not s.encode().translate(None, delete=safe_chars):
336+
# No quoting is needed if we are not forcing quoting
337+
# and `s` is an ASCII string consisting only of `safe_chars`.
338+
if (not force
339+
and s.isascii() and not s.encode().translate(None, delete=safe_chars)):
334340
return s
335341

336342
# use single quotes, and put single quotes into double quotes

Lib/test/test_shlex.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,14 @@ def testQuote(self):
342342
self.assertRaises(TypeError, shlex.quote, 42)
343343
self.assertRaises(TypeError, shlex.quote, b"abc")
344344

345+
def testForceQuote(self):
346+
self.assertEqual(shlex.quote("spam"), "spam")
347+
self.assertEqual(shlex.quote("spam", force=False), "spam")
348+
self.assertEqual(shlex.quote("spam", force=True), "'spam'")
349+
self.assertEqual(shlex.quote("spam eggs", force=False), "'spam eggs'")
350+
self.assertEqual(shlex.quote("spam eggs", force=True), "'spam eggs'")
351+
self.assertEqual(shlex.quote("two's-complement", force=False), "'two'\"'\"'s-complement'")
352+
345353
def testJoin(self):
346354
for split_command, command in [
347355
(['a ', 'b'], "'a ' b"),
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add keyword-only parameter *force* to :func:`shlex.quote` to force quoting
2+
a string, even if it is already safe for a shell without being quoted.

0 commit comments

Comments
 (0)