Source code for bookbuilderpy.shell
"""The tool for invoking shell commands."""
import subprocess # nosec
from typing import Callable, Iterable
from bookbuilderpy.logger import logger
from bookbuilderpy.path import Path
[docs]def shell(command: str | Iterable[str],
timeout: int = 3600,
cwd: str | None = None,
wants_stdout: bool = False,
exit_code_to_str: dict[int, str] | None = None,
check_stderr: Callable[[str], BaseException | None]
= lambda x: None) -> str | None:
"""
Execute a text-based command on the shell.
The command is executed and its stdout and stderr and return code are
captured. If the command had a non-zero exit code, an exception is
thrown. The command itself, as well as the parameters are logged via
the logger. If wants_stdout is True, the command's stdout is returned.
Otherwise, None is returned.
:param command: the command to execute
:param timeout: the timeout
:param cwd: the directory to run inside
:param wants_stdout: if `True`, the stdout is returned, if `False`,
`None` is returned
:param exit_code_to_str: an optional map
converting erroneous exit codes to strings
:param check_stderr: an optional callable that is applied to the std_err
string and may raise an exception if need be
"""
if timeout < 0:
raise ValueError(f"Timeout must be positive, but is {timeout}.")
if not command:
raise ValueError(f"Empty command '{command}'!")
cmd = [s.strip() for s in list(command)]
cmd = [s for s in cmd if s]
if not cmd:
raise ValueError(f"Empty stripped command '{cmd}'!")
execstr = "' '".join(cmd)
if cwd:
wd = Path.directory(cwd)
execstr = f"'{execstr}' in '{wd}'"
logger(f"executing {execstr}.")
# nosemgrep
ret = subprocess.run(cmd, check=False, text=True, # nosec # noqa
timeout=timeout, # nosec # noqa
capture_output=True, # nosec # noqa
cwd=wd) # nosec # noqa
else:
execstr = f"'{execstr}'"
logger(f"executing {execstr}.")
# nosemgrep
ret = subprocess.run(cmd, check=False, text=True, # nosec # noqa
timeout=timeout, # nosec # noqa
capture_output=True) # nosec # noqa
logging = [f"finished executing {execstr}.",
f"obtained return value {ret.returncode}."]
if (ret.returncode != 0) and exit_code_to_str:
ec: str | None = exit_code_to_str.get(ret.returncode, None)
if ec:
logging.append(f"meaning of return value: {ec}")
stdout = ret.stdout
if stdout:
stdout = stdout.strip()
if stdout:
logging.append(f"\nstdout:\n{stdout}")
else:
stdout = ""
stderr = ret.stderr
if stderr:
stderr = stderr.strip()
if stderr:
logging.append(f"\nstderr:\n{stderr}")
logger("\n".join(logging))
if ret.returncode != 0:
raise ValueError(
f"Error {ret.returncode} when executing {execstr} compressor.")
if stderr:
exception = check_stderr(stderr)
if exception is not None:
raise exception
return stdout if wants_stdout else None