Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 9 min read

Bash & Python Error Guide: 'syntax error near unexpected token'

Fix bash 'syntax error near unexpected token' caused by Windows CRLF line endings, broken heredocs, unbalanced quotes, and stray control characters in scripts.

  • #automation
  • #troubleshooting
  • #errors
  • #bash

Overview

A syntax error near unexpected token means bash hit a token (often `\r`, (, }, done, or newline) in a place its grammar does not allow. The script parsed fine up to that point, then the parser reached something it could not reconcile — frequently because the line it is reading is not what you see in your editor. The single most common cause on CI runners and shared servers is Windows-style CRLF line endings sneaking a carriage return onto the end of every line.

The classic form, where the token is literally a carriage return:

./deploy.sh: line 12: syntax error near unexpected token `$'\r''
'eploy.sh: line 12: `if [ "$ENV" = "prod" ]; then

Or the plain version when a block keyword is misplaced:

./build.sh: line 24: syntax error near unexpected token `done'
./build.sh: line 24: `done'

It occurs at parse time, before any command in the script runs. Because bash reports the line where parsing failed to resolve, the real mistake is often a few lines earlier (an unclosed quote, a heredoc that never terminated, or a function brace never opened).

Symptoms

  • The script aborts immediately with syntax error near unexpected token and never executes a single command.
  • The offending token is shown in backticks: `\r', `(', `}', `done', or `newline'.
  • The error line in the message looks syntactically fine in your editor.
  • The same script runs on your laptop but fails on a Linux CI runner (or vice versa).
bash ./deploy.sh
./deploy.sh: line 12: syntax error near unexpected token `$'\r''
# Confirm whether carriage returns are present
file ./deploy.sh
./deploy.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators

Common Root Causes

1. Windows CRLF line endings (\r)

The file was saved on Windows or by an editor configured for CRLF. Every line ends with \r\n; bash treats the \r as part of the token, so then\r is not the keyword then.

cat -A ./deploy.sh | head -5
#!/bin/bash$
$
ENV="$1"^M$
if [ "$ENV" = "prod" ]; then^M$
  echo "deploying"^M$

The trailing ^M is the carriage return. Strip it:

sed -i 's/\r$//' ./deploy.sh
# or
dos2unix ./deploy.sh

2. An unterminated heredoc

A <<EOF block whose closing delimiter is misspelled, indented (without <<-), or has trailing whitespace is never closed, so bash swallows the rest of the file and errors at EOF.

cat -n ./provision.sh
     5  ssh host <<EOF
     6    apt-get update
     7    apt-get install -y nginx
     8    EOF
./provision.sh: line 9: syntax error near unexpected token `newline'

Line 8’s EOF is indented, so it is not recognized as the delimiter. Use a left-aligned EOF, or <<-EOF with tab indentation.

3. Unbalanced quotes or parentheses

A missing closing quote makes bash keep reading until it finds another quote, merging lines and producing a token in an unexpected place.

grep -n "ECHO\|echo" ./report.sh
14:echo "Report for $(date)
15:echo "done"

The quote on line 14 is never closed, so echo "done" is parsed as a continuation. Close the quote on line 14.

4. A function or block brace never opened

}, fi, done, or esac appears with no matching opener — often after deleting a few lines and leaving the closer behind.

