import os import shlex import subprocess import sys from typing import Any, List, Optional, Union from ..compat import is_windows from ..errors import Errors def split_command(command: str) -> List[str]: """Split a string command using shlex. Handles platform compatibility. command (str) : The command to split RETURNS (List[str]): The split command. """ return shlex.split(command, posix=not is_windows) def join_command(command: List[str]) -> str: """Join a command using shlex. shlex.join is only available for Python 3.8+, so we're using a workaround here. command (List[str]): The command to join. RETURNS (str): The joined command """ return " ".join(shlex.quote(cmd) for cmd in command) def run_command( command: Union[str, List[str]], *, stdin: Optional[Any] = None, capture: bool = False, ) -> subprocess.CompletedProcess: """Run a command on the command line as a subprocess. If the subprocess returns a non-zero exit code, a system exit is performed. command (str / List[str]): The command. If provided as a string, the string will be split using shlex.split. stdin (Optional[Any]): stdin to read from or None. capture (bool): Whether to capture the output and errors. If False, the stdout and stderr will not be redirected, and if there's an error, sys.exit will be called with the return code. You should use capture=False when you want to turn over execution to the command, and capture=True when you want to run the command more like a function. RETURNS (Optional[CompletedProcess]): The process object. """ if isinstance(command, str): cmd_list = split_command(command) cmd_str = command else: cmd_list = command cmd_str = " ".join(command) try: ret = subprocess.run( cmd_list, env=os.environ.copy(), input=stdin, encoding="utf8", check=False, stdout=subprocess.PIPE if capture else None, stderr=subprocess.STDOUT if capture else None, ) except FileNotFoundError: # Indicates the *command* wasn't found, it's an error before the command # is run. raise FileNotFoundError( Errors.E501.format(str_command=cmd_str, tool=cmd_list[0]) ) from None if ret.returncode != 0 and capture: message = f"Error running command:\n\n{cmd_str}\n\n" message += f"Subprocess exited with status {ret.returncode}" if ret.stdout is not None: message += "\n\nProcess log (stdout and stderr):\n\n" message += ret.stdout error = subprocess.SubprocessError(message) error.ret = ret # type: ignore[attr-defined] error.command = cmd_str # type: ignore[attr-defined] raise error elif ret.returncode != 0: sys.exit(ret.returncode) return ret