### Goals see also https://github.com/easybuilders/easybuild-framework/issues/4252 - one single `run` function that supports running (interactive) shell commands (incl. Lmod) - sane API + defaults - supports everything that current `run_cmd` and `run_cmd_qa` functions do - enables new features like: - context-aware error parsing & reporting - wrapper functions for common commands like `run_make`, `run_cmake`, etc. - opt-in to jumping into an interactive shell session if command failed ### Current `run_cmd` and `run_cmd_qa` functions ```python def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True, log_output=False, path=None, force_in_dry_run=False, verbose=True, shell=None, trace=True, stream_output=None, asynchronous=False): """ Run specified command (in a subshell) :param cmd: command to run :param log_ok: only run output/exit code for failing commands (exit code non-zero) :param log_all: always log command output and exit code :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code) :param inp: the input given to the command via stdin :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error) :param log_output: indicate whether all output of command should be logged to a separate temporary logfile :param path: path to execute the command in; current working directory is used if unspecified :param force_in_dry_run: force running the command during dry run :param verbose: include message on running the command in dry run output :param shell: allow commands to not run in a shell (especially useful for cmd lists), defaults to True :param trace: print command being executed as part of trace output :param stream_output: enable streaming command output to stdout :param asynchronous: run command asynchronously (returns subprocess.Popen instance if set to True) """ ``` ```python def run_cmd_qa(cmd, qa, no_qa=None, log_ok=True, log_all=False, simple=False, regexp=True, std_qa=None, path=None, maxhits=50, trace=True): """ Run specified interactive command (in a subshell) :param cmd: command to run :param qa: dictionary which maps question to answers :param no_qa: list of patters that are not questions :param log_ok: only run output/exit code for failing commands (exit code non-zero) :param log_all: always log command output and exit code :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code) :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error) :param std_qa: dictionary which maps question regex patterns to answers :param path: path to execute the command is; current working directory is used if unspecified :param maxhits: maximum number of cycles (seconds) without being able to find a known question :param trace: print command being executed as part of trace output """ ``` ### Designing `run` function Default usage: ```python run(cmd) ``` * exits with error on non-zero exit code * always returns output (stdout+stderr combined) + exit code * always return named tuple as output value (less room for typos, immutable) ```python (output="all output", exit_code=0, stderr=None) ``` * always log output + exit code (only with `log.info`) Example: ```python res = run(cmd) res.output res.exit_code ``` Options: * don't fail on non-zero exit code (`fail_on_error=True` by default) * split stdout/stderr (`split_stderr=False` by default) * return value: ```python (output="stdout output", exit_code=0, stderr="errors + warnings") ``` * don't show command in terminal (for trace or dry run output) (`hidden=False` by default) * `hidden=True` is combo of `trace=False` + `verbose=False` in `run_cmd` * input to be sent to stdin (`stdin=None` by default) * like `inp` in `run_cmd` * also run command in dry run mode (`in_dry_run=False` by default) * like `force_in_dry_run` in `run_cmd` * location to run command in (`work_dir=None` by default, implies current working directory) * like `path` in `run_cmd` * do not run in shell (`shell=True` by default) * like `shell` in `run_cmd` (matches with `shell` in `subprocess.Popen`) * enable logging of output to temporary file (`output_file=False` by default) * like `log_output` in `run_cmd` * rename tmp file to `.out` instead of `.log` * no timestamps, so not really a log * enabling streaming of output to stdout (`stream_output=False` by default) * like `stream_output` in `run_cmd` * run command asynchronously (`asynchronous=False` by default) * like `asynchronous` in `run_cmd` * (can't use `async=False` since `async` is a Python keyword) For interactive commands: * `qa_patterns` to provide question/answer mapping, questions can be regular expressions (`qa=None` by default) * combo of `qa` and `std_qa` in `run_cmd_qa` * `qa` should be a list of tuples `("question", "answer")`, since we want to control order * question could be a string value (exact match (end-of-line?), after re.escape) or a compiled regex (regex match) * `qa_wait_patterns` to provide list of non-question patterns and how long to allow them * example: `[("not a question", 50)]` * combo of `no_qa` and `maxhits` in `run_cmd_qa` Not replacing: * cryptic `log_ok` and `log_all` options * `simple` option to only return `True`/`False` * `regexp` to specify custom error patterns (only used for logging currently) Function signature: ```python def run(cmd, fail_on_error=True, split_stderr=False, stdin=None, hidden=False, in_dry_run=False, work_dir=None, shell=True, output_file=False, stream_output=False, asynchronous=False, qa_patterns=None, qa_wait_patterns=None) ```