Skip to content

Commit cf9f2b2

Browse files
authored
Add system_raise_on_error config option for ! shell operator (#15073)
Fixes #12264 Add system_raise_on_error configuration option that raises CalledProcessError when shell commands executed via the ! operator return non-zero exit status. This brings similar error-handling behavior to the ! operator as was added to %%bash magic in PR #11287. Changes: - Add system_raise_on_error Bool traitlet config (default: False) - Import CalledProcessError from subprocess - Modify system_piped() to raise on non-zero exit when enabled - Modify system_raw() to raise on non-zero exit when enabled - Modify getoutput() to raise on non-zero exit when enabled (uses get_output_error_code to capture exit status) When enabled, users can halt notebook execution on command failures: get_ipython().system_raise_on_error = True !false # Now raises CalledProcessError Note: A follow-up PR to ipykernel will be needed to support this in Jupyter notebooks, as ZMQInteractiveShell overrides system_piped().
2 parents 6d0fa10 + d9a261a commit cf9f2b2

File tree

1 file changed

+39
-5
lines changed

1 file changed

+39
-5
lines changed

IPython/core/interactiveshell.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import runpy
2323
import shutil
2424
import subprocess
25+
from subprocess import CalledProcessError
2526
import sys
2627
import tempfile
2728
import traceback
@@ -90,7 +91,7 @@
9091
from IPython.utils.io import ask_yes_no
9192
from IPython.utils.ipstruct import Struct
9293
from IPython.utils.path import ensure_dir_exists, get_home_dir, get_py_filename
93-
from IPython.utils.process import getoutput, system
94+
from IPython.utils.process import get_output_error_code, getoutput, system
9495
from IPython.utils.strdispatch import StrDispatch
9596
from IPython.utils.syspathcontext import prepended_to_syspath
9697
from IPython.utils.text import DollarFormatter, LSString, SList, format_screen
@@ -538,6 +539,14 @@ def input_transformers_cleanup(self):
538539

539540
quiet = Bool(False).tag(config=True)
540541

542+
system_raise_on_error = Bool(False, help=
543+
"""
544+
Raise an exception on non-zero exit status from shell commands executed
545+
via the `!` operator. When set to True, shell commands that fail will raise
546+
CalledProcessError, similar to the behavior of %%script magics.
547+
"""
548+
).tag(config=True)
549+
541550
history_length = Integer(10000,
542551
help='Total length of command history'
543552
).tag(config=True)
@@ -2646,7 +2655,12 @@ def system_piped(self, cmd):
26462655
# we explicitly do NOT return the subprocess status code, because
26472656
# a non-None value would trigger :func:`sys.displayhook` calls.
26482657
# Instead, we store the exit_code in user_ns.
2649-
self.user_ns['_exit_code'] = system(self.var_expand(cmd, depth=1))
2658+
exit_code = system(self.var_expand(cmd, depth=1))
2659+
self.user_ns['_exit_code'] = exit_code
2660+
2661+
# Raise an exception if the command failed and system_raise_on_error is True
2662+
if self.system_raise_on_error and exit_code != 0:
2663+
raise CalledProcessError(exit_code, cmd)
26502664

26512665
def system_raw(self, cmd):
26522666
"""Call the given cmd in a subprocess using os.system on Windows or
@@ -2712,6 +2726,10 @@ def system_raw(self, cmd):
27122726
# but raising SystemExit(_exit_code) will give status 254!
27132727
self.user_ns['_exit_code'] = ec
27142728

2729+
# Raise an exception if the command failed and system_raise_on_error is True
2730+
if self.system_raise_on_error and ec != 0:
2731+
raise CalledProcessError(ec, cmd)
2732+
27152733
# use piped system by default, because it is better behaved
27162734
system = system_piped
27172735

@@ -2737,11 +2755,27 @@ def getoutput(self, cmd, split=True, depth=0):
27372755
if cmd.rstrip().endswith('&'):
27382756
# this is *far* from a rigorous test
27392757
raise OSError("Background processes not supported.")
2740-
out = getoutput(self.var_expand(cmd, depth=depth+1))
2758+
2759+
# Get output and exit code
2760+
expanded_cmd = self.var_expand(cmd, depth=depth+1)
2761+
if self.system_raise_on_error:
2762+
# Use get_output_error_code to get the exit code
2763+
out_str, err_str, exit_code = get_output_error_code(expanded_cmd)
2764+
# Combine stdout and stderr as getoutput does
2765+
out_combined = out_str if not err_str else out_str + err_str
2766+
self.user_ns['_exit_code'] = exit_code
2767+
2768+
# Raise an exception if the command failed
2769+
if exit_code != 0:
2770+
raise CalledProcessError(exit_code, cmd)
2771+
else:
2772+
# Use the original getoutput for backward compatibility
2773+
out_combined = getoutput(expanded_cmd)
2774+
27412775
if split:
2742-
out = SList(out.splitlines())
2776+
out = SList(out_combined.splitlines())
27432777
else:
2744-
out = LSString(out)
2778+
out = LSString(out_combined)
27452779
return out
27462780

27472781
#-------------------------------------------------------------------------

0 commit comments

Comments
 (0)