* feat: support multiple commands on iOS Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * Move split_command into a util module * (unrelated) fix test docstring * Implement short-circuit behaviour on test-command --------- Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: Joe Rickerby <joerick@mac.com>
99 lines
2.7 KiB
Python
99 lines
2.7 KiB
Python
import os
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import typing
|
|
from collections.abc import Iterator, Mapping
|
|
from typing import Final, Literal
|
|
|
|
from ..errors import FatalError
|
|
from ..typing import PathOrStr
|
|
|
|
_IS_WIN: Final[bool] = sys.platform.startswith("win")
|
|
|
|
|
|
@typing.overload
|
|
def call(
|
|
*args: PathOrStr,
|
|
env: Mapping[str, str] | None = None,
|
|
cwd: PathOrStr | None = None,
|
|
capture_stdout: Literal[False] = ...,
|
|
) -> None: ...
|
|
|
|
|
|
@typing.overload
|
|
def call(
|
|
*args: PathOrStr,
|
|
env: Mapping[str, str] | None = None,
|
|
cwd: PathOrStr | None = None,
|
|
capture_stdout: Literal[True],
|
|
) -> str: ...
|
|
|
|
|
|
def call(
|
|
*args: PathOrStr,
|
|
env: Mapping[str, str] | None = None,
|
|
cwd: PathOrStr | None = None,
|
|
capture_stdout: bool = False,
|
|
) -> str | None:
|
|
"""
|
|
Run subprocess.run, but print the commands first. Takes the commands as
|
|
*args. Uses shell=True on Windows due to a bug. Also converts to
|
|
Paths to strings, due to Windows behavior at least on older Pythons.
|
|
https://bugs.python.org/issue8557
|
|
"""
|
|
args_ = [str(arg) for arg in args]
|
|
# print the command executing for the logs
|
|
print("+ " + " ".join(shlex.quote(a) for a in args_))
|
|
# workaround platform behaviour differences outlined
|
|
# in https://github.com/python/cpython/issues/52803
|
|
path_env = env if env is not None else os.environ
|
|
path = path_env.get("PATH", None)
|
|
executable = shutil.which(args_[0], path=path)
|
|
if executable is None:
|
|
msg = f"Couldn't find {args_[0]!r} in PATH {path!r}"
|
|
raise FatalError(msg)
|
|
args_[0] = executable
|
|
try:
|
|
result = subprocess.run(
|
|
args_,
|
|
check=True,
|
|
shell=_IS_WIN,
|
|
env=env,
|
|
cwd=cwd,
|
|
capture_output=capture_stdout,
|
|
text=capture_stdout,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
if capture_stdout:
|
|
sys.stderr.write(e.stderr)
|
|
raise
|
|
if not capture_stdout:
|
|
return None
|
|
sys.stderr.write(result.stderr)
|
|
return typing.cast(str, result.stdout)
|
|
|
|
|
|
def shell(
|
|
*commands: str, env: Mapping[str, str] | None = None, cwd: PathOrStr | None = None
|
|
) -> None:
|
|
command = " ".join(commands)
|
|
print(f"+ {command}")
|
|
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
|
|
|
|
|
|
def split_command(lst: list[str]) -> Iterator[list[str]]:
|
|
"""
|
|
Split a shell-style command, as returned by shlex.split, into a sequence
|
|
of commands, separated by '&&'.
|
|
"""
|
|
items = list[str]()
|
|
for item in lst:
|
|
if item == "&&":
|
|
yield items
|
|
items = []
|
|
else:
|
|
items.append(item)
|
|
yield items
|