[elbe-devel] [PATCH 2/5] elbepack: shellhelper: introduce run() function

Thomas Weißschuh thomas.weissschuh at linutronix.de
Tue May 28 12:38:49 CEST 2024


the run() function is a thin wrapper around subprocess.run(),
with the following changes:

* It defaults to check=True for more correctness
* It accepts shellper.ELBE_LOGGING for stdin and stdout to forward these
  through the elbe log

In contrast with the original shellhelper functions, this has
many advantages:

It is much more flexible for callers to achieve the exact behaviour they
require. Specifying stdout, stderr, cwd, check, etc; capturing output and
validating return codes are all available independently from each other.

Executing through a shell needs opt-in as that is more error-prone.

Signed-off-by: Thomas Weißschuh <thomas.weissschuh at linutronix.de>
---
 elbepack/shellhelper.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 69 insertions(+)

diff --git a/elbepack/shellhelper.py b/elbepack/shellhelper.py
index dd7312c913bc..d04699d9977c 100644
--- a/elbepack/shellhelper.py
+++ b/elbepack/shellhelper.py
@@ -3,6 +3,7 @@
 # SPDX-FileCopyrightText: 2014-2017 Linutronix GmbH
 # SPDX-FileCopyrightText: 2014 Ferdinand Schwenk <ferdinand.schwenk at emtrion.de>
 
+import contextlib
 import logging
 import os
 import shlex
@@ -11,6 +12,12 @@ import subprocess
 from elbepack.log import async_logging_ctx
 
 
+"""
+Forward to elbe logging system.
+"""
+ELBE_LOGGING = object()
+
+
 def _is_shell_cmd(cmd):
     return isinstance(cmd, str)
 
@@ -22,6 +29,68 @@ def _log_cmd(cmd):
         return shlex.join(map(os.fspath, cmd))
 
 
+def run(cmd, /, *, check=True, log_cmd=None, **kwargs):
+    """
+    Like subprocess.run() but
+     * defaults to check=True
+     * logs the executed command
+     * accepts ELBE_LOGGING for stdout and stderr
+
+    --
+
+    Let's quiet the loggers
+
+    >>> import os
+    >>> import sys
+    >>> from elbepack.log import open_logging
+    >>> open_logging({"files":os.devnull})
+
+    >>> run(['echo', 'ELBE'])
+    CompletedProcess(args=['echo', 'ELBE'], returncode=0)
+
+    >>> run(['echo', 'ELBE'], capture_output=True)
+    CompletedProcess(args=['echo', 'ELBE'], returncode=0, stdout=b'ELBE\\n', stderr=b'')
+
+    >>> run(['false']) # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    subprocess.CalledProcessError: ...
+
+    >>> run('false', check=False).returncode
+    1
+
+    >>> run(['cat', '-'], input=b'ELBE', capture_output=True).stdout
+    b'ELBE'
+
+    >>> run(['echo', 'ELBE'], stdout=ELBE_LOGGING)
+    CompletedProcess(args=['echo', 'ELBE'], returncode=0)
+
+    Let's redirect the loggers to current stdout
+
+    >>> from elbepack.log import open_logging
+    >>> open_logging({"streams":sys.stdout})
+
+    >>> run(['echo', 'ELBE'], stdout=ELBE_LOGGING)
+    [CMD] echo ELBE
+    ELBE
+    ELBE
+    CompletedProcess(args=['echo', 'ELBE'], returncode=0)
+    """
+    stdout = kwargs.pop('stdout', None)
+    stderr = kwargs.pop('stderr', None)
+
+    with contextlib.ExitStack() as stack:
+        if stdout is ELBE_LOGGING or stderr is ELBE_LOGGING:
+            log_fd = stack.enter_context(async_logging_ctx())
+            if stdout is ELBE_LOGGING:
+                stdout = log_fd
+            if stderr is ELBE_LOGGING:
+                stderr = log_fd
+
+        logging.info(log_cmd or _log_cmd(cmd), extra={'context': '[CMD] '})
+        return subprocess.run(cmd, stdout=stdout, stderr=stderr, check=check, **kwargs)
+
+
 def do(cmd, /, *, check=True, env_add=None, log_cmd=None, **kwargs):
     """do() - Execute cmd in a shell and redirect outputs to logging.
 

-- 
2.45.1



More information about the elbe-devel mailing list