bash -n ./menu.sh
./menu.sh: line 30: syntax error near unexpected token `}'

bash -n (no-exec syntax check) pinpoints the unmatched closer. Find the missing {, then, do, or case.

5. A bare ( from an unescaped subshell or array in the wrong shell

Bash array syntax arr=(a b c) or process substitution <(...) fails under sh / dash, which many systems use for /bin/sh.

head -1 ./collect.sh
sh ./collect.sh
#!/bin/sh
./collect.sh: 4: Syntax error: "(" unexpected

The shebang says sh but the script uses bash-only (...). Change the shebang to #!/bin/bash or run with bash.

6. A UTF-8 BOM or non-breaking space at the top of the file

A byte-order mark (\xEF\xBB\xBF) or a non-breaking space (\xC2\xA0) copied from a web page sits invisibly before the shebang or a keyword.

hexdump -C ./setup.sh | head -1
00000000  ef bb bf 23 21 2f 62 69  6e 2f 62 61 73 68 0a 23  |...#!/bin/bash.#|

The leading ef bb bf is a BOM. Strip it:

sed -i '1s/^\xEF\xBB\xBF//' ./setup.sh

Diagnostic Workflow

Step 1: Run the no-exec syntax check

bash -n ./script.sh

This parses without running anything and reports the failing line — safe even for scripts with side effects.

Step 2: Check for CRLF and control characters

file ./script.sh
cat -A ./script.sh | grep -n '\^M\|\^I' | head

with CRLF line terminators or trailing ^M means line 1 of your fix is dos2unix.

Step 3: Inspect the reported line and the lines just above it

sed -n '20,28p' ./script.sh | cat -A

The real fault (unclosed quote, missing do/then) is usually a few lines before the reported line. cat -A exposes hidden characters.

Step 4: Verify heredoc delimiters are left-aligned and clean

grep -n '<<' ./script.sh
grep -nE '^[[:space:]]+EOF[[:space:]]*$' ./script.sh

Any indented or trailing-whitespace EOF is a non-terminating delimiter.

Step 5: Confirm the interpreter matches the syntax

head -1 ./script.sh
# If it uses arrays/(( ))/<() , run under bash explicitly:
bash ./script.sh

Example Root Cause Analysis

A deploy.sh that works on a developer’s macOS laptop fails the moment it runs in the GitLab CI runner:

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

The token includes \r, pointing straight at line endings. Confirming:

file ./deploy.sh
./deploy.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators

The repo had no .gitattributes, and a Windows contributor’s editor saved the file with CRLF. macOS bash tolerated the trailing \r in some contexts, but the CI runner’s stricter parse rejected it on the very first brace.

Fix: normalize the file and pin line endings going forward:

dos2unix ./deploy.sh
printf '*.sh text eol=lf\n' >> .gitattributes
git add .gitattributes ./deploy.sh

After conversion, bash -n ./deploy.sh returns clean and the pipeline proceeds.

Prevention Best Practices

  • Add a .gitattributes with *.sh text eol=lf so shell scripts are always checked out with Unix line endings regardless of who commits them.
  • Run bash -n (or shellcheck) on every script in CI before it executes — it catches CRLF, unbalanced quotes, and stray closers in milliseconds.
  • Keep heredoc delimiters left-aligned, or use <<-EOF with tab-only indentation; never indent a plain EOF with spaces.
  • Configure your editor to show whitespace and use LF; reject paste-ins that carry a BOM or non-breaking spaces.
  • Match the shebang to the syntax: use #!/bin/bash whenever you use arrays, [[ ]], (( )), or process substitution.
  • For more hardening patterns, see the Bash & Python automation guides.

Quick Command Reference

# Syntax-check without executing
bash -n ./script.sh

# Detect CRLF / control characters
file ./script.sh
cat -A ./script.sh | head

# Strip carriage returns
sed -i 's/\r$//' ./script.sh
dos2unix ./script.sh

# Strip a UTF-8 BOM from line 1
sed -i '1s/^\xEF\xBB\xBF//' ./script.sh

# Find heredoc delimiters and unbalanced ones
grep -n '<<' ./script.sh

# Lint thoroughly
shellcheck ./script.sh

Conclusion

syntax error near unexpected token is a parse-time failure where bash met a token its grammar forbids. The recurring causes:

  1. Windows CRLF line endings adding a \r to every line (the `$'\r'' token).
  2. A heredoc whose closing delimiter is misspelled, indented, or has trailing whitespace.
  3. An unbalanced quote or parenthesis merging lines together.
  4. A }, fi, done, or esac with no matching opener.
  5. Bash-only syntax run under sh/dash.
  6. An invisible BOM or non-breaking space before a keyword.

Start with bash -n and file/cat -A; the trailing ^M or the reported line (and the few lines above it) almost always reveals the culprit, and dos2unix plus a .gitattributes rule prevents the most common recurrence.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.