Bash Heredoc: <<EOF, Variables, Indentation and Here Strings

Tested on RHEL 10.2 (Coughlan)
Package bash 5.2.26-6.el10
Applies to Linux distributions with Bash installed
Privilege Normal user; sudo access required only for the protected-file example
Scope Bash heredoc (<<) and here-string (<<<) syntax: delimiter quoting, expansion rules, <<- tab stripping, redirection, variable assignment, SSH local-vs-remote expansion, and common delimiter mistakes.
Related guides Shell scripting course hub
Short on time?Beta Get the gist of this 12-minute read in seconds, then ask follow-up questions

A Bash heredoc feeds multiple lines of text to a command's standard input until Bash sees a closing delimiter line. A here string (<<<) feeds a single expanded string to a command's standard input and appends a trailing newline.

The delimiter word controls whether Bash expands $variables, $(commands), and $(( arithmetic )) in the body before the text reaches the command. That one choice also decides whether local variables expand before an SSH command sends the text to a remote shell.


Bash Heredoc Syntax

The basic form is command <<DELIMITER, body lines, then DELIMITER alone on its own line:

bash
cat <<EOF
line one
line two
EOF
output
line one
line two

DELIMITER is arbitrary text. EOF is convention, not a special keyword — you could use END or CONFIG instead.

The <<DELIMITER redirection belongs to the command that will receive the heredoc as input. With ordinary <<, the closing delimiter must appear by itself with no leading or trailing characters. <<- is the exception that allows leading tabs, as shown later.


What Expands in an Unquoted Heredoc?

When the delimiter word is unquoted (<<EOF), Bash performs parameter expansion, command substitution, and arithmetic expansion on the heredoc body before the command reads it from standard input.

Set a few local values and print them through an unquoted heredoc:

bash
user=golinuxcloud
cat <<EOF
user=$user
home=$HOME
today=$(date +%F)
EOF
output
user=golinuxcloud
home=/home/golinuxcloud
today=2026-09-10

That looks like double-quoted expansion, but a heredoc body is not identical to one double-quoted string on the command line:

  • Pathname expansion (globbing) does not run on heredoc lines — *.log stays literal.
  • Word splitting does not break heredoc lines into separate shell words — each line is passed as a line of input.
  • In an unquoted heredoc, \ can quote \, $, and backticks. A \ immediately followed by a newline is a special case: Bash removes the pair and joins the two physical lines before checking for the closing delimiter.

Show globbing and escaping in the same body:

bash
cat <<EOF
glob stays literal: *.log
escaped dollar: \$HOME
sum=$((2 + 2))
EOF
output
glob stays literal: *.log
escaped dollar: $HOME
sum=4

For the full expansion pipeline outside heredocs, see Bash shell expansions. An unquoted heredoc uses an expansion model similar to double-quoted text: parameter expansion, command substitution, and arithmetic expansion occur, while ordinary word splitting and filename expansion do not run on the heredoc body.


Disable Expansion with <<'EOF'

If any part of the delimiter word is quoted, Bash treats the entire heredoc body literally. No parameter expansion, command substitution, or arithmetic expansion runs.

Compare unquoted and single-quoted delimiters on the same template:

bash
user=golinuxcloud
cat <<EOF
user=$user
home=$HOME
today=$(date +%F)
EOF
output
user=golinuxcloud
home=/home/golinuxcloud
today=2026-09-10

The same template with a quoted delimiter keeps every $ and $(...) intact:

bash
cat <<'EOF'
user=$user
home=$HOME
today=$(date +%F)
EOF
output
user=$user
home=$HOME
today=$(date +%F)

These forms also disable expansion because the delimiter word is quoted:

  • <<'EOF' — single quotes on the delimiter
  • <<"EOF" — double quotes on the delimiter
  • <<\EOF — backslash before the delimiter word

Use a quoted delimiter for remote scripts, SQL templates, JSON skeletons, and any body that must contain literal $ signs.


Indent a Heredoc with <<-EOF

<<-DELIMITER strips all leading tab characters from each heredoc line and from the closing delimiter line. It does not strip spaces.

In the following source, the tab-prefixed line and closing EOF begin with actual tab characters, while the first line begins with spaces. Make sure your editor inserts real tab characters here rather than converting tabs to spaces.

bash
write_config() {
	cat <<-EOF
 	 space-prefixed line stays
	 tab-prefixed line strips
	EOF
}
write_config
output
 	 space-prefixed line stays
 tab-prefixed line strips

Only tabs are removed from the start of each line — not spaces. Scripts that use spaces for indentation still send those spaces to the command.

The closing delimiter line may also be tab-indented when you use <<-. Match the delimiter name exactly; do not mix <<-EOF with a closing line that has trailing spaces after EOF.


Redirect Heredoc Content to a File

To create a file from heredoc text, feed the heredoc to cat and redirect cat's stdout to the file:

bash
user=golinuxcloud
cat <<EOF > app.conf
app_user=$user
app_home=$HOME
EOF

The file receives expanded text after Bash processes the heredoc:

