When an apparently correct script fails on Linux with erratic messages such as \$'\\r': command not found, syntax error near unexpected token, or \$'{\\r'', the root cause almost always resides in the encoding and line-termination layer, rather than the Bash logic itself.
In this article, I document the production-grade procedure I utilize to:
- Technically prove the origin of the failure;
- Correct the affected files without unintended side effects;
- Shield the deployment pipeline to ensure the incident does not recur.
1) Root cause: CRLF (Windows) persistence in Linux runtimes#
The structural difference is straightforward yet critical:
- Windows records line endings as
CRLF(\r\n); - Linux/Unix strictly expects
LF(\n).
When Bash encounters a \r (carriage return) at the end of a line, it interprets this extra byte as part of the command token. The result is a "command not found" error for a non-existent command named \r, or structural syntax errors in code that visually appears perfect.
Classic field-observed errors:
./deploy.sh: line 2: $'\r': command not found
./deploy.sh: line 18: syntax error near unexpected token `$'{\r''
2) Reliable forensic diagnosis (moving beyond assumptions)#
Before attempting a fix, validate the evidence. I utilize four distinct verification methods.
2.1 file: objective detection of line terminators#
file script.sh
If the output contains with CRLF line terminators, the diagnosis is definitive.
2.2 cat -v: visualizing non-printable characters#
cat -v script.sh | sed -n '1,40p'
If you see ^M at the end of lines, that ^M is the literal \r damaging the execution flow.
2.3 od/hexdump: binary proof#
od -An -tx1 -N 80 script.sh
# or
hexdump -C script.sh | head
Look for the byte sequence 0d 0a (CRLF). In a pure Linux-native script, you should only see 0a.
2.4 shebang and permission validation (avoiding confusion)#
head -n 1 script.sh
ls -l script.sh
Ensure the script starts with a valid shebang (#!/usr/bin/env bash) and possesses the correct execution permissions. While this doesn't resolve CRLF, it eliminates false positives during triage.
3) Safe correction: three operational methods#
3.1 preferred method: dos2unix#
This is a dedicated, predictable tool for CRLF -> LF conversion.
dos2unix script.sh
Installation:
# Debian/Ubuntu
sudo apt update && sudo apt install -y dos2unix
# RHEL/CentOS/Alma/Rocky
sudo dnf install -y dos2unix
3.2 universal method without extra packages: sed#
sed -i 's/\r$//' script.sh
This command surgically removes the \r character at the end of each line while preserving all other content.
3.3 pipeline with temporary output: tr#
tr -d '\r' < script.sh > script.sh.lf && mv script.sh.lf script.sh
Use this when an explicit comparison between the original and converted file is required before performing the final replacement.
4) Bulk correction of entire directories (with protection)#
In real-world incidents, the issue rarely exists in a single file. To handle bulk conversion safely without affecting binaries:
find . -type f \( -name "*.sh" -o -name "*.bash" -o -name "*.env" \) -print0 \
| xargs -0 dos2unix
Post-conversion validation:
find . -type f -name "*.sh" -exec file {} \; | grep -i crlf && echo "CRLF still present" || echo "All scripts normalized to LF"
5) Recurrent production case: CRLF + bad interpreter#
Beyond the standard command error, I frequently encounter:
/bin/bash^M: bad interpreter: No such file or directory
This occurs when the \r contaminates the shebang line itself. The correction is identical (LF), followed by a fresh execution test.
6) Definitive hardening in the DevOps workflow#
Remediating a file resolves the current incident. To prevent recurrence, I standardized the repository, editor settings, and Git configuration.
6.1 .editorconfig for EOL governance#
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
[*.sh]
indent_size = 2
6.2 .gitattributes for repository normalization#
* text=auto
*.sh text eol=lf
*.bash text eol=lf
*.env text eol=lf
6.3 recommended workstation Git settings#
git config --global core.autocrlf input
git config --global core.eol lf
By setting autocrlf to input, Git automatically converts CRLF -> LF during the commit phase, preventing contaminated scripts from entering the repository.
6.4 CI quality gate#
I include an automated validation to break the pipeline if CRLF is detected in shell scripts:
#!/usr/bin/env bash
set -euo pipefail
if git ls-files '*.sh' '*.bash' | xargs file | grep -qi 'CRLF'; then
echo "ERROR: CRLF-encoded scripts detected"
git ls-files '*.sh' '*.bash' | xargs file | grep -i 'CRLF'
exit 1
fi
echo "OK: All shell scripts use LF"
7) Fast incident response runbook#
When the error manifests in production, I execute this exact sequence:
- Confirm the Issue:
file deploy.sh
cat -v deploy.sh | head
- Correct:
dos2unix deploy.sh
chmod +x deploy.sh
- Validate Syntax:
bash -n deploy.sh
- Execute with Controlled Trace:
bash -x deploy.sh
If dealing with a release batch, I apply the fix to all scripts and run bash -n checks for integrity:
find scripts -type f -name '*.sh' -exec bash -n {} \;
8) Technical conclusion#
the \$'\\r': command not found error is not a Bash bug - it is a lack of standardization between Windows and Linux environments. In a professional operation, this type of failure must be addressed as a code supply chain issue (editor + VCS + CI), rather than an isolated script incident.
By utilizing evidence-based diagnosis (file, cat -v, hexdump), controlled correction (dos2unix/sed), and pipeline hardening (.editorconfig, .gitattributes, CI gates), this problem effectively disappears from the daily operational cycle.
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments