Decoding "syntax error near unexpected token"

Decoding "syntax error near unexpected token"

What You'll Learn

  • How to read the syntax error near unexpected token message
  • Why the token's position and the real cause often differ
  • How to debug by token: fi / then / done / ( / newline / end of file

Quick Summary

In syntax error near unexpected token 'X', the X is where bash gave up parsing, and the actual mistake is usually before it.

  1. Classify by the token (fi/then/done = control flow, ( = parentheses / special chars, newline/end of file = something unclosed)
  2. Read the error line and the line just before it (suspect what comes before the token, not the token itself)
  3. Check syntax only with bash -n script.sh (find the line without running it)
  4. Inspect line endings and hidden chars with cat -A

Assumptions

  • Shell: bash (the default on Ubuntu / Debian)
  • Target: a .sh script you wrote, or a command typed in an interactive shell
  • /bin/sh (dash) prints different wording (covered below)

What is syntax error near unexpected token?

Conclusion: While bash parses your command, it hit a word (token) that cannot legally appear there. It stops at the syntax-check stage, before running anything.

Before running a command, bash first parses it to check that the words are arranged according to its grammar. The moment it decides "this word cannot legally be here," it prints this error and runs nothing.

$ echo hello world)
bash: syntax error near unexpected token `)'

Here ) is the unexpected token. bash read echo hello world fine, but a ) appeared with no matching (, so it flagged a syntax error.

While command not found means "the command was not located (but the grammar was fine)," syntax error means "this is not even a valid statement." Nothing was executed.

Why does the token position differ from the real cause?

Conclusion: bash reads left to right and stops at the first point it can no longer interpret correctly. The reported token is that stopping point; the cause (something unclosed or missing) is usually before it.

This is the biggest trap with this error. Consider:

if [ "$x" -gt 0 ]
  echo "big"
fi
$ bash script.sh
script.sh: line 3: syntax error near unexpected token `fi'

The report points at fi on line 3, but the real cause is the missing then on line 1. bash was waiting for then after if [ ... ], got echo instead, then fi, and only then decided it could no longer interpret the input. So it reports the point where it stopped (fi).

Reading tip

When the token is a closing/terminating word () / } / fi / done / esac / end of file), suspect a problem with the matching opening side. Don't fix the token's position; review the structure before it.

When the token is fi / then / done

Conclusion: A missing separator (; or newline), a missing then / do, or an unclosed block. Check that if-fi and for-done pair up.

When the structure of if / for / while / case is broken, these keywords become the unexpected token.

No separator before then

A ; or newline is required between the condition and then.

# NG: no separator between ] and then
if [ "$x" = "1" ] then
  echo ok
fi
$ bash script.sh
script.sh: line 3: syntax error near unexpected token `fi'
# OK: separate with ; (or put then on the next line)
if [ "$x" = "1" ]; then
  echo ok
fi

Missing do, or an unmatched done / fi

# NG: the do is missing
for f in *.txt
  echo "$f"
done
$ bash script.sh
script.sh: line 3: syntax error near unexpected token `done'

for ... in ... must be followed by do. Reaching done with no do triggers this error.

When you paste only some lines, it's easy to drop a lone if or do and leave the structure half-open. Always verify a block's opening and closing as a pair.

