Spaces:
Runtime error
Runtime error
"""Wrapper around subprocess to run commands.""" | |
from __future__ import annotations | |
import platform | |
import re | |
import subprocess | |
from typing import TYPE_CHECKING, List, Union | |
from uuid import uuid4 | |
if TYPE_CHECKING: | |
import pexpect | |
class BashProcess: | |
"""Wrapper for starting subprocesses. | |
Uses the python built-in subprocesses.run() | |
Persistent processes are **not** available | |
on Windows systems, as pexpect makes use of | |
Unix pseudoterminals (ptys). MacOS and Linux | |
are okay. | |
Example: | |
.. code-block:: python | |
from langchain_community.utilities.bash import BashProcess | |
bash = BashProcess( | |
strip_newlines = False, | |
return_err_output = False, | |
persistent = False | |
) | |
bash.run('echo \'hello world\'') | |
""" | |
strip_newlines: bool = False | |
"""Whether or not to run .strip() on the output""" | |
return_err_output: bool = False | |
"""Whether or not to return the output of a failed | |
command, or just the error message and stacktrace""" | |
persistent: bool = False | |
"""Whether or not to spawn a persistent session | |
NOTE: Unavailable for Windows environments""" | |
def __init__( | |
self, | |
strip_newlines: bool = False, | |
return_err_output: bool = False, | |
persistent: bool = False, | |
): | |
""" | |
Initializes with default settings | |
""" | |
self.strip_newlines = strip_newlines | |
self.return_err_output = return_err_output | |
self.prompt = "" | |
self.process = None | |
if persistent: | |
self.prompt = str(uuid4()) | |
self.process = self._initialize_persistent_process(self, self.prompt) | |
def _lazy_import_pexpect() -> pexpect: | |
"""Import pexpect only when needed.""" | |
if platform.system() == "Windows": | |
raise ValueError( | |
"Persistent bash processes are not yet supported on Windows." | |
) | |
try: | |
import pexpect | |
except ImportError: | |
raise ImportError( | |
"pexpect required for persistent bash processes." | |
" To install, run `pip install pexpect`." | |
) | |
return pexpect | |
def _initialize_persistent_process(self: BashProcess, prompt: str) -> pexpect.spawn: | |
# Start bash in a clean environment | |
# Doesn't work on windows | |
""" | |
Initializes a persistent bash setting in a | |
clean environment. | |
NOTE: Unavailable on Windows | |
Args: | |
Prompt(str): the bash command to execute | |
""" | |
pexpect = self._lazy_import_pexpect() | |
process = pexpect.spawn( | |
"env", ["-i", "bash", "--norc", "--noprofile"], encoding="utf-8" | |
) | |
# Set the custom prompt | |
process.sendline("PS1=" + prompt) | |
process.expect_exact(prompt, timeout=10) | |
return process | |
def run(self, commands: Union[str, List[str]]) -> str: | |
""" | |
Run commands in either an existing persistent | |
subprocess or on in a new subprocess environment. | |
Args: | |
commands(List[str]): a list of commands to | |
execute in the session | |
""" | |
if isinstance(commands, str): | |
commands = [commands] | |
commands = ";".join(commands) | |
if self.process is not None: | |
return self._run_persistent( | |
commands, | |
) | |
else: | |
return self._run(commands) | |
def _run(self, command: str) -> str: | |
""" | |
Runs a command in a subprocess and returns | |
the output. | |
Args: | |
command: The command to run | |
""" | |
try: | |
output = subprocess.run( | |
command, | |
shell=True, | |
check=True, | |
stdout=subprocess.PIPE, | |
stderr=subprocess.STDOUT, | |
).stdout.decode() | |
except subprocess.CalledProcessError as error: | |
if self.return_err_output: | |
return error.stdout.decode() | |
return str(error) | |
if self.strip_newlines: | |
output = output.strip() | |
return output | |
def process_output(self, output: str, command: str) -> str: | |
""" | |
Uses regex to remove the command from the output | |
Args: | |
output: a process' output string | |
command: the executed command | |
""" | |
pattern = re.escape(command) + r"\s*\n" | |
output = re.sub(pattern, "", output, count=1) | |
return output.strip() | |
def _run_persistent(self, command: str) -> str: | |
""" | |
Runs commands in a persistent environment | |
and returns the output. | |
Args: | |
command: the command to execute | |
""" | |
pexpect = self._lazy_import_pexpect() | |
if self.process is None: | |
raise ValueError("Process not initialized") | |
self.process.sendline(command) | |
# Clear the output with an empty string | |
self.process.expect(self.prompt, timeout=10) | |
self.process.sendline("") | |
try: | |
self.process.expect([self.prompt, pexpect.EOF], timeout=10) | |
except pexpect.TIMEOUT: | |
return f"Timeout error while executing command {command}" | |
if self.process.after == pexpect.EOF: | |
return f"Exited with error status: {self.process.exitstatus}" | |
output = self.process.before | |
output = self.process_output(output, command) | |
if self.strip_newlines: | |
return output.strip() | |
return output | |