Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 2.2.2 - 2/23/25

- Bugfix where it was impossible to use a signal as an `ok_code` [#699](https://github.com/amoffat/sh/issues/699)

## 2.2.1 - 1/9/25

- Bugfix where `async` and `return_cmd` does not raise exceptions [#746](https://github.com/amoffat/sh/pull/746)
Expand Down
10 changes: 10 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ To run a single test::

$> make test='FunctionalTests.test_background' test_one

Docs
----

To build the docs, make sure you've run ``poetry install`` to install the dev dependencies, then::

$> cd docs
$> make html

This will generate the docs in ``docs/build/html``. You can open the ``index.html`` file in your browser to view the docs.

Coverage
--------

Expand Down
5 changes: 5 additions & 0 deletions docs/source/sections/exit_codes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ exception raised in this situation is :ref:`signal_exc`, which subclasses
except sh.SignalException_SIGKILL:
print("killed")

This behavior could be blocked by appending the negative value of the signal to
:ref:`ok_code`. All signals that raises :ref:`signal_exc` are ``[SIGABRT,
SIGBUS, SIGFPE, SIGILL, SIGINT, SIGKILL, SIGPIPE, SIGQUIT, SIGSEGV, SIGTERM,
SIGTERM]``.

.. note::

You can catch :ref:`signal_exc` by using either a number or a signal name.
Expand Down
14 changes: 14 additions & 0 deletions docs/source/sections/special_arguments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,20 @@ programs use exit codes other than 0 to indicate success.
import sh
sh.weird_program(_ok_code=[0,3,5])

If the process is killed by a signal, a :ref:`signal_exc` is raised by
default. This behavior could be blocked by appending a negative number to
:ref:`ok_code` that represents the signal.

.. code-block:: python

import sh
# the process won't raise SignalException if SIGINT, SIGKILL, or SIGTERM
# are sent to kill the process
p = sh.sleep(3, _bg=True, _ok_code=[0, -2, -9, -15])

# No exception will be raised here
p.kill()

.. seealso:: :ref:`exit_codes`

.. _new_session:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "sh"
version = "2.2.1"
version = "2.2.2"
description = "Python subprocess replacement"
authors = ["Andrew Moffat <[email protected]>"]
readme = "README.rst"
Expand Down
11 changes: 7 additions & 4 deletions sh.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
https://sh.readthedocs.io/en/latest/
https://github.com/amoffat/sh
"""

# ===============================================================================
# Copyright (C) 2011-2023 by Andrew Moffat
# Copyright (C) 2011-2025 by Andrew Moffat
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand All @@ -24,10 +25,9 @@
# THE SOFTWARE.
# ===============================================================================
import asyncio
import platform
from collections import deque
from collections.abc import Mapping

import platform
from importlib import metadata

try:
Expand Down Expand Up @@ -1730,7 +1730,10 @@ def fn(chunk):
def get_exc_exit_code_would_raise(exit_code, ok_codes, sigpipe_ok):
exc = None
success = exit_code in ok_codes
bad_sig = -exit_code in SIGNALS_THAT_SHOULD_THROW_EXCEPTION
signals_that_should_throw_exception = [
sig for sig in SIGNALS_THAT_SHOULD_THROW_EXCEPTION if -sig not in ok_codes
]
bad_sig = -exit_code in signals_that_should_throw_exception

# if this is a piped command, SIGPIPE must be ignored by us and not raise an
# exception, since it's perfectly normal for the consumer of a process's
Expand Down
70 changes: 70 additions & 0 deletions tests/sh_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@
tempdir = Path(tempfile.gettempdir()).resolve()
IS_MACOS = platform.system() in ("AIX", "Darwin")

SIGNALS_THAT_SHOULD_THROW_EXCEPTION = [
signal.SIGABRT,
signal.SIGBUS,
signal.SIGFPE,
signal.SIGILL,
signal.SIGINT,
signal.SIGKILL,
signal.SIGPIPE,
signal.SIGQUIT,
signal.SIGSEGV,
signal.SIGTERM,
signal.SIGSYS,
]


def hash(a: str):
h = md5(a.encode("utf8") + RAND_BYTES)
Expand Down Expand Up @@ -87,6 +101,7 @@ def append_module_path(env, m):
append_module_path(baked_env, sh)
python = system_python.bake(_env=baked_env, _return_cmd=True)
pythons = python.bake(_return_cmd=False)
python_bg = system_python.bake(_env=baked_env, _bg=True)


def requires_progs(*progs):
Expand Down Expand Up @@ -3137,6 +3152,61 @@ def test_unchecked_pipeline_failure(self):
ErrorReturnCode_2, python, middleman_normal_pipe, consumer.name
)

def test_bad_sig_raise_exception(self):
# test all bad signal are correctly raised
py = create_tmp_test(
"""
import time
import sys

time.sleep(2)
sys.exit(1)
"""
)
for sig in SIGNALS_THAT_SHOULD_THROW_EXCEPTION:
if sig == signal.SIGPIPE:
continue
sig_exception_name = f"SignalException_{sig}"
sig_exception = getattr(sh, sig_exception_name)
try:
p = python_bg(py.name)
time.sleep(0.5)
p.signal(sig)
p.wait()
except sig_exception:
pass
else:
self.fail(f"{sig_exception_name} not raised")

def test_ok_code_ignores_bad_sig_exception(self):
# Test if I have [-sig] in _ok_code, the exception won't be raised
py = create_tmp_test(
"""
import time
import sys

time.sleep(2)
sys.exit(1)
"""
)
for sig in SIGNALS_THAT_SHOULD_THROW_EXCEPTION:
if sig == signal.SIGPIPE:
continue
sig_exception_name = f"SignalException_{sig}"
sig_exception = getattr(sh, sig_exception_name)
python_bg_no_sig_exception = python_bg.bake(_ok_code=[-sig])
try:
p = python_bg_no_sig_exception(py.name)
time.sleep(0.5)
p.signal(sig)
p.wait()
except sig_exception:
self.fail(
f"{sig_exception_name} should not be raised setting _ok_code."
)
else:
self.assertEqual(p.exit_code, -sig)


class MockTests(BaseTests):
def test_patch_command_cls(self):
Expand Down
Loading