bash
cat app.conf
output
app_user=golinuxcloud
app_home=/home/golinuxcloud

To append heredoc content instead of replacing the file, use >>:

bash
cat <<'EOF' >> app.conf
log_level=info
EOF
cat app.conf
output
app_user=golinuxcloud
app_home=/home/golinuxcloud
log_level=info

> truncates and rewrites the file, while >> preserves the existing content and appends the heredoc text.

Use a quoted delimiter when the file must preserve literal $ signs for another tool to interpret later. For input and output redirection patterns beyond heredocs, the redirection article covers file descriptors and pipes.

Write to a root-owned file with sudo tee

Writing directly with sudo cat <<EOF > /etc/myapp.conf often fails with Permission denied because your current shell performs > without elevated privileges — only cat runs under sudo.

Feed the heredoc to sudo tee instead:

bash
sudo tee /etc/myapp.conf >/dev/null <<'EOF'
option=value
path=$HOME
EOF

sudo applies to tee, so tee opens the protected file with elevated privileges. Putting sudo only before another command does not elevate a > redirection performed by your current shell. Use sudo tee -a when you need to append to a root-owned file instead of overwriting it.

When $HOME must expand locally before the file is written, use an unquoted delimiter. When the file must keep literal $ signs, quote the delimiter as shown above.


Feed a Heredoc to a Command

Any command that reads standard input can consume a heredoc, including tools such as wc, database clients, and ssh. cat is useful for demos because it prints the body unchanged.

Count lines fed from a heredoc:

bash
wc -l <<EOF
alpha
beta
gamma
EOF
output
3

Store a Heredoc in a Bash Variable

Assign a heredoc body with command substitution:

bash
message=$(
cat <<'EOF'
line one
line two
$HOME stays literal
EOF
)
printf '%s\n' "$message"
output
line one
line two
$HOME stays literal

Command substitution removes trailing newlines from the captured output. Use this form when preserving the internal multiline structure matters but the final newline does not.

Use an unquoted delimiter inside the substitution when you want local expansion before the text is stored in the variable.


Here Strings with <<<

A here string (<<<) applies tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal to its right-hand word, but does not perform word splitting or filename expansion. Bash then supplies the result as a single string to standard input with a trailing newline.

bash
grep -o 'lab' <<< "search in lab text"
output
lab

<<< is not a drop-in replacement for echo | command. The right-hand text is expanded under those rules, then passed as stdin — including the appended newline.

Measure the byte length Bash actually feeds to stdin:

bash
printf 'bytes=%s\n' "$(wc -c <<< 'hello')"
od -An -tx1 <<< 'hello'
output
bytes=6
 68 65 6c 6c 6f 0a

Six bytes means hello plus the newline Bash appends (0a). A pipe from Bash echo vs printf can produce different spacing or omit the newline depending on how you build the pipeline.

Here strings suit short filters (grep, read, bc) when a single expanded string is enough. Multi-line input still belongs in a heredoc.


Heredoc vs Here String vs Pipe

Mechanism Input shape Typical use
<<DELIMITER Multiple lines until closing delimiter Config files, SQL scripts, remote shell input
<<< word One expanded string plus trailing newline Short tests, one-line patterns
| pipe stdout from upstream command Streaming output, long command chains

Choose a heredoc when readability matters and the text spans several lines. Choose a here string for a single expanded string. Choose a pipe when the input is the output of another command rather than literal text in the script.


Heredoc over SSH: Local vs Remote Expansion

The local shell processes the heredoc first and supplies the resulting text to ssh through standard input. Whether $variables and $(commands) expand locally depends on the delimiter quoting.

Set local variables and simulate what the remote shell would receive:

bash
remote_user=deploy
cat <<EOF | sed 's/^/remote sees: /'
user=$remote_user
path=$HOME/bin
EOF
output
remote sees: user=deploy
remote sees: path=/home/golinuxcloud/bin

With an unquoted delimiter, the local shell expands $remote_user and $HOME before ssh receives the heredoc as standard input. The remote shell therefore sees the resulting values rather than those variable references.

Use a quoted delimiter when the remote shell should expand its own variables:

bash
cat <<'EOF' | sed 's/^/remote sees: /'
user=$remote_user
path=$HOME/bin
EOF
output
remote sees: user=$remote_user
remote sees: path=$HOME/bin

On a real ssh host 'bash -s' <<'EOF' call, the quoted form sends literal $HOME so the remote Bash expands $HOME on that system. The unquoted form expands $HOME locally and sends your local home path instead.

Rule of thumb:

  • <<EOF — expand locally before SSH sends the text.
  • <<'EOF' — send literal $ text so the remote shell can expand it.

The quoted delimiter only guarantees the local shell does not perform those expansions. Whether variables expand on the other side depends on whether the received text is interpreted by a remote shell.


Practical Heredoc Examples

Generate a config with local values

Build an nginx-style snippet with an unquoted heredoc and Bash variables:

