Bash: Syntax Error Near Unexpected Token (tested)

bash: syntax error near unexpected token (exit code 2) means the parser got confused about the script's structure — usually a block that never closed, or a line ending the parser didn't expect. We reproduced both of the two most common real causes: a missing fi/done/esac, and Windows-style CRLF line endings.

1. Reproduced: a missing fi, done, or esac at the very end of a script

$ cat missingfi.sh
#!/bin/bash
if [ "$1" = "x" ]; then
  echo "yes"
$ bash missingfi.sh
missingfi.sh: line 4: syntax error: unexpected end of file

We got the identical "unexpected end of file" message for a missing done closing a for loop and a missing esac closing a case — when the missing keyword is the very last thing in the file, bash just keeps reading, waiting for the block to close, until it runs out of file. Exit code was 2 in every case.

2. Reproduced: the same mistake, but now it really does say "unexpected token"

The slug of this guide is the literal message, so here's exactly when you get it instead of "unexpected end of file": when there's more file left to parse after the point where bash gives up. We left esac out of a case block and added one more line after it:

$ cat missingesac2.sh
#!/bin/bash
case "$1" in
  x) echo "x" ;;
echo "after case"
$ bash missingesac2.sh
missingesac2.sh: line 4: syntax error near unexpected token `"after case"'
missingesac2.sh: line 4: `echo "after case"'

case/esac has stricter grammar than if/for: after a pattern's ;;, bash expects either another pattern or esac, not an arbitrary command — so it names the bad token immediately instead of reading to EOF.

The same thing happens with an unclosed if or for inside a function, because the function's own closing } is what bash trips over:

$ cat missingfi3.sh
myfunc() {
  if [ "$1" = "x" ]; then
    echo "yes"
}
$ bash missingfi3.sh
missingfi3.sh: line 5: syntax error near unexpected token `}'
missingfi3.sh: line 5: `}'

And a stray extra fi (one too many) names itself directly:

$ printf 'if [ "$1" = x ]; then\n echo yes\nfi\nfi\n' | bash
bash: line 4: syntax error near unexpected token `fi'

3. Reproduced: Windows CRLF line endings, variant 1 — a stray $'\r' command

A script edited on Windows (or downloaded/copied without conversion) has \r\n line endings instead of bash's expected \n. file and cat -A show it instantly — ^M is the carriage return:

$ file crlfblank.sh
crlfblank.sh: ASCII text, with CRLF line terminators
$ cat -A crlfblank.sh
echo one^M$
^M$
echo two^M$

Running it is revealing: a blank-looking line (just the stray \r) gets executed as its own "command":

$ bash crlfblank.sh
one
crlfblank.sh: line 2: $'\r': command not found
two

The script didn't stop — that line just failed on its own and bash moved on. This is the single most common tell of a CRLF file: an error mentioning literal $'\r'.

4. Reproduced: CRLF, variant 2 — a real "unexpected token"

The more disruptive case: a \r glued directly onto a keyword like then, in, or { makes that keyword unrecognizable to the parser — then\r is a different token than then. We built a file with CRLF endings around a case block:

$ cat -A t3.sh
case "$1" in^M$
  x) echo x ;;^M$
esac^M$
echo done^M$
$ bash t3.sh
t3.sh: line 1: syntax error near unexpected token `$'in\r''
t3.sh: line 1: `case "$1" in'

Same result on a function definition, where {\r isn't recognized as opening the function body:

$ bash t4.sh
t4.sh: line 1: syntax error near unexpected token `$'{\r''

The $'...\r' quoting in the error message is the giveaway — bash is showing you that the token it choked on has an invisible carriage return stuck to the end of it. If you see $'...\r' anywhere in a syntax error, the file has CRLF endings; stop looking for a missing fi and fix the line endings instead.

5. Fixing CRLF, confirmed

Either of these strips the \r and leaves the script byte-identical otherwise. We confirmed both on the broken files above and reran them successfully afterward:

$ sed -i 's/\r$//' script.sh
$ tr -d '\r' < script.sh > fixed.sh

dos2unix script.sh does the same if it's installed (we didn't have it on this box, so we didn't measure it — but it's a standard, widely documented tool for exactly this). After stripping, re-check with file script.sh; it should say "ASCII text" with no mention of CRLF.

6. Reproduced: an unclosed quote, several lines before the error

A different, sneakier cause of the same family of errors: a quote that never closes. We opened a double quote on line 3 and never closed it:

$ cat -n quotesneaky.sh
     1  #!/bin/bash
     2  # config
     3  name="deploy-bot
     4  log() {
     5    echo "[$name] $1"
     6  }
     7  log "starting deploy"
     8  log "step 1 done"
     9  log "step 2 done"
    10  log "finished"
$ bash quotesneaky.sh
quotesneaky.sh: line 10: unexpected EOF while looking for matching `"'

The unclosed quote is on line 3. Bash reports line 10 — the real last line of the file, 7 lines away — because once bash sees the opening ", everything after it (including what look like other commands, other quotes, even the function's closing }) is swallowed as plain text inside that one unterminated string, all the way to true end of file. The message is also worded differently from the missing-fi case above: unexpected EOF while looking for matching `"' instead of syntax error: unexpected end of file — both mean bash ran out of file before a construct closed, just phrased differently depending on whether it was waiting for a keyword or a quote character.