When the token is ( or a symbol

Conclusion: An unquoted shell special character such as ( ) & ; | < >. Wrap symbols inside filenames or strings in quotes.

( and ) are special characters used for subshells and function definitions, so writing them bare in a filename or argument causes a syntax error.

# NG: the () in the filename is read as special characters
$ cp report(final).txt /backup/
bash: syntax error near unexpected token `('

Fix it by quoting or escaping.

# OK: wrap in quotes
$ cp 'report(final).txt' /backup/

# OK: escape with a backslash
$ cp report\(final\).txt /backup/

The same happens with & (background) or ; (command separator).

# NG: & is read as a background operator
$ echo Tom & Jerry

Always quote any symbol you mean to pass as literal text.

A paste-time trap

Copying from the web or docs can turn " into curly "smart quotes" (" "). They look similar but bash treats them as different characters, so the quote never closes and you get a syntax error. Check with cat -A, or retype the quotes by hand.

When the token is newline or end of file

Conclusion: An unclosed quote, parenthesis, here-document, or control block. bash read to the end of the file without finding the close and stopped at the terminator.

unexpected token 'newline' or unexpected end of file is a sign that something opened was never closed.

An unclosed quote

$ echo "hello
>

The opened " is never closed, so bash treats the next line as a continuation of the string and keeps waiting for input (the > prompt). In a script, it stops at the end.

echo "hello
echo "world"
$ bash script.sh
script.sh: line 3: unexpected EOF while looking for matching `"'
script.sh: line 4: syntax error: unexpected end of file

A mismatched here-document terminator

# NG: the terminator does not match (not at the start of the line / has leading spaces)
cat <<EOF
hello
  EOF

A here-document terminator must be an exact label starting at the beginning of the line. Indent it and it is not recognized, so you get unexpected end of file at the end of the file (<<-EOF only allows leading tabs).

Are you running it with sh?

Conclusion: Running bash-only syntax ([[ ]], arrays, process substitution) with sh script.sh causes a syntax error. Run it with bash, or set the shebang to #!/bin/bash.

On Ubuntu, /bin/sh is dash, which does not understand bash extensions. A script that works under bash will fail when run with sh.

# A script with bash-only arrays and [[ ]]
arr=(a b c)
[[ -n "$1" ]] && echo "$1"
# NG: run with sh (dash)
$ sh script.sh
script.sh: 1: Syntax error: "(" unexpected

dash prints Syntax error: "(" unexpected with different wording, but the cause is the same: syntax that POSIX sh does not support. Fix it one of these ways.

# 1. Run it explicitly with bash
$ bash script.sh

# 2. Set the shebang to bash and run it directly
$ head -1 script.sh
#!/bin/bash
$ ./script.sh

./script.sh runs under the interpreter in the shebang, but sh script.sh ignores the shebang and runs under dash. Check that you aren't accidentally prefixing sh. See Fixing "bad interpreter" for details.

When CRLF line endings are the cause

Conclusion: Scripts edited on Windows carry a \r (CR) at each line end, causing syntax errors and odd behavior. Spot ^M with cat -A and convert with dos2unix.

A script saved by a Windows editor uses CRLF (\r\n) line endings, leaving an invisible \r at the end of every line. It sticks to keywords and quotes and triggers syntax errors or command not found.

# the ^M at line ends shows CR contamination
$ cat -A script.sh
#!/bin/bash^M$
if [ "$x" = "1" ]; then^M$
echo ok^M$
fi^M$

$ marks the line end and ^M is the CR. Convert it to LF.

# shortest, if dos2unix is available
$ dos2unix script.sh

# otherwise strip it with sed / tr
$ sed -i 's/\r$//' script.sh

Set your editor to save with "line endings: LF" and "encoding: UTF-8" to prevent recurrence. Adding *.sh text eol=lf to .gitattributes also works well.

Diagnostic Checklist

Conclusion: Work through "classify by token -> suspect what's before it -> check with bash -n -> inspect hidden chars with cat -A" and you'll pin down almost every syntax error near unexpected token.

Go top to bottom.

  • [ ] Checked the token name (fi/then/done = control flow, ( = special char, newline/end of file = unclosed)
  • [ ] Re-read the line before the token, not the token's position
  • [ ] Ran bash -n script.sh to check syntax only and locate the line
  • [ ] Verified if-fi / for-done openings and closings match
  • [ ] Verified quotes, parentheses, and here-documents are closed
  • [ ] Quoted special characters in filenames and arguments
  • [ ] Ran with bash script.sh / ./script.sh, not sh script.sh
  • [ ] Checked for ^M (CR) with cat -A and ran dos2unix if present

Next reading: