> ## Documentation Index
> Fetch the complete documentation index at: https://tally.wharflab.com/llms.txt
> Use this file to discover all available pages before exploring further.

# tally/prefer-formatted-heredocs

> Pretty-print structured COPY/ADD heredocs, shell heredocs, and PowerShell heredocs.

Pretty-prints structured `COPY` and `ADD` heredoc bodies, `COPY` heredocs that contain shell scripts, `COPY` or `ADD` heredocs that contain
PowerShell scripts, and executable `RUN` heredoc bodies.

| Property | Value      |
| -------- | ---------- |
| Severity | Style      |
| Category | Style      |
| Default  | Enabled    |
| Auto-fix | Yes (safe) |

## Description

This rule formats heredoc payloads that are copied into JSON, YAML, TOML, XML, or INI files. It detects structured file types from the heredoc
destination path extension and uses the repository's `.editorconfig` settings for indentation. YAML heredocs can also use `max_line_length` as a
preferred scalar wrapping width.

The rule also formats `COPY` heredocs as shell scripts when the first payload line is a recognized shell shebang, or when the destination path ends
in `.sh` and the payload has no shebang. Plain `.sh` payloads are parsed as POSIX shell. Unsupported shebangs are skipped instead of falling back to
the `.sh` extension.

The rule also formats `RUN <<EOF` heredocs whose body is executed as a shell script. It uses `mvdan.cc/sh/v3`, the same formatter library behind
`shfmt`, for Bash, POSIX shell, mksh, Bats, and zsh. Heredocs that are stdin payloads for another command, such as `RUN cat <<EOF`, are left alone.
cmd and unknown shebang bodies are skipped.

PowerShell heredocs are formatted with PSScriptAnalyzer's `Invoke-Formatter`. This applies to `COPY` or `ADD` heredocs targeting `.ps1`, `.psm1`, or
`.psd1` files, and to `RUN <<EOF` bodies when the active Dockerfile `SHELL` is PowerShell or the heredoc has a PowerShell shebang. tally starts the
PowerShell sidecar lazily, only when slow checks are enabled and a PowerShell heredoc actually needs formatting.

For structured payload EditorConfig lookup, tally resolves a virtual filename next to the Dockerfile using the destination basename. For example, a
heredoc in `services/api/Dockerfile` targeting `/etc/app/config.yaml` is matched as `services/api/config.yaml`, so selectors such as `*.yaml` and
`config.yaml` apply naturally.

`COPY` shell heredocs use the same destination-basename lookup, so a target such as `/usr/local/bin/entrypoint.sh` is matched as
`entrypoint.sh` and can use EditorConfig sections such as `*.sh` or `entrypoint.sh`.

For `RUN` shell heredocs, tally resolves a virtual filename next to the Dockerfile named `Dockerfile.heredoc.<dialect>`, such as
`Dockerfile.heredoc.sh`, `Dockerfile.heredoc.bash`, or `Dockerfile.heredoc.zsh`.

PowerShell heredocs use PSScriptAnalyzer's code formatter with command-casing rewrites disabled, so formatting is stable across Linux, macOS, and
Windows hosts. They are skipped when slow checks are disabled and do not use EditorConfig today.

The formatter also runs as a final auto-fix pass. This means heredocs emitted by other rules, such as `tally/prefer-copy-heredoc`, are formatted in
the same `--fix` run when this rule is enabled.

## Examples

### Bad: COPY

```dockerfile theme={null}
FROM alpine:3.20
COPY <<EOF /etc/app/config.json
{"b":2,"a":1}
EOF
```

### Good: COPY

```dockerfile theme={null}
FROM alpine:3.20
COPY <<EOF /etc/app/config.json
{
  "b": 2,
  "a": 1
}
EOF
```

### Bad: RUN

```dockerfile theme={null}
FROM alpine:3.20
RUN <<EOF
set -eu; apk add --no-cache ca-certificates curl openssl; install -d -o app -g app /opt/app/bin /opt/app/cache; chown -R app:app /opt/app
EOF
```

### Good: RUN

