Skip to content
Shell & TerminalDeep Dive Published Updated 3 min readViews unavailable

Here-Documents in Shell: Expansion, Delimiters, Tabs, and Temporary Data

A rigorous guide to shell here-document parsing, quoted delimiters, expansion, tab stripping, indentation, remote commands, and safer data handling.

A here-document attaches lines from the shell source to a command’s standard input or another descriptor. Whether parameter expansion, command substitution, and arithmetic expansion occur in those lines is controlled by quoting the delimiter word—not by quoting individual lines after the fact. That makes the delimiter part of the data’s security boundary.

Quote the delimiter to keep content literal

An unquoted delimiter enables shell expansions in the body:

name='Ada'
cat <<EOF
Hello, $name
Generated: $(date)
EOF

The shell performs the substitutions before cat receives bytes. If the content is a script, JSON template, SQL statement, or remote command containing $, backticks, or backslashes, quote the delimiter:

cat <<'EOF'
The literal text is $HOME and $(not executed).
EOF

Any quoted character in the delimiter word disables body expansion and quote removal yields the delimiter to match. The terminating line must contain only that delimiter, with no trailing spaces. Choose a distinctive token that cannot appear alone in the payload.

An expanded here-document is not word-split or pathname-expanded in the same manner as an unquoted ordinary parameter expansion, but command substitutions still execute and their output becomes data. Do not interpolate untrusted text into executable shell, SQL, or configuration syntax merely because it is inside a here-document.

<<- strips leading tab characters only

The dash form removes leading tab characters from body and delimiter lines, allowing indentation in a tab-indented script:

if ready; then
	cat <<-EOF
	ready=$ready
	EOF
fi

Spaces are not tabs and are not stripped. Editors that convert tabs to spaces can break the terminator or alter generated data. For space-indented projects, place the delimiter at column zero, generate indentation explicitly, or use a separate template file.

Multiple here-documents on one command are collected in shell parsing order, while redirection order still controls which one ultimately supplies a descriptor. This is legal and hard to review; prefer one clear input per command.

Local versus remote expansion must be explicit

A common deployment mistake is:

ssh host <<EOF
echo "$HOME"
EOF

The local shell expands $HOME before ssh sees it. Quoting <<'EOF' sends the expression for the remote shell to expand. Neither is intrinsically correct; the script must state which host owns each value. For arbitrary values, pass them as arguments or environment through a safe protocol rather than injecting them into remote shell source.

Here-documents may be implemented with pipes or temporary files. Do not assume giant payloads remain in memory, and do not use them to conceal large secrets. Command lines, traces, core dumps, child processes, or temporary storage may still expose data. Use a credential or file-descriptor mechanism designed for the target application.

Locale and encoding also remain outside the here-document syntax. The shell reads source bytes, while the receiving program decides whether they are UTF-8, another encoding, or arbitrary data. Keep scripts in a declared encoding, avoid visually confusable delimiter characters, and validate generated structured text with its real parser. A here-document that terminates correctly can still contain invalid JSON, YAML, SQL, or configuration semantics.

Test bytes, not appearance

Use od -An -tx1 or a language parser to verify tabs, trailing newlines, encodings, and backslashes. Run the script under every claimed POSIX shell, because extension syntax near here-documents can affect parsing even when the construct itself is standard. Include literal $, backticks, backslashes, delimiter-like lines, empty payloads, and a missing final newline.

A here-document is predictable when the author chooses literal or expanded mode at the delimiter, controls indentation, and keeps data separate from code. Visual formatting alone is not the contract; the exact bytes reaching the command are.

Related:

Sources:

Comments