To find the real line instead of guessing from the EOF line, count quotes (an odd total confirms one is unmatched) and then bisect:

$ grep -o '"' quotesneaky.sh | wc -l
11
$ head -n 3 quotesneaky.sh > bisect3.sh && bash -n bisect3.sh
bisect3.sh: line 3: unexpected EOF while looking for matching `"'
$ head -n 2 quotesneaky.sh > bisect2.sh && bash -n bisect2.sh
$ echo $?
0

Line 2 parses clean, line 3 doesn't — confirming the unclosed quote's exact location directly instead of guessing. This same truncate-and-check trick works for a missing fi/done/esac too: shrink the file with head -n N right after the suspect block and see if bash -n still complains.

7. A heredoc missing its closing delimiter is only a warning, not a hard error

This one behaves differently from every case above. We wrote a heredoc with no closing EOF:

$ cat heredocbad.sh
#!/bin/bash
echo "start"
cat <

Only a warning — the script still ran to completion and exited 0. Bash treats "ran out of file before the heredoc's closing delimiter" as recoverable: it just closes the heredoc at real end-of-file and keeps going. If a script is silently swallowing trailing lines into a heredoc instead of crashing outright, this easy-to-miss warning on stderr is usually why.

8. Checklist

  • "unexpected end of file": count your if/fi, for&while/done, and case/esac pairs — one of them isn't closed, usually the last one before the error line.
  • "unexpected token `}'" inside a function: same missing-keyword problem, just caught by the function's closing brace instead of true end-of-file.
  • "unexpected token" naming a quoted string or command right after a case block: missing esac.
  • "unexpected EOF while looking for matching `"' (or `'')": an unclosed quote, possibly many lines before the reported line. Count quotes with grep -o '"' script.sh | wc -l (odd = unclosed), then bisect with head -n N + bash -n to find the exact line.
  • A "here-document delimited by end-of-file" warning: a heredoc's closing delimiter is missing or misspelled. Unlike the others this doesn't stop the script, so check output carefully for swallowed trailing lines.
  • Error mentions $'\r' anywhere, or file yourscript.sh says "CRLF": Windows line endings. Run sed -i 's/\r$//' script.sh and retry before touching anything else.
  • Run bash -n script.sh to check syntax without actually running the script — useful for testing any of the fixes above quickly and safely, even on scripts with real side effects.

If the script runs but dies on a different error about a variable, see Bash: Unbound Variable. If the problem is the command itself rather than the script's structure, see Bash: Command Not Found.

Frequently asked questions

Why does a missing fi sometimes say "unexpected end of file" and sometimes "unexpected token"?

We confirmed both: if the missing fi/done/esac is the last thing in the file, bash just keeps waiting for the block to close until it hits real end-of-file, and reports 'unexpected end of file'. If there's more code after the point of confusion — e.g. a function's closing } right after an unclosed if, or another line after a case block missing esac — bash names that specific token instead. Same root cause, different message depending on what comes after.

What does $'\r': command not found actually mean?

It means the script has Windows-style CRLF line endings, and a line that looks blank is actually just a lone carriage return, which bash tries to run as a command and fails. We reproduced this exactly with a CRLF file containing a blank line between two echo commands — the script kept running afterward, it just failed that one line.

How do I tell a CRLF problem from a genuinely missing fi/done/esac?

Check the error text and the file itself. We confirmed CRLF errors either say $'\r': command not found, or show a token with a visible \r glued to it in the error message, like unexpected token `$'in\r''. Also run file script.sh — it will say 'with CRLF line terminators' if that's the issue. A genuinely missing fi/done/esac shows none of that; it's just 'unexpected end of file' or a plain keyword name.

What's the fastest fix for a CRLF script?

We confirmed both of these work: sed -i 's/\r$//' script.sh or tr -d '\r' < script.sh > fixed.sh. Re-run file script.sh afterward to confirm it now says plain ASCII text with no CRLF mention before retrying the script.

How can I check a script's syntax without actually running it?

bash -n script.sh parses the file and reports syntax errors without executing anything. We used it alongside the direct runs in this guide and got identical error text, so it's a safe way to test a suspected missing fi/done/esac, unclosed quote, or CRLF fix before running a script that has real side effects.

Why does bash report a line number that doesn't have the actual mistake on it?

We confirmed this for an unclosed quote: everything after the opening quote, including lines that look like separate commands, is swallowed into one string, so the error only surfaces at the real end of the file — which can be many lines after the actual open quote. The same thing happens for a missing fi/done/esac: bash keeps reading hoping to find the closer and only reports a line once it hits true end of file.

Is a heredoc missing its closing delimiter a fatal error?

No — we measured this directly: it's only a warning ("here-document delimited by end-of-file"), the script still runs to completion, and the exit code was 0 in our test. The heredoc just captures everything up to the real end of file as its content instead of stopping where expected, which can silently swallow trailing lines.

Written with AI assistance. Measured on 2026-10-03 with GNU bash (Debian) on a real Linux box. Every error message and exit code below is copy-pasted output, not paraphrased. PlainHeaders is our extension; bash is free.