advanced patterns for multi-line text in bash scripts
A heredoc lets you embed multi-line text directly in a script without escaping every single line. But without knowing the difference between <
Table of Contents
- 1. Why heredocs and herestrings exist
- 2. <
- 3. <<-EOF: stripping leading tabs automatically
- 4. Herestring <<<"string": redirecting input without a pipe
- 5. Quoting the delimiter: preventing variable expansion in a heredoc
- 6. Generating configuration files from scripts
- 7. Capturing heredoc content into a variable
- 8. Common heredoc pitfalls
- 9. Heredoc variants compared
- 10. Summary
- 11. FAQ
1. Why heredocs and herestrings exist
A heredoc (short for here document) redirects multi-line text directly from the script into a command's standard input, without needing every line quoted or line breaks reconstructed with \n. Without a heredoc, a multi-line string would either have to be assembled from many separate echo calls or written as one nearly unreadable line full of \n escapes.
A herestring solves a related but smaller problem: passing an already existing, usually single-line string as standard input to a command, without writing an extra echo "$var" | command and thereby creating an extra pipe and an extra process. Both constructs solve the same basic problem, providing text as input, just for different amounts of text and with different syntax.
2. <
The basic form command < starts with a freely chosen delimiter right after <<, usually EOF (end of file) or END, and everything up to the line that contains exactly that delimiter and nothing else gets forwarded as standard input to the command. The delimiter's name has no special meaning to Bash, it only needs to be unique and must not accidentally appear as its own line inside the text body.
A common source of errors is that the closing delimiter line has to appear exactly, without leading whitespace (except with <<-EOF, see the next section) and without any trailing characters. A single trailing space after EOF or a comment on the same line makes Bash fail to recognize the delimiter and keep waiting for more input, which makes a script hang for no apparent reason.
cat <<EOF
line one
line two with a variable: $USER
EOF
# output:
# line one
# line two with a variable: deploy
3. <<-EOF: stripping leading tabs automatically
When a heredoc is indented inside a function or an if block, the variant <<-EOF with an extra hyphen makes Bash automatically strip leading tab characters from both the text body and the closing delimiter line before passing the text along. That lets the heredoc block visually follow the surrounding code instead of sticking to the left column and breaking the reading flow.
The restriction to tabs matters: <<-EOF only strips leading tab characters, never spaces. Anyone whose editor is configured to auto-indent with spaces instead of tabs sees code that looks visually identical, but where <<-EOF has no effect and the spaces end up in the output text. This trap is particularly nasty because the difference is usually invisible in the editor.
deploy_step() {
if true; then
cat <<-EOF
This line starts with tabs in the source,
but ends up without leading tabs in the output.
EOF
fi
}
deploy_step
4. Herestring <<<"string": redirecting input without a pipe
A herestring using <<< passes an already existing string directly as standard input, without spawning an extra process for echo. grep pattern <<< "$content" is functionally equivalent to echo "$content" | grep pattern, but saves a pipe and a subshell, which is measurably faster especially in loops with many iterations.
The practical benefit shows up mainly when feeding read calls with already existing variable values, for example splitting a string on spaces into several variables without needing a pipe, which would create its own subshell and cause variables set inside it to vanish after the call.
version_string="2.4.8-p4"
IFS='.' read -r major minor patch <<< "$version_string"
echo "Major: $major, Minor: $minor"
# Major: 2, Minor: 4
5. Quoting the delimiter: preventing variable expansion in a heredoc
By default, Bash expands variables, command substitution and backslash escapes inside a heredoc, exactly like inside double quotes. cat < followed by $HOME in the body replaces that placeholder with the actual path. That is desirable when a script needs to fill a configuration file with dynamic values, but dangerous when the heredoc content is supposed to be an example script or a literal template that itself contains $ characters.
Putting the delimiter in single or double quotes instead, for example <<'EOF', disables every expansion inside the heredoc entirely, and the text gets forwarded byte for byte exactly as it appears in the script. This one decision, quoted versus unquoted delimiter, is the single most important lever with heredocs and should be made deliberately for every heredoc instead of relying on the default behavior.
# Unquoted: $USER gets expanded
cat <<EOF
Current user: $USER
EOF
# Quoted: $USER stays literal text, no expansion
cat <<'EOF'
Example variable in the target script: $USER
EOF
6. Generating configuration files from scripts
One productive use case is writing a multi-line configuration file directly from a deployment script, by redirecting the heredoc to a target file instead of piping it through cat. The same quoting rule from the previous section decides whether variables from the surrounding script should flow into the generated file, or whether the target file itself should keep placeholders in $name format that get interpreted later by other software.
For Nginx or systemd unit files that use their own $ syntax, a quoted delimiter is almost always the right choice, so Bash does not mistakenly interpret those characters as its own variables. For templates that are meant to be deliberately filled with values from the script, like a hostname or a port number, an unquoted delimiter is exactly right instead.
#!/usr/bin/env bash
set -euo pipefail
app_port=8080
target="/etc/nginx/sites-available/app.conf"
# Quoted delimiter for Nginx's own $-variables, but $app_port
# from the script is deliberately interpolated beforehand
cat > "$target" <<CONF
server {
listen ${app_port};
location / {
proxy_pass http://127.0.0.1:${app_port};
proxy_set_header Host \$host;
}
}
CONF
7. Capturing heredoc content into a variable
Heredocs can be combined with command substitution to store multi-line, dynamically generated text in a variable instead of sending it directly to a file or a program, for later processing, for example as an email template or a log message that still needs formatting.
The combination text=$(cat < works exactly like a plain heredoc, with the same quoting behavior for the delimiter, except that the output of cat goes into the variable text instead of the terminal's standard output. It matters that command substitution always strips a trailing newline at the end, which is usually desirable with multi-line text but occasionally surprising.
hostname_local="$(hostname)"
report=$(cat <<EOF
Deployment report for $hostname_local
Time: $(date -Iseconds)
Status: success
EOF
)
echo "$report"
mail -s "Deployment" ops@example.invalid <<< "$report"
8. Common heredoc pitfalls
The most common trap is an invisible trailing space after the closing delimiter line, which makes Bash fail to recognize the heredoc as finished and leaves the script apparently hanging for no reason, until interrupted with Ctrl+D or Ctrl+C. An editor with visible whitespace characters, or a quick look with cat -A script.sh, reliably reveals these cases.
The second common trap is mixing tabs and spaces with <<-EOF: if an editor auto-converts tabs to spaces, indentation stays visually intact, but <<-EOF no longer strips it, and the generated output ends up with unwanted leading spaces, which causes hard-to-diagnose parse errors especially in generated YAML files.
9. Heredoc variants compared
The choice between <, <<-EOF, a quoted delimiter and a herestring depends on whether variables should be expanded, whether the block is indented in the source, and how much text actually needs to be passed. The table below summarizes the decision criteria.
Variant
Variable expansion
Leading tabs stripped
Typical use
<
Yes
No
Configuration file with dynamic values
<<-EOF
Yes
Yes, tabs only
Indented heredoc inside functions
<<'EOF'
No, literal text
No
Example scripts, templates with their own $ syntax
<<<"string"
Yes, before passing
N/A, single-line string
Passing an existing variable without a pipe
Mironsoft
Shell automation, DevOps tooling and deployment infrastructure
Shell scripts that hold up in production?
We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.
Code Review
ShellCheck analysis and manual review for critical Bash pattern violations.
Refactoring
Retrofitting error handling, logging and safe file operations.
CI Integration
Wiring ShellCheck and BATS into pipelines and building regression tests.
10. Summary
Heredocs and Herestrings: The Essentials at a Glance
Basic rule
The delimiter after << marks the heredoc's start and end, the closing line must appear exactly, with no extra characters.
Quoting
A quoted delimiter like <<'EOF' disables all variable expansion, an unquoted delimiter expands $VAR just like inside double quotes.
Indentation
<<-EOF only strips leading tabs, never spaces. With space-based indentation in the editor, the effect simply does not happen.
Herestring
<<<"string" replaces echo var | command for already existing, usually single-line values without an extra pipe.
11. FAQ: Heredocs and Herestrings: The Essentials at a Glance
1What is the difference between a heredoc and a herestring?
A heredoc using << redirects multi-line text between two delimiter lines. A herestring using <<< passes an already existing, usually single-line string directly as input.
2Why does my script hang after a heredoc?
Usually there is an invisible space or character after the closing delimiter line. Bash then fails to recognize the delimiter and keeps waiting for more input.
3How do I stop variables from expanding inside a heredoc?
Put the delimiter in quotes, for example <<'EOF' instead of <
4What does <<-EOF do differently from <
<<-EOF strips leading tab characters from the text body and the closing line, so the heredoc can be indented in the source. Spaces are not stripped.
5Can I write a heredoc directly to a file?
Yes, with cat > file <
6Why is a herestring faster than echo var | command?
Because echo var | command spawns an extra process for echo plus a pipe, while <<<"var" redirects the input directly with no extra process.
7Can I store heredoc content in a variable?
Yes, with var=$(cat <
8Does the delimiter have to be named EOF?
No, the name is freely chosen as long as it is unique and does not accidentally appear as its own line in the text body. EOF and END are just common conventions.
9Are backslash escapes processed inside a heredoc?
With an unquoted delimiter, yes, exactly like inside double quotes. With a quoted delimiter, the backslash also stays a literal character.
10Does <<-EOF also work with space-based indentation?
No, <<-EOF only strips leading tabs. With space-based indentation in the editor, the spaces end up unchanged in the output.
The basic form command <<<, usually EOF (end of file) or END, and everything up to the line that contains exactly that delimiter and nothing else gets forwarded as standard input to the command. The delimiter's name has no special meaning to Bash, it only needs to be unique and must not accidentally appear as its own line inside the text body.
A common source of errors is that the closing delimiter line has to appear exactly, without leading whitespace (except with <<-EOF, see the next section) and without any trailing characters. A single trailing space after EOF or a comment on the same line makes Bash fail to recognize the delimiter and keep waiting for more input, which makes a script hang for no apparent reason.
cat <<EOF
line one
line two with a variable: $USER
EOF
# output:
# line one
# line two with a variable: deploy
3. <<-EOF: stripping leading tabs automatically
When a heredoc is indented inside a function or an if block, the variant <<-EOF with an extra hyphen makes Bash automatically strip leading tab characters from both the text body and the closing delimiter line before passing the text along. That lets the heredoc block visually follow the surrounding code instead of sticking to the left column and breaking the reading flow.
The restriction to tabs matters: <<-EOF only strips leading tab characters, never spaces. Anyone whose editor is configured to auto-indent with spaces instead of tabs sees code that looks visually identical, but where <<-EOF has no effect and the spaces end up in the output text. This trap is particularly nasty because the difference is usually invisible in the editor.
deploy_step() {
if true; then
cat <<-EOF
This line starts with tabs in the source,
but ends up without leading tabs in the output.
EOF
fi
}
deploy_step
4. Herestring <<<"string": redirecting input without a pipe
A herestring using <<< passes an already existing string directly as standard input, without spawning an extra process for echo. grep pattern <<< "$content" is functionally equivalent to echo "$content" | grep pattern, but saves a pipe and a subshell, which is measurably faster especially in loops with many iterations.
The practical benefit shows up mainly when feeding read calls with already existing variable values, for example splitting a string on spaces into several variables without needing a pipe, which would create its own subshell and cause variables set inside it to vanish after the call.
version_string="2.4.8-p4"
IFS='.' read -r major minor patch <<< "$version_string"
echo "Major: $major, Minor: $minor"
# Major: 2, Minor: 4
5. Quoting the delimiter: preventing variable expansion in a heredoc
By default, Bash expands variables, command substitution and backslash escapes inside a heredoc, exactly like inside double quotes. cat <$HOME in the body replaces that placeholder with the actual path. That is desirable when a script needs to fill a configuration file with dynamic values, but dangerous when the heredoc content is supposed to be an example script or a literal template that itself contains $ characters.
Putting the delimiter in single or double quotes instead, for example <<'EOF', disables every expansion inside the heredoc entirely, and the text gets forwarded byte for byte exactly as it appears in the script. This one decision, quoted versus unquoted delimiter, is the single most important lever with heredocs and should be made deliberately for every heredoc instead of relying on the default behavior.
# Unquoted: $USER gets expanded
cat <<EOF
Current user: $USER
EOF
# Quoted: $USER stays literal text, no expansion
cat <<'EOF'
Example variable in the target script: $USER
EOF
6. Generating configuration files from scripts
One productive use case is writing a multi-line configuration file directly from a deployment script, by redirecting the heredoc to a target file instead of piping it through cat. The same quoting rule from the previous section decides whether variables from the surrounding script should flow into the generated file, or whether the target file itself should keep placeholders in $name format that get interpreted later by other software.
For Nginx or systemd unit files that use their own $ syntax, a quoted delimiter is almost always the right choice, so Bash does not mistakenly interpret those characters as its own variables. For templates that are meant to be deliberately filled with values from the script, like a hostname or a port number, an unquoted delimiter is exactly right instead.
#!/usr/bin/env bash
set -euo pipefail
app_port=8080
target="/etc/nginx/sites-available/app.conf"
# Quoted delimiter for Nginx's own $-variables, but $app_port
# from the script is deliberately interpolated beforehand
cat > "$target" <<CONF
server {
listen ${app_port};
location / {
proxy_pass http://127.0.0.1:${app_port};
proxy_set_header Host \$host;
}
}
CONF
7. Capturing heredoc content into a variable
Heredocs can be combined with command substitution to store multi-line, dynamically generated text in a variable instead of sending it directly to a file or a program, for later processing, for example as an email template or a log message that still needs formatting.
The combination text=$(cat <cat goes into the variable text instead of the terminal's standard output. It matters that command substitution always strips a trailing newline at the end, which is usually desirable with multi-line text but occasionally surprising.
hostname_local="$(hostname)"
report=$(cat <<EOF
Deployment report for $hostname_local
Time: $(date -Iseconds)
Status: success
EOF
)
echo "$report"
mail -s "Deployment" ops@example.invalid <<< "$report"
8. Common heredoc pitfalls
The most common trap is an invisible trailing space after the closing delimiter line, which makes Bash fail to recognize the heredoc as finished and leaves the script apparently hanging for no reason, until interrupted with Ctrl+D or Ctrl+C. An editor with visible whitespace characters, or a quick look with cat -A script.sh, reliably reveals these cases.
The second common trap is mixing tabs and spaces with <<-EOF: if an editor auto-converts tabs to spaces, indentation stays visually intact, but <<-EOF no longer strips it, and the generated output ends up with unwanted leading spaces, which causes hard-to-diagnose parse errors especially in generated YAML files.
9. Heredoc variants compared
The choice between <<<-EOF, a quoted delimiter and a herestring depends on whether variables should be expanded, whether the block is indented in the source, and how much text actually needs to be passed. The table below summarizes the decision criteria.
| Variant | Variable expansion | Leading tabs stripped | Typical use |
|---|---|---|---|
< |
Yes | No | Configuration file with dynamic values |
<<-EOF |
Yes | Yes, tabs only | Indented heredoc inside functions |
<<'EOF' |
No, literal text | No | Example scripts, templates with their own $ syntax |
<<<"string" |
Yes, before passing | N/A, single-line string | Passing an existing variable without a pipe |
Mironsoft
Shell automation, DevOps tooling and deployment infrastructure
Shell scripts that hold up in production?
We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.
Code Review
ShellCheck analysis and manual review for critical Bash pattern violations.
Refactoring
Retrofitting error handling, logging and safe file operations.
CI Integration
Wiring ShellCheck and BATS into pipelines and building regression tests.
10. Summary
Heredocs and Herestrings: The Essentials at a Glance
Basic rule
The delimiter after << marks the heredoc's start and end, the closing line must appear exactly, with no extra characters.
Quoting
A quoted delimiter like <<'EOF' disables all variable expansion, an unquoted delimiter expands $VAR just like inside double quotes.
Indentation
<<-EOF only strips leading tabs, never spaces. With space-based indentation in the editor, the effect simply does not happen.
Herestring
<<<"string" replaces echo var | command for already existing, usually single-line values without an extra pipe.