Coverage for src / mafw / tools / shell_tools.py: 100%
34 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-06-28 13:34 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-06-28 13:34 +0000
1# Copyright 2025–2026 European Union
2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
3# SPDX-License-Identifier: EUPL-1.2
4"""
5Shell execution utilities.
7This module provides unified functions for executing external commands via subprocess,
8with support for dry-runs, environment merging, and standardized output.
9"""
11import os
12import shlex
13import subprocess
14from pathlib import Path
15from typing import Any
17from rich.console import Console
19CONSOLE = Console()
20"""Shared console for standardized output."""
23def _to_command_line(command: str | list[str]) -> str:
24 """
25 Convert a command into a printable shell-like string.
27 :param command: Command expressed as a string or tokenized list.
28 :type command: str | list[str]
29 :return: Human-readable command line.
30 :rtype: str
31 """
32 if isinstance(command, str):
33 return command
34 return shlex.join(command)
37def run(
38 command: str | list[str],
39 *,
40 cwd: Path | str | None = None,
41 dry_run: bool = False,
42 env: dict[str, str] | None = None,
43 quiet: bool = False,
44 **kwargs: Any,
45) -> subprocess.CompletedProcess[Any]:
46 """
47 Execute a subprocess command and return a completed process object.
49 The command line is always printed with a standard prefix. In dry-run mode,
50 execution is skipped and a successful synthetic ``CompletedProcess`` is returned.
52 .. versionadded:: 2.2
53 Add the `quiet` parameter to do not display the command being executed
55 :param command: Command to execute.
56 :type command: str | list[str]
57 :param cwd: Working directory for command execution.
58 :type cwd: Path | str | None
59 :param dry_run: If ``True``, print the command without executing it.
60 :type dry_run: bool
61 :param env: Optional environment variables to merge with the current environment.
62 :type env: dict[str, str] | None
63 :param quiet: Suppress the standard console banner when ``True``.
64 :type quiet: bool
65 :param kwargs: Additional keyword arguments forwarded to ``subprocess.run``.
66 :type kwargs: Any
67 :return: Completed process produced by the command execution.
68 :rtype: subprocess.CompletedProcess[Any]
69 """
70 command_str = _to_command_line(command)
71 if not quiet:
72 CONSOLE.print(f'🧩 Running: {command_str}')
74 if dry_run:
75 return subprocess.CompletedProcess(args=command, returncode=0, stdout='', stderr='')
77 kwargs.setdefault('check', True)
78 kwargs.setdefault('text', True)
80 if env is not None:
81 merged_env = os.environ.copy()
82 merged_env.update(env)
83 kwargs['env'] = merged_env
85 if isinstance(command, str):
86 kwargs.setdefault('shell', True)
88 return subprocess.run(command, cwd=cwd, **kwargs)
91def run_stdout(command: str | list[str], *, dry_run: bool = False, quiet: bool = False, **kwargs: Any) -> str:
92 """
93 Execute a command and return stripped standard output.
95 .. versionadded:: 2.2
96 Add the `quiet` parameter to do not display the command being executed
98 :param command: Command to execute.
99 :type command: str | list[str]
100 :param dry_run: If ``True``, do not execute the command.
101 :type dry_run: bool
102 :param quiet: Suppress the standard console banner when ``True``.
103 :type quiet: bool
104 :param kwargs: Additional keyword arguments forwarded to ``run``.
105 :type kwargs: Any
106 :return: Standard output stripped of leading and trailing whitespace.
107 :rtype: str
108 """
109 kwargs['capture_output'] = True
110 result = run(command, dry_run=dry_run, quiet=quiet, **kwargs)
111 stdout = result.stdout
112 if isinstance(stdout, bytes):
113 return stdout.decode().strip()
114 return (stdout or '').strip()