### 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)
```