Fix bash 'syntax error near unexpected token'
Bash names the token it could not parse. Read it to find the cause: a pasted <file>, a missing then or do, CRLF line endings, or an HTML page piped into bash.
What "syntax error near unexpected token" means
Bash prints syntax error near unexpected token when it reads a word or symbol that its grammar does not allow at that point. The token in quotes is your best clue. It names what bash found when it gave up, and the real mistake is on that line or on a line above it.
A full error line has three parts. First comes a name: the script's file name, or bash when bash read the commands from a pipe or from bash -c. The GNU bash manual says this name, $0, is the one used in warning and error messages. Next comes a line number. Last comes the token. The command exits with a non-zero status, so a script that checks $? sees the failure.
Bash reads a script one command at a time. It parses a command, runs it, then reads the next one. So the commands on lines before the broken one may already have run when the error appears. A whole if block or loop is parsed before any of it runs, so nothing inside a broken block runs.
Check which bash you have before you compare your output with anyone else's:
bash --version | head -n 1As of October 2026, the Ubuntu archive lists bash 5.2.21 for Ubuntu 24.04 and bash 5.3 for Ubuntu 26.04. These are different releases. Compare the token, not the whole line.
Find your token
newline: a pasted placeholder such as<file>, or a redirect with nothing after it.fiordone: a missingthenordo, a missing semicolon after a[ ... ]test, or an empty body.thenordo: a keyword where bash needed a command, such aselsegiven a condition.(: parentheses in a filename, an extglob pattern with extglob off, or bash syntax run withsh.- a token with
\rinside it: Windows line endings. <ornewlineon line 1 ofcurl ... | bash: the download was a web page, not a script.
Set up a scratch directory
Every example below is safe to run. Each one writes a small file and then runs it.
mkdir -p ~/syntax-lab
cd ~/syntax-labThe examples write files with a quoted heredoc. The quotes around 'EOF' stop the shell you are typing in from expanding anything, so each file holds exactly the text shown.
Token newline: a pasted placeholder or a dangling redirect
Documentation often marks a value you must replace with angle brackets, such as <file> or <your-key>. Paste that line as it is, and bash reads the brackets as redirection operators. < means "read standard input from the next word". > means "write standard output to the next word". In cat <file>, the < takes file as its input. The > has nothing after it except the end of the line, so bash names the end of the line as the token: newline.
cat > placeholder.sh <<'EOF'
cat <file>
EOF
bash placeholder.sh
echo "exit status: $?"The output contains syntax error near unexpected token followed by newline, and the exit status is not 0. A redirect at the end of a line gives the same token, for example echo done > or command 2>. That usually happens when you delete a filename while editing and leave the operator behind.
The fix is to replace the placeholder with a real value:
printf 'hello from notes\n' > notes.txt
cat > placeholder.sh <<'EOF'
cat notes.txt
EOF
bash placeholder.shIt should print hello from notes. If you really want angle brackets as text, quote them. echo '<file>' prints the brackets and redirects nothing.
Tokens fi and done: a missing then, do or semicolon
After if, bash collects a list of commands as the condition. It keeps collecting until it reads then. Forget the then, and bash takes your next lines as part of the condition. Then it reaches fi while it is still waiting for then, so the token is fi.
cat > no-then.sh <<'EOF'
if [ -f /etc/passwd ]
echo "this server has a passwd file"
fi
EOF
bash no-then.sh
echo "exit status: $?"The output contains syntax error near unexpected token and fi. Look at the line number. The mistake is on line 1, but bash reports line 3, because line 3 is where it noticed.
A missing semicolon gives the same token. In if [ -f /etc/passwd ] then, the [ is an ordinary command, so then on the same line becomes one more argument to it. Bash still has not seen a real then, and it names fi again. End the test with ;, or put then on its own line:
cat > no-then.sh <<'EOF'
if [ -f /etc/passwd ]; then
echo "this server has a passwd file"
fi
EOF
bash no-then.shIt should print this server has a passwd file.
Loops work the same way. A while collects its condition until it reads do, so a missing do makes bash name done:
cat > no-do.sh <<'EOF'
while read -r line
echo "read: $line"
done < notes.txt
EOF
bash no-do.sh
echo "exit status: $?"The token is done. Change line 1 to while read -r line; do and the script prints read: hello from notes.
The empty body
A block with only a comment inside also fails. A comment is not a command, and bash needs at least one command between then and fi, or between do and done.
cat > empty-body.sh <<'EOF'
if [ -f /etc/passwd ]; then
# fill this in later
fi
EOF
bash empty-body.sh
echo "exit status: $?"The token is fi. Put the null command : in the body. It does nothing and succeeds, which is all a placeholder needs to do:
cat > empty-body.sh <<'EOF'
if [ -f /etc/passwd ]; then
: # fill this in later
fi
EOF
bash empty-body.sh
echo "exit status: $?"The exit status is now 0.
Tokens then and do: a keyword where a command should be
When the token is then or do itself, bash met that keyword at a point where it needed a command. A common cause is giving else a condition. Only if and elif take a condition. else takes commands only, so bash treats the test after it as an ordinary command, and the then that follows has nothing to belong to.
cat > else-test.sh <<'EOF'
if [ -d /no/such/dir ]; then
echo "first branch"
else [ -f /etc/passwd ]; then
echo "this server has a passwd file"
fi
EOF
bash else-test.sh
echo "exit status: $?"The output contains syntax error near unexpected token and then, on line 3. Neither echo runs, because bash parses the whole if block before it runs any of it. Change else to elif and the script prints this server has a passwd file:
cat > else-test.sh <<'EOF'
if [ -d /no/such/dir ]; then
echo "first branch"
elif [ -f /etc/passwd ]; then
echo "this server has a passwd file"
fi
EOF
bash else-test.shThe same rule explains do. A while or until needs at least one command as its condition, so while do with nothing between the two words makes bash name do. In both cases, read the line bash reports and the line above it for a keyword that lost its partner or its condition.
Token (: parentheses in a filename
In bash, ( and ) are operators. They start a subshell, or they mark a function definition when they follow a name. In the middle of a filename, bash cannot fit ( into its grammar, so it names (.
cat > parens.sh <<'EOF'
cp report(final).txt /tmp/
EOF
bash parens.sh
echo "exit status: $?"Quote the name, or put a backslash in front of each parenthesis:
printf 'final numbers\n' > 'report(final).txt'
cat > parens.sh <<'EOF'
cp 'report(final).txt' /tmp/
EOF
bash parens.sh
ls -l '/tmp/report(final).txt'ls should list the copied file. In an interactive shell, pressing Tab to complete the name adds the backslashes for you.
Extended glob patterns use parentheses too. A pattern such as !(*.log) only parses when the extglob option is on. In a shell where it is off, bash names (. Run shopt extglob to see the current setting, and read how bash globbing and extglob patterns work before you depend on one in a script.
Token ( again: bash syntax run with sh
This script uses an array, which is bash syntax:
cat > array.sh <<'EOF'
#!/bin/bash
files=(one.txt two.txt)
echo "second file: ${files[1]}"
EOF
bash array.sh
sh array.sh
echo "exit status: $?"bash array.sh prints second file: two.txt. sh array.sh fails on line 2. The #!/bin/bash line did not help, because the kernel reads that line only when you execute the file directly. When you type sh array.sh, you chose the interpreter yourself, and sh reads the #! line as a comment.
Check which shell sh is on your server:
ls -l /bin/sh
readlink -f /bin/shIf readlink prints /usr/bin/dash, then sh is dash, the Debian Almquist shell. Debian's package page calls dash the default system shell on Debian. Dash rejects the files=(...) line. Its message uses its own wording, not bash's, so look for the ( in it rather than for the bash phrase.
The fix is to run the file with bash. Either name bash, or make the file executable and let the #! line choose:
chmod +x array.sh
./array.shThe +x here is a symbolic mode, and chmod numeric and symbolic modes covers the other forms. ./array.sh and bash array.sh both end up in bash, but by different routes, and sourcing versus executing a bash script explains which shell reads the file in each case. Anything that hands your file to sh gives the same failure. A scheduled job is a common place to meet it, so check why a cron job runs by hand but not on schedule if the script only breaks under cron.
Token with \r: Windows line endings
Windows editors end each line with CRLF (carriage return, line feed). Linux tools end lines with LF only. Bash treats LF as the end of a line and treats the CR as an ordinary character. So the CR stays attached to the last word on every line. Here { becomes { plus a carriage return, which is not the { that opens a function body.
printf 'greet() {\r\n echo "hello"\r\n}\r\ngreet\r\n' > crlf.sh
bash crlf.sh
echo "exit status: $?"The output contains syntax error near unexpected token. Bash prints the token in its $'...' quoting so you can see the hidden character, and \r appears inside it. In other files the same cause shows up as the token $'\r' alone, as a command not found for a name ending in \r, or as an unexpected end of file when the CR sits on a fi or done.
Make the CRs visible with cat -A. The GNU coreutils manual says -A shows line ends as $ and shows the CRLF pair as ^M$:
cat -A crlf.shEvery line should end in ^M$. Strip the CRs with GNU sed. In GNU sed, \r matches a carriage return, and -i edits the file in place:
sed -i 's/\r$//' crlf.sh
cat -A crlf.sh
bash crlf.shNow each line ends in a plain $, and the script prints hello. The sed on macOS treats -i and \r differently, so run this fix on the Linux server. To stop the problem coming back, set your editor to save shell scripts with LF line endings.
Token < on line 1: you piped a web page into bash
This is the case behind many Claude Code install reports. In January 2026, a user opened GitHub issue 17813 on the Claude Code repository. They had run Anthropic's official installer, curl -fsSL https://claude.ai/install.sh | bash, on macOS. Bash printed syntax error near unexpected token with < as the token. On the next line it quoted line 1 of what it had received, which started with <!DOCTYPE html>. The page was titled "App unavailable in region | Claude". A commenter pointed out that the syntax error was not the real problem. The real problem was the region page. The issue was closed as not planned in February 2026.
Anthropic's Claude Code setup page now names this error. If the install command fails with syntax error near unexpected token '<', it sends readers to the installation troubleshooting page. Its section "Install script returns HTML instead of a shell script" says the install URL returned a web page, an XML document or an error status instead of the script. If the error output quotes "App unavailable in region", Claude Code is not available in your country. The page also lists network and routing problems and short service outages as causes. Its first fix is to wait a few minutes and try the same command again. It also lists other install methods, such as brew install --cask claude-code on macOS.
See it happen without a download
You do not need a failing server to see this. Pipe a fake HTML (the markup language of web pages) page into bash with printf. The first page is all on one line, the way many servers send it:
printf '<!DOCTYPE html><html><head><title>Error</title></head></html>\n' | bash
echo "exit status: $?"The output contains syntax error near unexpected token and <, on line 1. Here is why. To bash, <!DOCTYPE html> is a command named html with two redirects. The < reads input from a file named !DOCTYPE. The > that closes the tag is an output redirect, so it needs a file name next. The next thing on the line is the < of <html>, which is another operator, not a name. So bash names <.
Now send the same page with a line break after each tag:
printf '<!DOCTYPE html>\n<html>\n<head><title>Error</title></head>\n</html>\n' | bash
echo "exit status: $?"This time line 1 ends right after the >, so the token is newline. Both tokens mean the same thing on line 1 of a piped install: bash was handed a page, not a script. The message starts with bash: line 1: because bash read from a pipe and has no file name to show.
If you paste that HTML into an interactive terminal instead, you may see event not found first. That is because ! starts history expansion with the bang character in an interactive shell, before the parser ever sees the line.
The safer habit: download, read, then run
A pipe into bash runs whatever arrives, before you see it. Save the script to a file first, look at it, then run it:
curl -fsSL -o install.sh https://claude.ai/install.sh
head -n 5 install.sh
bash -n install.sh && echo "parses as a shell script"
less install.sh
bash install.shA shell script starts with a #! line or with shell commands. If head shows <!DOCTYPE html> or <html>, stop there. No edit to the file will help, because the problem is the download. When a vendor publishes a checksum, verify the download against its checksum before the last step. For the rest of the Linux install, see running Claude on the Linux desktop and in the CLI.
Check a script without running it: bash -n
bash -n parses a script and runs none of it. The Ubuntu 24.04 bash manual describes the option this way: "Read commands but do not execute them. This may be used to check a shell script for syntax errors."
printf 'if true\n echo hi\nfi\n' > check-me.sh
bash -n check-me.sh
echo "exit status: $?"
printf 'if true; then\n echo hi\nfi\n' > check-me.sh
bash -n check-me.sh
echo "exit status: $?"The first check reports syntax error near unexpected token with fi, and the status is not 0. The second prints nothing and exits 0. Neither run prints hi, because nothing in either file ran.
bash -n checks grammar only, and only bash's grammar. It does not know whether a command exists. It does not run command substitution, so it cannot tell you what a $(...) would return. It also accepts bash-only syntax that sh would reject. For that last case, ask dash. The dash manual says its -n option reads commands without running them and is useful for checking the syntax of shell scripts:
dash -n array.sh
echo "exit status: $?"Dash reports the array line, and the status is not 0. One more detail from the bash manual: interactive shells ignore this option, so typing set -n in your terminal does nothing.
A second opinion: ShellCheck
ShellCheck is a static analysis tool for shell scripts. Its README says one of its goals is to point out and clarify the beginner syntax mistakes that make a shell print cryptic error messages, which is this whole class of error. It is in the Ubuntu archive as shellcheck.
sudo apt update
sudo apt install -y shellcheck
shellcheck --versionAs of October 2026, the archive has shellcheck 0.9.0 for Ubuntu 24.04 and 0.11.0 for Ubuntu 26.04. Check your version line before you compare its output with someone else's.
ShellCheck reads the #! line to decide which shell a script is written for. Give it the array script with a sh header:
cat > sh-array.sh <<'EOF'
#!/bin/sh
files=(one.txt two.txt)
echo "second file: ${files[1]}"
EOF
shellcheck sh-array.shIt flags the array lines as syntax that sh does not support. You learn that before the script ever runs under dash. Unlike bash -n, it also explains each problem in plain words and links to a page for it. Run shellcheck on a script before you commit it, and run bash -n on any script before its first real run.
JavaScript's "Unexpected token '<'" is a different error
If the message starts with SyntaxError and comes from a browser console or Node.js, it is not a bash error. JSON.parse throws a SyntaxError when the text it gets is not valid JSON (JavaScript Object Notation). When the token is <, the text starts with <, as an HTML page does. The fix is on the request side, in the URL or the server response, and nothing in a shell script will change it.
FAQ
What does "syntax error near unexpected token" mean in bash?
Bash read a word or symbol that its grammar does not allow at that point, so it stopped before running that command. The token in quotes is where it stopped, and the mistake is on that line or a line above it. newline points at a stray < or >. fi or done points at a missing then, do or semicolon, or an empty block. ( points at parentheses bash cannot place, and a token containing \r points at Windows line endings.
Why does my script work with bash but fail with sh?
sh script.sh runs the file with whatever /bin/sh is, and it ignores the #!/bin/bash line because you named the interpreter yourself. Run readlink -f /bin/sh to see which shell that is. If it is dash, bash-only syntax such as arrays fails to parse. Run the script with bash script.sh, or make it executable and run ./script.sh so the #! line picks bash.
How do I fix syntax error near unexpected token $'\r'?
The file has Windows CRLF line endings, so every line ends in a carriage return that bash treats as part of the last word. Run cat -A script.sh and look for ^M$ at the line ends. Strip the carriage returns with GNU sed: sed -i 's/\r$//' script.sh. Then set your editor to save shell scripts with LF line endings so the problem does not come back.
Why does curl piped into bash fail with unexpected token '<' on line 1?
The URL returned an HTML page instead of a shell script, and bash tried to run the page. To bash, the < of an HTML tag is a redirection operator. For the Claude Code installer, Anthropic's troubleshooting page says this means the install URL returned a web page, an XML document or an error status. If the output quotes "App unavailable in region", Claude Code is not available in your country. Otherwise, wait a few minutes and retry. Download the script to a file and read its first lines before you run it.