Skip to content

Here Documents and Here Strings


Every earlier chapter’s hands-on labs quietly relied on a piece of syntax never actually explained: cat > script.sh << 'EOF' ... EOF. That’s a here document, and the quotes around EOF weren’t decoration — they were doing real work. This chapter finally covers that syntax properly: here documents for multi-line input, here strings for single-line input, and exactly what that delimiter quoting was protecting you from all along.

Here Documents: <<

A here document feeds multi-line text directly into a command’s stdin, written right inline in your script — no separate file needed:

cat << EOF
Line one
Line two
Line three
EOF
Line one
Line two
Line three

The << operator is followed by a delimiter — here, EOF — which marks where the input begins. Everything after that, up to a line containing only that same delimiter, is fed to the command as stdin. EOF (“end of file”) is just a convention, not a keyword; any token works:

cat << DONE
This works identically
DONE

The closing delimiter has to be alone on its own line — no leading or trailing characters, including trailing spaces. That last part is a genuine gotcha worth remembering: a stray trailing space after the closing delimiter means Bash won’t recognize it as the end, and keeps reading subsequent lines of your script as heredoc data instead of running them as commands.

Quoted vs. Unquoted Delimiters

This is the part every earlier chapter’s << 'EOF' pattern was relying on. Whether the delimiter is quoted controls whether the heredoc body undergoes expansion:

Unquoted delimiter — variables and command substitution expand, same as inside double quotes:

name="Alice"
cat << EOF
Hello, $name
Today is $(date +%A)
EOF
Hello, Alice
Today is Saturday

Quoted delimiter — nothing expands; the body is treated completely literally, same as inside single quotes:

name="Alice"
cat << 'EOF'
Hello, $name
Today is $(date +%A)
EOF
Hello, $name
Today is $(date +%A)

Either 'EOF' or "EOF" triggers the literal, no-expansion behavior — Bash only checks whether the delimiter is quoted at all, not which quote style.

This is exactly why the scripts you’ve written throughout this course so far consistently used << 'EOF': those heredocs were writing the contents of another script, full of $variable references that needed to end up literally in the output file, to be expanded later when that script actually ran — not expanded immediately by the outer shell doing the writing. You’ll see that exact scenario worked through directly in this chapter’s Shell-Safety Considerations section.

Indented Here Documents: <<-

<<- behaves like <<, but strips leading tab characters from each line of the body and from the closing delimiter — letting you indent a heredoc to match the surrounding code, which is otherwise impossible since the closing delimiter normally has to start in column one:

if true; then
	cat <<- EOF
	This line is indented in the source
	So is this one
	EOF
fi
This line is indented in the source
So is this one

Warning

<<- strips tabs only — not spaces. If your editor is configured to insert spaces for indentation (extremely common), <<- won’t strip anything, and the leading spaces will show up in your output verbatim. Check your editor’s indentation settings before relying on this, or just skip <<- and leave heredoc bodies unindented, which is the simpler and more portable choice.

Here Strings: <<<

A here string feeds a single string as stdin in one line, with no delimiter needed at all:

cat <<< "A single line of input"
A single line of input

This is the simplest option whenever you already have the content in a variable and just need to hand it to a command as stdin:

sentence="The quick brown fox"
grep -o "quick" <<< "$sentence"
quick

Compare that to piping the same value in: echo "$sentence" | grep -o "quick" produces the same result, but spawns an extra process for echo just to move a string from a variable into a pipe. A here string skips that entirely.

Practical Uses

Here documents are commonly used for exactly the pattern you’ve already been using throughout this course — writing the contents of a file directly inline in a script:

cat > example-config.txt << 'EOF'
setting_one=true
setting_two=false
EOF

They’re also useful for feeding a fixed block of multi-line input to any command that reads from stdin, without needing a temporary file on disk at all. Here strings cover the same use case when the input is a single line already held in a variable — reaching for whichever one matches the shape of the data you actually have avoids unnecessary pipes or throwaway files.

