skip to content

A bash script has a here-document nested inside an indented function, opened with `<<-EOF` and closed with an indented `EOF`. Bash still warns `here-document at line N delimited by end-of-file (wanted 'EOF')`. What does the `<<-` operator actually strip, and what must the closing delimiter line contain?

level: middleimportance: nice to knowfreq 40%

answer

  1. whitespace has a type here
  2. only one whitespace character qualifies
  3. the closing line is matched exactly
  4. tabs are stripped, spaces never

basics

~20 s

The dash in <<- strips leading TAB characters only — never spaces — from both the body lines and the delimiter line. A closing line indented with spaces therefore never matches, so bash reads to end of file and swallows the rest of the script.

solid answer

~40 s

`<<-` was designed for makefiles-era indentation: it removes leading **tab** characters from every body line and from the line holding the delimiter, so you can indent a here-doc to match the surrounding block. It does nothing about spaces, and most editors are configured to insert spaces or to strip trailing whitespace, which is why this breaks so often. The closing line must be the delimiter word alone — optionally preceded by tabs when you used `<<-`, with no leading spaces, no trailing spaces and nothing after it. When bash cannot find that line it consumes the remainder of the file as here-doc data and emits the `delimited by end-of-file` warning, so the symptom usually appears far from the real defect. The dash is orthogonal to quoting: `<<-'EOF'` strips tabs *and* keeps the body literal.

code

bash · 9 lines
bash
greet() {
	cat <<-EOF
		hello from $USER
	EOF
	cat <<-'EOF'
		literal $USER
	EOF
}
greet

go deeper

for a junior

Know that the closing delimiter must sit alone on its own line, and that the - in <<- exists to let you indent it. Do not add trailing spaces after it.

for a middle

State precisely that only leading tabs are stripped, from both the body and the delimiter line, and explain the end-of-file warning that follows a mismatch. Mention that the dash is independent of delimiter quoting.

for a senior

Show how you diagnose it: bash -n to parse without executing, cat -A to see whether the indentation is really tabs, and a team policy — pinned tabs in .editorconfig or column-zero here-docs — so the invariant survives editors and formatters.

for a principal

Own the tradeoff between readability and fragility: a construct whose correctness depends on an invisible character will break under any formatting toolchain you adopt later. Decide whether generated text belongs in a here-doc at all or in a versioned template file.

## The problem `<<-` solves A plain here-document takes its body verbatim, and its terminator must sit at the very start of a line. Inside an indented function or `if` block that looks wrong: ```bash deploy() { if [[ -n $target ]]; then cat <<EOF usage: deploy TARGET EOF fi } ``` The body and the `EOF` have to break out to column zero, which readers routinely mistake for a mis-indentation and "fix". `<<-` exists so the whole construct can be indented with the code around it. ## What it strips — exactly When the operator is `<<-`, bash removes **leading tab characters** from each line of the body and from the line containing the delimiter, before the body is handed to the command and before the delimiter is compared. That is the entire feature. Note the three things it does *not* do: - It does not strip spaces. Not one. An indentation of four spaces is data. - It does not strip tabs that appear after the first non-tab character, so alignment inside a line is preserved. - It does not change expansion. Quoting still lives on the delimiter word: `<<-EOF` expands, `<<-'EOF'` does not. A working example, where every indent below is a literal tab: ```bash greet() { cat <<-EOF hello from $USER EOF } ``` The body arrives as `hello from …` with no leading whitespace at all — including the extra tab used for visual nesting, because *all* leading tabs go, not just one level. ## Why the failure is so confusing If the delimiter line does not match, bash does not stop where you wrote the here-doc. It keeps reading, treating every following line as here-doc input, until it reaches end of file. Then it prints: ``` bash: warning: here-document at line 12 delimited by end-of-file (wanted `EOF') ``` and runs the command with a body that contains the rest of your script. So the visible symptom is often "the second half of my script never ran" or "cat printed my source code", and the reported line number points at the *opening* of the here-doc, not at the broken terminator. The four ways to break the match are all invisible in a diff: 1. The closing line is indented with spaces (or space-then-tab) rather than tabs, under `<<-`. 2. The closing line is indented at all, and you used plain `<<` rather than `<<-`. 3. There is trailing whitespace after the delimiter word. 4. Something follows the delimiter on the line — a comment, a semicolon, a `)`. A fifth, subtler one: the delimiter word must match after quote removal, so an opener of `<<-'EOF'` is still closed by a bare `EOF`, not by `'EOF'`. ## Living with editors The practical difficulty is that tabs are hostile to modern tooling. Editors configured with `expandtab`, format-on-save hooks and "trim trailing whitespace" settings all quietly convert or delete exactly the characters `<<-` depends on, and a reviewer reading the diff sees nothing. Teams therefore tend to pick one of three policies: - **Don't indent the here-doc.** Accept the visually odd column-zero body and terminator with a plain `<<`. This is the most robust and by far the most common choice in scripts that many people edit. - **Use `<<-` and pin tabs** for shell files in `.editorconfig` or the project's editor settings, so the invariant is enforced rather than remembered. - **Avoid the here-doc entirely** for short blocks, using `printf '%s\n' 'line one' 'line two'`, which indents like ordinary code and needs no terminator. ## Reading it in review When a script mysteriously stops executing partway through, `<<-` with space indentation is high on the list of candidates, along with an unterminated quote. Two quick checks: run `bash -n script.sh` to parse without executing, and `grep -nP '^\t* *EOF *$' script.sh` (GNU grep) or `cat -A` to reveal whether the indentation on the delimiter line is really tabs. `cat -A` renders tabs as `^I` and marks line ends with `$`, which makes both the wrong-character and the trailing-space cases obvious immediately.

  • What actually happens to the script when the closing delimiter never matches?
    Bash keeps reading. Every remaining line of the file becomes here-doc input, the command runs with that body, and bash prints `warning: here-document at line N delimited by end-of-file (wanted 'EOF')`. The reported line is where the here-doc opened, so the symptom — the rest of the script never executing — appears far from the real defect.
  • Why do many teams avoid `<<-` altogether?
    Because its correctness depends on literal tab characters, and editor settings such as expand-tabs and trim-trailing-whitespace, plus formatters and copy-paste, silently convert or remove them. The breakage is invisible in a diff. Most teams either leave the body and terminator at column zero with a plain `<<`, or pin tab indentation for shell files in .editorconfig.
  • Does `<<-` change whether the body is expanded?
    No — the two are independent. Expansion is controlled solely by whether the delimiter word is quoted, so `<<-EOF` strips tabs and still expands variables, while `<<-'EOF'` strips tabs and keeps the body literal. Pick the dash for indentation and the quotes for expansion separately.

saying these in an interview costs you the question

  • Says <<- strips leading spaces as well as tabs
  • Indents the closing delimiter with spaces
  • Leaves a trailing space after the delimiter word
  • Believes <<- also turns off variable expansion
  • Closes a <<-'EOF' here-doc with a quoted 'EOF' line

context