# exec.run

The `exec.run` function runs an external program with an argument list, without a shell, and returns its
standard output, standard error, and exit code. By default the output streams live and a nonzero exit code raises
an error.

## Usage

```python
exec.run(argv, working_directory = ..., env = ..., output = "stream", check = True, timeout = "", retry = None)
```

## Arguments

- **`argv`**

  Required. A non-empty list or tuple of strings. The first element is the program and the rest are its
  arguments, passed exactly as written. A single string such as `"ls -l"` is rejected: split it into a list.
- **`working_directory`**

  (Optional) The directory to run in. Defaults to the script's working directory: the step's
  `working_directory`, or the process directory when none is set. A relative path resolves against that
  directory.
- **`env`**

  (Optional) A dictionary of extra environment variables with string keys and string values. These layer over
  the inherited execution environment, and a value given here replaces an inherited variable of the same name.
- **`output`**

  (Optional) `"stream"` (the default) shows the program's output live and also captures it. The value `"capture"`
  captures stdout and stderr without showing them, which suits secret-bearing responses. Any other value is an
  error.
- **`check`**

  (Optional) Defaults to `True`: a nonzero exit code raises an error. Pass `False` to receive the result
  and inspect `exit_code` yourself.

The `timeout` and `retry` options apply to this individual process call:

- **`timeout`**
  A positive duration such as 
  `"30s"`
   or 
  `"5m"`
  . It bounds the entire call,
  including all attempts and retry delays. Omit it for no additional deadline;
  an enclosing task or script can still cancel the call.
- **`retry`**
  A dictionary using the same policy fields as

  [`steps.task`](/functions/automation/steps.task#retry-policy)
  , including

  `max_attempts`
  , 
  `initial_delay`
  , and 
  `backoff_strategy`
  . Optional 
  `conditions`

  are regular expressions matched against the failed attempt's stdout and stderr.
  At least one must match when supplied. Omit the policy to run once.

Only ordinary nonzero exits are retried. Cancellation, timeout, failure to start,
signals, and I/O errors stop the call. With `check=False`, an ordinary nonzero exit
is returned immediately and does not trigger retries. Captured output contains
only the final attempt; streaming displays each attempt as it runs. Retrying
repeats the command's effects, so use it only for operations safe to repeat.
Set `max_attempts` or `timeout` to bound a retry policy.

## Returns

A result with captured output and a decoded JSON view:

- **`data`**
  The JSON value decoded from 
  `stdout`
   on first access. Objects become dictionaries, arrays become lists,
  and JSON scalars retain their types. Decoded collections are read-only and safe to share with parallel tasks.
  Empty or invalid JSON raises an error only when you access this attribute.
- **`stdout`**
  Everything the program wrote to standard output, as a string.
- **`stderr`**
  Everything the program wrote to standard error, as a string.
- **`exit_code`**
  The exit code, as an integer.

With `output="stream"`, the program's stdout also goes to the script's own stdout, so it becomes the step's value when
the script does not assign [`output`](/functions/automation/output). Use `output="capture"` to keep it out of the
step value. [`component.exec`](/functions/automation/component.exec) accepts the same options.
The [`atmos.run`](/functions/automation/atmos.run) function and other `atmos.*` wrappers
share the result fields and capture/check options; use a
[`steps.task`](/functions/automation/steps.task) to apply a timeout or retry policy to those wrappers.

Reading `data` does not change `stdout`. Use `json.decode(result.stdout)` when you need a mutable copy.
Serializing the whole result with `json.encode(result)` preserves the raw fields and does not access `data`.
With `check=False`, inspect `exit_code` before reading `data` if the command may return a non-JSON error.

## Errors

- An empty list, a non-list `argv`, or a non-string element fails with an argument error.
- With `check=True`, a nonzero exit code raises an error that names the command and the exit code, and shows the last
  lines of stderr.
- A program that cannot start, such as a missing command or a nonexistent directory, always raises, whatever `check`
  says. A canceled run and a program stopped by a signal raise as well.
- A handled nonzero exit does not trigger a [`steps.task`](/functions/automation/steps.task) retry. Call `fail()`
  when the result should fail the task.

Tools pinned with [`dependencies.tools`](/functions/automation/dependencies.tools) are on the `PATH` of every call
that follows.

## Examples

### Bound a command and retry transient failures

```python
result = exec.run(
    ["curl", "--fail", "https://example.com/"],
    output = "capture",
    timeout = "30s",
    retry = {"max_attempts": 3, "initial_delay": "1s"},
)
print(result.stdout)
```

### Run a program and use its output

```python
result = exec.run(["git", "rev-parse", "--short", "HEAD"], output = "capture")
sha = result.stdout.strip()
output = {"sha": sha}
```

### Set the directory and environment

```python
exec.run(
    ["./deploy.sh", "api"],
    working_directory = "scripts",
    env = {"AWS_REGION": "us-east-1", "DEPLOY_ENV": "dev"},
)
```

### Assert an expected failure

```python
result = exec.run(["terraform", "validate"], check = False, output = "capture")
if result.exit_code == 0:
    fail("expected validation to fail")
if "Missing required argument" not in result.stderr:
    fail("validation failed for an unexpected reason:\n" + result.stderr)
```

### Run several programs in parallel

```python
def lint(directory):
    return exec.run(["tflint"], working_directory = directory, output = "capture").stdout

output = steps.parallel(
    tasks = [steps.task(name = d, function = lint, args = [d]) for d in ["vpc", "eks", "rds"]],
    max_concurrency = 2,
)
```

Inside a parallel group with more than one task, each streamed line is prefixed with the task name.

## Related

- [`component.exec`](/functions/automation/component.exec) runs a program in a component's directory.
- [`atmos.run`](/functions/automation/atmos.run) runs Atmos itself.
- [`dependencies.tools`](/functions/automation/dependencies.tools) installs tools before you run them.
- [Atmos Automation Language](/automation/language) and the [script step](/steps/type/script)