Best Practices

  • Quote the delimiter whenever the heredoc body is meant to be literal — especially when writing out the contents of another script, a config file with $-prefixed values, or anything containing characters that look like expansion syntax but aren’t meant to be treated as such.
  • Leave the delimiter unquoted only when you deliberately want variable or command substitution inside the body — treat this as an intentional choice each time, not a default.
  • Keep the closing delimiter on its own line with no trailing characters. A stray trailing space is invisible in most editors and silently breaks the heredoc’s termination.
  • Prefer here strings over echo "$var" | command when feeding a single variable’s value to a command’s stdin — it’s simpler and avoids spawning an extra process.

Shell-Safety Considerations

Forgetting to quote a heredoc delimiter when writing out another script’s contents is a genuine, easy-to-make mistake — and it fails silently, producing a broken file with no error message at all.

Naive (broken) version — unquoted delimiter, writing a template script that’s meant to reference a variable at its own runtime:

cat > backup-template.sh << EOF
#!/usr/bin/env bash
target_dir="$HOME/backups"
echo "Backing up to $target_dir"
EOF
cat backup-template.sh
#!/usr/bin/env bash
target_dir="/home/you/backups"
echo "Backing up to "

Two things went wrong, both from the same cause. $HOME got expanded immediately, by the outer shell writing this file — which happens to give a plausible-looking result here, but means the generated script now has your home directory hardcoded into it rather than resolving $HOME freshly whenever it actually runs. Worse, $target_dir expanded too — to an empty string, because at the moment this heredoc was written, no target_dir variable existed yet in the outer shell. The generated script’s own echo line lost its variable reference entirely, silently, with no error anywhere in the process.

Corrected version — quoted delimiter, so the body is written completely literally:

cat > backup-template.sh << 'EOF'
#!/usr/bin/env bash
target_dir="$HOME/backups"
echo "Backing up to $target_dir"
EOF
cat backup-template.sh
#!/usr/bin/env bash
target_dir="$HOME/backups"
echo "Backing up to $target_dir"

Now both $HOME and $target_dir are preserved literally in the written file, exactly as intended — ready to be correctly expanded later, when backup-template.sh itself actually runs.

Hands-On: Here Documents And Here Strings In Practice

1. Set up a working directory.

mkdir -p ~/shell-course/ch6
cd ~/shell-course/ch6

2. Compare expansion with an unquoted vs. quoted delimiter.

name="Alice"

cat << EOF
Hello, $name
EOF

cat << 'EOF'
Hello, $name
EOF

3. Feed a variable to a command with a here string.

sentence="The quick brown fox jumps"
grep -o "brown" <<< "$sentence"
wc -w <<< "$sentence"

4. Reproduce the template-generation bug, then fix it.

cat > backup-template.sh << EOF
#!/usr/bin/env bash
target_dir="$HOME/backups"
echo "Backing up to $target_dir"
EOF
cat backup-template.sh

Confirm target_dir= shows an already-resolved (or empty) value instead of the literal variable reference. Then regenerate it correctly:

cat > backup-template.sh << 'EOF'
#!/usr/bin/env bash
target_dir="$HOME/backups"
echo "Backing up to $target_dir"
EOF
cat backup-template.sh
chmod +x backup-template.sh
./backup-template.sh
Backing up to /home/you/backups

Run correctly this time, backup-template.sh resolves $HOME and $target_dir itself, at its own runtime — exactly the behavior the broken version failed to produce.

5. Clean up.

cd ~
rm -rf ~/shell-course/ch6

You’ve now written heredocs both ways deliberately, used a here string to skip an unnecessary pipe, and seen — concretely, not just in theory — why quoting a heredoc’s delimiter is the difference between a script template that works and one that silently loses its own variables. The next chapter moves to conditionals: if/elif/else, the test and [[ forms, and pattern matching with case.

Last updated on