Imagine dictating a letter to a secretary. If you say "write this exactly as I say, word for word", the letter comes out with every symbol you spoke. If you just say "write this", the secretary helpfully fills in blanks: every time you say "dollar name", they write in the person's actual name, and if they do not know it, they leave it empty.
A here-document is that dictation: cat <<EOF ... EOF feeds several lines to a command. Unquoted EOF is the helpful secretary who expands $var and $(cmd). Quoted 'EOF' is word for word. When writing a unit file, a script or an nginx config that contains $, you want word for word. And sudo tee file <<'EOF' is how you dictate straight into a root-owned file.
Why this lesson
Scripts often need to write a whole file - a systemd unit, a config, another script. Ten echo lines with >> are ugly and easy to get wrong. A here-document (heredoc) lets you paste the file's text right into the script and feed it to a command as its stdin. You have been using them since 6.2; this lesson explains the one detail that decides whether they work.
What you need to know already: stdin and < (1.7); sudo and why sudo echo x > /etc/file fails (1.11); systemd drop-ins (2.3); subshells and pipes (6.5).
The shape
cat <<EOF
line one
line two
EOF
<<EOF means "the lines that follow, up to a line that is exactly EOF, are this command's stdin". EOF is just a marker word, the delimiter - any word works, EOF is the custom.
The first prints hello world. The second prints hello $name.
($(date +%F) runs date and inserts today's date, e.g. 2026-09-22.)
Quote the delimiter whenever you are writing a file that contains $ - a script, a systemd unit, a config file. Forget, and the shell that is writing the file expands the variables right now, usually to empty strings, and you get a file full of blanks.
Remember why it is sudo tee and not sudo cat > file (1.11): the > is performed by your shell, as you, before sudo even starts. Here the heredoc is fed to tee's stdin, and tee - running as root - opens the file and writes it.
> /dev/null because tee also copies everything to stdout, and you do not need to see it twice.
<<- strips leading TABS
if true; then
cat <<-'EOF'
this line is indented with a tab in the script
but comes out flush left
EOF
fi
Only tabs, not spaces - which is why it is more trouble than it is worth in an editor that expands tabs.
Process substitution
diff <(sort a.txt) <(sort b.txt)
while read -r line; do ...; done < <(find . -name '*.log')
<(cmd) is process substitution: it makes a command's output look like a file. diff A B compares two files line by line, so the first example compares the sorted versions without saving them anywhere.
The second form you met in 6.8: piping into while read runs the loop in a subshell (6.5), so variables set inside it are lost afterwards. < <(...) feeds the loop from the command while keeping it in the current shell.
count=0
find . -name '*.log' | while read -r f; do count=$((count+1)); done
echo "$count" # 0 - the loop ran in a subshell
while read -r f; do count=$((count+1)); done < <(find . -name '*.log')
echo "$count" # correct
What you can now do
Write a multi-line file from a script with a heredoc, and choose <<'EOF' (literal) or <<EOF (expanded) on purpose.
Write a root-owned file with sudo tee FILE > /dev/null <<'EOF'.
Why it helps
Here-documents are how scripts generate config files, unit files, the setup script a new server runs on first boot, and input for other tools. Getting quoting wrong produces a config full of blanks where variables were expanded to empty strings, like an nginx config missing every $host or a script with its own variables replaced at write time, and nothing reports an error.
sudo tee /etc/systemd/system/x.service > /dev/null <<'EOF' is a pattern you will use and see in runbooks constantly, and it combines three lessons: quoting, redirection and the sudo trap. Process substitution in the same lesson fixes the while read subshell bug, and diff <(...) <(...) is a daily tool for comparing configs across environments.
How do I expand some variables but not others in a heredoc?
Use an unquoted delimiter and escape the dollars you want kept literal: \$host stays $host, while $PORT is expanded. That gets messy with many $, so alternatives are a quoted heredoc plus envsubst with an explicit variable list (envsubst '$PORT $HOST' < template), or sed substitution of placeholders. Dedicated templating tools are better for anything complex.
Does it matter which quote I use on the delimiter?
No. <<'EOF', <<"EOF" and even quoting part of it, <<E"OF", all make the body literal. What matters is whether any part of the delimiter word is quoted. The closing line must still be the plain word EOF, alone on its line, with no quotes and no trailing spaces, or bash keeps reading until the end of the file.
Why does my indented EOF not end the heredoc?
The closing delimiter must be at the start of the line. <<- allows leading tab characters, stripping them from the body and the delimiter line, but not spaces. Editors that convert tabs to spaces break it. Keep heredoc bodies unindented, or use <<- with real tabs and a shared editor config, or generate the text with printf lines instead.
Why use sudo tee instead of sudo cat > file with a heredoc?
In sudo cat > /etc/file <<'EOF', the > /etc/file redirection is performed by your own unprivileged shell before sudo runs, so it fails with Permission denied. With sudo tee /etc/file > /dev/null <<'EOF', the heredoc goes to tee's stdin and tee, running as root, opens the file. > /dev/null discards tee's copy of the content on stdout.
Can I feed a heredoc to any command?
Yes, to any command that reads stdin: sudo tee file, bash -s (run the lines as a script), or ssh host bash -s <<'EOF' to run a few commands on another machine. Quote the delimiter if the text contains $ meant for the other side. It is convenient in runbooks, but for anything long-lived, commit the file to git instead, so changes are reviewable.
In an interview Junior
What is the difference between a quoted and an unquoted here-document delimiter?
With an unquoted delimiter (<<EOF), the body is expanded: $var, $(cmd) and backticks are replaced by the shell writing it. With a quoted one (<<'EOF'), the body is passed literally.
Rule: quote the delimiter whenever the text you are writing contains $ - a script, a systemd unit with $MAINPID, a config file. Forget, and the variables are expanded now, usually to empty strings, and you get a broken file with no error.
To write a root-owned file: sudo tee /etc/systemd/system/demo.service > /dev/null <<'EOF' - tee runs as root and opens the file; sudo cat > file would fail because your own shell does the >.
Also asked: How do you write a root-owned file from a script? · What is process substitution, and what problem does < <(cmd) solve? · What does <<- do?