| 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 |
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:
cat <<EOF
line one
line two
EOFline one
line twoDELIMITER 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:
user=golinuxcloud
cat <<EOF
user=$user
home=$HOME
today=$(date +%F)
EOFuser=golinuxcloud
home=/home/golinuxcloud
today=2026-09-10That 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 —
*.logstays 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:
cat <<EOF
glob stays literal: *.log
escaped dollar: \$HOME
sum=$((2 + 2))
EOFglob stays literal: *.log
escaped dollar: $HOME
sum=4For 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:
user=golinuxcloud
cat <<EOF
user=$user
home=$HOME
today=$(date +%F)
EOFuser=golinuxcloud
home=/home/golinuxcloud
today=2026-09-10The same template with a quoted delimiter keeps every $ and $(...) intact:
cat <<'EOF'
user=$user
home=$HOME
today=$(date +%F)
EOFuser=$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.
write_config() {
cat <<-EOF
space-prefixed line stays
tab-prefixed line strips
EOF
}
write_config space-prefixed line stays
tab-prefixed line stripsOnly 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:
user=golinuxcloud
cat <<EOF > app.conf
app_user=$user
app_home=$HOME
EOFThe file receives expanded text after Bash processes the heredoc:
cat app.confapp_user=golinuxcloud
app_home=/home/golinuxcloudTo append heredoc content instead of replacing the file, use >>:
cat <<'EOF' >> app.conf
log_level=info
EOF
cat app.confapp_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:
sudo tee /etc/myapp.conf >/dev/null <<'EOF'
option=value
path=$HOME
EOFsudo 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:
wc -l <<EOF
alpha
beta
gamma
EOF3Store a Heredoc in a Bash Variable
Assign a heredoc body with command substitution:
message=$(
cat <<'EOF'
line one
line two
$HOME stays literal
EOF
)
printf '%s\n' "$message"line one
line two
$HOME stays literalCommand 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.
grep -o 'lab' <<< "search in lab text"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:
printf 'bytes=%s\n' "$(wc -c <<< 'hello')"
od -An -tx1 <<< 'hello'bytes=6
68 65 6c 6c 6f 0aSix 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:
remote_user=deploy
cat <<EOF | sed 's/^/remote sees: /'
user=$remote_user
path=$HOME/bin
EOFremote sees: user=deploy
remote sees: path=/home/golinuxcloud/binWith 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:
cat <<'EOF' | sed 's/^/remote sees: /'
user=$remote_user
path=$HOME/bin
EOFremote sees: user=$remote_user
remote sees: path=$HOME/binOn 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:
user=golinuxcloud
cat <<EOF > nginx-snippet.conf
server_name $user.example;
root $HOME/www;
EOF
cat nginx-snippet.confserver_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:
cat <<'EOF' > template.txt
install_dir=$HOME/app
built_on=$(date +%F)
EOF
cat template.txtinstall_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:
sqlite3 :memory: <<'SQL'
SELECT 'literal $user stays' AS note;
SQLliteral $user staysUse an unquoted delimiter only when you intentionally inject shell values — and only when you trust the data:
user=golinuxcloud
sqlite3 :memory: <<SQL
SELECT '$user' AS expanded;
SQLgolinuxcloudPrefer 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:
name='O"Brien'
cat <<EOF
{"user":"$name"}
EOF{"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 -c 'cat <<EOF
line
EOF
' 2>&1bash: line 3: warning: here-document at line 1 delimited by end-of-file (wanted `EOF')
line
EOFBecause 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
- GNU Bash Manual — Here Documents — delimiter quoting and
<<- - GNU Bash Manual — Here Strings —
<<<redirection - GNU Bash Manual — Shell Expansions — what runs in unquoted heredoc bodies
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.

