Linux / Bash

Bash Heredoc Syntax Errors, Common Mistakes

Bash heredoc syntax errors almost always come down to a small set of recurring mistakes — trailing whitespace after the closing delimiter, unexpected variable expansion inside quoted heredocs, or indentation that Bash doesn't strip the way many people expect.

The Problem

A heredoc block that looks syntactically correct produces an error like:

./script.sh: line 15: warning: here-document at line 5 delimited by end-of-file (wanted 'EOF')

Or the heredoc runs without an explicit error, but the content isn't what was intended — variables get expanded when they shouldn't be, or indentation appears in output where it wasn't wanted.

Why It Happens

Heredocs have specific, strict rules that are easy to violate without realizing it:

  • The closing delimiter must appear on its own line with absolutely no trailing whitespace or other characters after it — even a single trailing space after EOF prevents Bash from recognizing it as the closing delimiter, causing it to keep reading until actual end-of-file
  • By default, heredocs perform variable expansion, command substitution, and other shell processing inside the block, which is often not what's expected when writing something that should be treated as completely literal text
  • Regular heredocs (<<EOF) don't automatically strip leading whitespace from each line, so indented heredoc content (common when writing them inside an already-indented function or if block) preserves that indentation in the actual output, unless the specific tab-stripping variant is used
  • Using a quoted delimiter incorrectly, or forgetting that quoting the delimiter changes expansion behavior for the entire block, not just a specific line

The Fix

For the closing delimiter issue, ensure it appears alone on its own line, at the start of the line, with no trailing characters whatsoever:

cat <<EOF
some content here
EOF

If you need the heredoc content indented to match surrounding code (common inside functions), use the <<- variant instead of plain <<, which strips leading tab characters (not spaces) from each line, including the closing delimiter, allowing you to indent for readability without that indentation appearing in the actual content:

function generate_config() {
    cat <<-EOF
	This line can be indented with a tab
	EOF
}

Critically, this only strips tabs, not spaces — if your editor uses spaces for indentation (very common, especially with default settings in many editors), the <<- variant won't strip them, and you'll still see unwanted leading whitespace in the output. Confirm your editor is actually inserting tab characters, not spaces, for this to work as expected.

To prevent variable expansion and command substitution inside the heredoc, quote the delimiter:

cat <<'EOF'
This $variable will NOT be expanded
This $(command) will NOT be executed
EOF

Without quotes around the delimiter, the same content would have $variable and $(command) actually expanded/executed, which is usually not what you want when writing literal example text, configuration templates with their own separate variable syntax, or documentation content:

cat <<EOF
This $variable WILL be expanded (if quotes are omitted)
EOF

Choose deliberately based on what you actually need — quote the delimiter for literal content, leave it unquoted when you specifically want Bash to substitute variables into the heredoc's content.

Still Not Working?

If you're piping a heredoc into a command and seeing unexpected behavior specifically around exit codes or command chaining, remember that a heredoc is part of the command it's attached to, not a separate statement — placing anything between the heredoc's opening line and its closing delimiter that isn't intended as literal heredoc content (like a stray command) will be treated as content, not executed as a separate command, which can be a subtle source of confusion in scripts that mix heredocs with other logic in a single block:

# This entire block, including "echo done", is heredoc content -
# it does NOT run as a separate command until after the closing EOF
cat <<EOF
some content
echo done
EOF