Bash Script CRLF Character Causing Hidden Syntax Errors
Quick answer
CRLF characters in a Bash script are, by definition, invisible in a normal text editor — the carriage return character doesn't display as anything visible,...
CRLF characters in a Bash script are, by definition, invisible in a normal text editor — the carriage return character doesn't display as anything visible, which is exactly why this class of bug is so frustrating: the script looks completely correct to the eye, and the actual problem requires a tool that reveals non-printing characters to actually see.
The Problem
A script produces confusing syntax errors that don't correspond to any visible mistake when you look at the code:
./deploy.sh: line 12: syntax error near unexpected token `$'do\r''
./deploy.sh: line 12: `for i in 1 2 3; do'
Opening the file in a typical text editor shows what looks like completely normal, syntactically valid Bash — the actual invisible character causing the problem simply isn't rendered by most editors in their default view.
Why It's Genuinely Invisible
A carriage return (\r, hex 0D) is a non-printing control character — when a text editor displays a line containing one, it typically either silently consumes it as part of rendering the line ending, or, on some systems/editors, moves the cursor to the start of the line without advancing it, potentially even causing the next visible character to visually overwrite what's already there, further hiding rather than revealing its presence. This is precisely why standard visual inspection of the file, however careful, generally cannot detect this specific class of issue.
The Fix — First, Actually See the Character
Use cat -A, which reveals non-printing characters explicitly, including a specific, visible marker for carriage returns:
cat -A deploy.sh | head -15
Lines containing a carriage return show a literal ^M marker at the position where it occurs — typically right before the newline at the end of each affected line, which immediately confirms the diagnosis and shows you exactly which lines are affected:
for i in 1 2 3; do^M
echo $i^M
done^M
Alternatively, file often detects and reports this directly without needing to inspect line-by-line content at all:
file deploy.sh
deploy.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators
For the most granular, byte-level confirmation, hexdump or xxd shows the actual raw bytes, letting you see the 0d 0a (CR LF) sequence directly rather than relying on any tool's interpretation or rendering of it:
hexdump -C deploy.sh | head -5
The Fix — Removing the Characters
Once confirmed, convert the file to Unix-style line endings using dos2unix, the most direct, purpose-built tool for this:
dos2unix deploy.sh
If dos2unix isn't available, sed achieves the same result and is available virtually everywhere by default:
sed -i 's/\r$//' deploy.sh
Re-verify the fix using the same detection method that originally confirmed the problem, confirming the ^M markers (or the 0d bytes) are genuinely gone:
cat -A deploy.sh | head -15
# should now show clean lines with no ^M markers present
To prevent this recurring, especially on a team where some members might be using Windows-based editors, configure your editor to save with LF line endings specifically for shell script files, and add a .gitattributes rule enforcing this at the repository level, so Git normalizes line endings consistently regardless of any individual contributor's local editor configuration:
# .gitattributes
*.sh text eol=lf
This ensures that even if someone's editor or Git configuration would otherwise introduce CRLF endings, the repository itself enforces LF for shell scripts specifically upon checkout, preventing the invisible-character problem from being reintroduced by a future commit from any contributor.
Still Not Working?
If dos2unix and sed both report success, but the script still exhibits strange behavior, check for other, less common invisible or non-standard characters beyond plain CRLF — a byte-order mark (BOM) at the very start of the file, or other Unicode whitespace characters that look like a normal space but aren't, can produce similarly confusing symptoms and require their own separate detection and cleanup approach. A broader byte-level inspection with hexdump -C across the entire file (not just the first few lines) is the most thorough way to rule out any other unexpected non-standard byte sequences beyond the specific CRLF pattern already addressed.