bash
user=golinuxcloud
cat <<EOF > nginx-snippet.conf
server_name $user.example;
root $HOME/www;
EOF
cat nginx-snippet.conf
output
server_name golinuxcloud.example;
root /home/golinuxcloud/www;

Create a literal template

When the file must keep $HOME and $(date) for documentation or templating tools, quote the delimiter:

bash
cat <<'EOF' > template.txt
install_dir=$HOME/app
built_on=$(date +%F)
EOF
cat template.txt
output
install_dir=$HOME/app
built_on=$(date +%F)

Feed SQL to a client

Database clients read SQL from stdin. Use a quoted delimiter when the SQL contains $ signs that must not expand locally. The example below uses sqlite3 3.46.1 on the lab host:

bash
sqlite3 :memory: <<'SQL'
SELECT 'literal $user stays' AS note;
SQL
output
literal $user stays

Use an unquoted delimiter only when you intentionally inject shell values — and only when you trust the data:

bash
user=golinuxcloud
sqlite3 :memory: <<SQL
SELECT '$user' AS expanded;
SQL
output
golinuxcloud

Prefer parameterized queries in real scripts. A heredoc is fine for fixed admin SQL; dynamic values belong in client-specific bind parameters, not string concatenation.

JSON and escaping

Heredocs make it easy to produce invalid JSON when values contain quotes:

bash
name='O"Brien'
cat <<EOF
{"user":"$name"}
EOF
output
{"user":"O"Brien"}

That output breaks JSON parsing. When escaping matters, build JSON with jq or another serializer instead of an unquoted heredoc.


Common Heredoc Errors

Mistake What goes wrong
Leading space before closing EOF Delimiter never matches; malformed line becomes body content
Quoting only the closing delimiter Only the opening <<DELIMITER line controls expansion
Expecting <<- to strip spaces Only leading tabs are removed, not spaces
sudo cat <<EOF > /etc/file Current shell opens > without sudo; use sudo tee instead
Unquoted heredoc over SSH Local shell expands $vars before the remote shell runs
Building JSON in unquoted heredocs Quotes and backslashes in variable values break the format

Reproduce the leading-space delimiter failure:

bash
bash -c 'cat <<EOF
line
 EOF
' 2>&1
output
bash: line 3: warning: here-document at line 1 delimited by end-of-file (wanted `EOF')
line
 EOF

Because the leading space prevents EOF from matching the delimiter, Bash continues reading until end-of-file. The malformed EOF line is treated as heredoc content rather than as the closing delimiter.


Summary

Bash heredocs feed multi-line text to stdin until a closing delimiter line matches. An unquoted delimiter lets parameter expansion, command substitution, and arithmetic expansion run in the body — similar to double-quoted text, but without word splitting or filename expansion on the body. Quoting any part of the delimiter word disables all expansion.

Use <<- when tab-indented function bodies should align in source code but arrive at the command without leading tabs. Assign a heredoc to a variable with command substitution when you need the text in memory, remembering that trailing newlines are stripped from the capture. Here strings (<<<) apply tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal without word splitting or filename expansion, then supply the result to standard input with a trailing newline.

Over SSH, delimiter quoting decides whether $variables expand locally or are left for a remote shell to interpret. For config files, SQL, and other generated text, choose delimiter quoting based on whether Bash should expand $ expressions or preserve them for another tool. When generating JSON from variable data, use a serializer such as jq rather than relying on heredoc escaping. The : <<'COMMENT' no-op pattern sometimes used to skip blocks is still heredoc redirection—not a real multiline comment; prefer # lines per Bash comments unless you understand the expansion rules above.


References


Frequently Asked Questions

How do I stop variables expanding in a heredoc?

Quote any part of the delimiter word on the opening line: <<'EOF', <<"EOF", or <<\EOF. Bash then treats the entire body literally and skips parameter expansion, command substitution, and arithmetic expansion.

How do I assign a heredoc to a variable in Bash?

Wrap the heredoc in command substitution: var=$(cat <<'EOF' ... EOF). Use a quoted delimiter when the body must stay literal. Command substitution strips trailing newlines from the captured text.

How do I use a heredoc with sudo?

Feed the heredoc to sudo tee /path/to/file, rather than using sudo cat <<EOF > /path. The shell performs > with your user privileges; sudo tee opens the destination file with elevated privileges.

Can the closing EOF line be indented?

Only when you use <<-DELIMITER on the opening line. Bash strips all leading tabs from each body line and allows a tab-indented closing delimiter. Spaces before EOF on the closing line still break an ordinary <<EOF heredoc.

What is the difference between << and <<<?

<<DELIMITER reads multiple lines until a matching delimiter line. <<< word applies tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal without word splitting or filename expansion, then feeds the result to stdin with a trailing newline. Use heredocs for blocks of text; use here strings for a single expanded string.

Where do variables expand in an SSH heredoc?

In the local shell first when the delimiter is unquoted (<<EOF). With a quoted delimiter (<<'EOF'), the local shell sends literal $ text; if that text is executed by a remote shell, variables such as $HOME are expanded there.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)