```dockerfile theme={null}
FROM alpine:3.20
RUN <<EOF
set -eu
apk add --no-cache ca-certificates curl openssl
install -d -o app -g app /opt/app/bin /opt/app/cache
chown -R app:app /opt/app
EOF
```

### Bad: COPY shell

```dockerfile theme={null}
FROM alpine:3.20
COPY <<EOF /usr/local/bin/entrypoint
#!/usr/bin/env bash
set -eu; mkdir -p /run/app /var/cache/app; chown -R app:app /run/app /var/cache/app
EOF
```

### Good: COPY shell

```dockerfile theme={null}
FROM alpine:3.20
COPY <<EOF /usr/local/bin/entrypoint
#!/usr/bin/env bash
set -eu
mkdir -p /run/app /var/cache/app
chown -R app:app /run/app /var/cache/app
EOF
```

### Bad: RUN PowerShell

```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:lts-alpine-3.20
SHELL ["pwsh", "-Command"]
RUN <<EOF
if ($true) {
Write-Host hi
}
EOF
```

### Good: RUN PowerShell

```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:lts-alpine-3.20
SHELL ["pwsh", "-Command"]
RUN <<EOF
if ($true) {
    Write-Host hi
}
EOF
```

## Supported Types

| Extension                                           | Format |
| --------------------------------------------------- | ------ |
| `.json`                                             | JSON   |
| `.yaml`, `.yml`                                     | YAML   |
| `.toml`                                             | TOML   |
| `.xml`, `.config`, `.xsd`, `.wsdl`, `.xsl`, `.xslt` | XML    |
| `.ini`                                              | INI    |

## Supported PowerShell Targets

| Extension | Format                                  |
| --------- | --------------------------------------- |
| `.ps1`    | PowerShell script                       |
| `.psm1`   | PowerShell script module                |
| `.psd1`   | PowerShell data file or module manifest |

## Supported Shells

Shell formatting applies to executable `RUN` heredocs and to `COPY` heredocs with a recognized shell shebang. A `COPY` heredoc targeting `.sh`
without a shebang uses POSIX shell. PowerShell `RUN` heredocs use PSScriptAnalyzer instead of a shfmt dialect.

| Shell               | Formatter dialect |
| ------------------- | ----------------- |
| `bash`              | Bash              |
| `sh`, `dash`, `ash` | POSIX shell       |
| `mksh`, `ksh`       | mksh              |
| `bats`              | Bats              |
| `zsh`               | zsh               |

## Configuration

Default (no config needed):

```toml theme={null}
# Enabled by default with no rule-specific options
```

This rule intentionally has no rule-specific formatter options. Use `.editorconfig` for indentation style and width, YAML scalar wrapping width, and
POSIX-style shell heredoc line width.
See [EditorConfig integration](/integrations/editorconfig) for recommended Dockerfile settings and path resolution details.

The rule reads `indent_style`, `indent_size`, `tab_width`, and `max_line_length`. For YAML, `max_line_length` is used as a preferred scalar wrapping
width. For shell heredocs, it is used as a preferred command wrapping width; when unset or invalid, tally uses `100`, and `off` disables this shell
line-width pass. This is not a hard maximum.

For POSIX-style shell heredocs, tally also reads shfmt-compatible EditorConfig booleans: `binary_next_line`, `switch_case_indent`, `space_redirects`,
`keep_padding`, `function_next_line`, `simplify`, and `minify`.

The rule does not currently interpret `insert_final_newline`, `end_of_line`, `charset`, or `trim_trailing_whitespace` for the virtual payload file.
Formatted heredoc bodies always end with exactly one newline before the heredoc terminator, and line endings follow the parent Dockerfile/fix
application rather than a separate payload setting. The current formatters do not emit incidental trailing whitespace, but tally does not run a
separate trailing-whitespace trim pass because whitespace can be payload data.

## Auto-fix

Run:

```bash theme={null}
tally lint --fix Dockerfile
```

To format heredocs produced by another selected rule, select both rules:

```bash theme={null}
tally lint --fix --fix-unsafe \
  --select tally/prefer-copy-heredoc \
  --select tally/prefer-formatted-heredocs \
  Dockerfile
```
