Skip to content

gh-151485: Fix command quoting in subprocess.CalledProcessError.__str__#151486

Open
BenjyWiener wants to merge 3 commits into
python:mainfrom
BenjyWiener:fix-CalledProcessError-quoting
Open

gh-151485: Fix command quoting in subprocess.CalledProcessError.__str__#151486
BenjyWiener wants to merge 3 commits into
python:mainfrom
BenjyWiener:fix-CalledProcessError-quoting

Conversation

@BenjyWiener

@BenjyWiener BenjyWiener commented Jun 15, 2026

Copy link
Copy Markdown

subprocess.CalledProcessError.__str__ previously formatted cmd as "... '%s' ...". This lead to unbalanced quoting when cmd contains single-quotes or, more commonly, when cmd is a list. This change updates the relevant format strings to use %r instead.

In terms of backwards compatibility, the only impact I anticipate is on doctests, and I suspect doctests for subprocess.CalledProcessError to be quite rare.

Before this PR:

>>> import subprocess
>>> subprocess.check_call(['false'])
Traceback (most recent call last):
  ...
subprocess.CalledProcessError: Command '['false']' returned non-zero exit status 1.

After:

>>> import subprocess
>>> subprocess.check_call(['false'])
Traceback (most recent call last):
  ...
subprocess.CalledProcessError: Command ['false'] returned non-zero exit status 1.

CalledProcessError previously formatted cmd as `"... '%s' ..."`. This lead to
unbalanced quoting when cmd contains single-quotes or, more commonly, when cmd
is a list. This change updates the relevant format strings to use %r instead.
@bedevere-app

bedevere-app Bot commented Jun 15, 2026

Copy link
Copy Markdown

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a test to test_subprocess?

Comment on lines 2431 to +2461
def test_CalledProcessError_str_signal(self):
err = subprocess.CalledProcessError(-int(signal.SIGABRT), "fake cmd")
error_string = str(err)
# We're relying on the repr() of the signal.Signals intenum to provide
# the word signal, the signal name and the numeric value.
self.assertIn("signal", error_string.lower())
# We're not being specific about the signal name as some signals have
# multiple names and which name is revealed can vary.
self.assertIn("SIG", error_string)
self.assertIn(str(signal.SIGABRT), error_string)

def test_CalledProcessError_str_unknown_signal(self):
err = subprocess.CalledProcessError(-9876543, "fake cmd")
error_string = str(err)
self.assertIn("unknown signal 9876543.", error_string)

def test_CalledProcessError_str_non_zero(self):
err = subprocess.CalledProcessError(2, "fake cmd")
error_string = str(err)
self.assertIn("non-zero exit status 2.", error_string)

def test_CalledProcessError_str_quote_in_cmd(self):
err = subprocess.CalledProcessError(2, "fake ' cmd")
error_string = str(err)
self.assertStartsWith(error_string, 'Command "fake \' cmd" ')

def test_CalledProcessError_str_list_cmd(self):
err = subprocess.CalledProcessError(2, ["fake", "cmd"])
error_string = str(err)
self.assertStartsWith(error_string, "Command ['fake', 'cmd'] ")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, IMO it's now time to check the whole string using assertEqual(). I suggest rewriting existing tests in a single method, and add your 2 new tests there:

Suggested change
def test_CalledProcessError_str(self):
# command string
err = subprocess.CalledProcessError(2, "fake cmd")
self.assertEqual(str(err), "Command 'fake cmd' returned non-zero exit status 2.")
# command string with a quote
err = subprocess.CalledProcessError(2, "fake ' cmd")
self.assertEqual(str(err), 'Command "fake \' cmd" returned non-zero exit status 2.')
# command list
err = subprocess.CalledProcessError(2, ["fake", "cmd"])
self.assertEqual(str(err), "Command ['fake', 'cmd'] returned non-zero exit status 2.")
# signal
err = subprocess.CalledProcessError(-int(signal.SIGABRT), "fake cmd")
self.assertEqual(str(err), f"Command 'fake cmd' died with {repr(signal.SIGABRT)}.")
# unknown signal
err = subprocess.CalledProcessError(-9876543, "fake cmd")
self.assertEqual(str(err), "Command 'fake cmd' died with unknown signal 9876543.")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Is there a preference for f'{repr(...)}' over {...!r} (for the signal case)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the same, pick your favorite syntax.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants