84 lines
2.9 KiB
Python
84 lines
2.9 KiB
Python
|
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
|