Fixing CRLF Line Endings in Shell Scripts
Errors like bad interpreter and $' ': command not found mean carriage returns reached a Unix shell script - confirm bytes, convert, enforce Git policy.
A script copied from a Windows-oriented editor may contain CRLF line endings: carriage return followed by line feed. Unix tools normally expect LF. The extra carriage return is invisible in many editors but remains part of tokens the shell parses.
This is a byte-level diagnosis, not a reason to blame an operating system. A repository can contain CRLF on any host, and a file created on Windows can already be LF-only. Check the affected file and the path that executes it before changing a whole checkout.
Recognize the signatures
/usr/bin/env: 'bash\r': No such file or directory
$'\r': command not found
syntax error near unexpected token `$'do\r''
In the first case, the kernel reads the shebang interpreter as bash\r, which is a different, nonexistent filename. Confirm the bytes instead of guessing:
file script.sh
sed -n '1,5l' script.sh
hexdump -C script.sh | head
sed -n l makes carriage returns visible as \r on many implementations.
Convert a copy, then verify
If the whole file is known to be text with CRLF endings:
tr -d '\r' <script.sh >script.fixed.sh
This simple command removes every carriage return, including any intentional one inside data. Tools such as dos2unix apply line-ending-aware conversions and are clearer when available. Compare the result, retain executable permissions as needed, and run the project’s tests before replacement.
Do not run a recursive conversion across a repository containing binary files. Select files by known type or let version-control attributes define the policy.
For an individual script, keep the original until the converted copy passes inspection. Check whether every CRLF pair is a line ending and whether standalone carriage-return bytes have a legitimate meaning before choosing a converter. A program that removes all carriage returns changes payload bytes too; a line-ending-aware conversion preserves unrelated data. Then inspect the first line, run a syntax check with the intended interpreter, and execute the project’s tests in a safe environment. For example, bash -n script.fixed.sh checks Bash syntax without running the script; it does not verify that the script’s commands will behave correctly.
Prevent recurrence with Git attributes
A repository can state that shell sources are normalized to LF:
*.sh text eol=lf
After adding .gitattributes, use Git’s documented renormalization workflow in a clean branch and inspect the resulting diff. Editor configuration should also save shell scripts as LF, but repository policy is the shared enforcement point.
The two attributes have related but different jobs: text enables normalization to LF in Git’s index, while eol=lf requests LF in the checked-out working tree. Git’s renormalization procedure uses git add --renormalize and then asks you to inspect git status; it can make broad changes, so start from a clean branch and review exactly which paths changed. Do not commit a repository-wide normalization merely because the command completed successfully. A local core.autocrlf setting or a more-specific .gitattributes pattern can affect the result, so inspect the actual attributes for the file.
Line endings are separate from executable permission and encoding. A converted script can still fail because its executable bit is absent, its shebang points to an unavailable interpreter, or it contains invalid syntax. Fix the confirmed byte-level problem, then diagnose any remaining error independently.
Diagnose the bytes, not the operating-system label
CR (0x0d) and LF (0x0a) are distinct control bytes. A CRLF file contains both before each line break; a mixed file may use different byte sequences on different lines. Do not assume every file from Windows has CRLF or every file from Unix has LF: editors, generators, archives, and Git attributes can all change the stored bytes. Inspect a small sample before choosing a conversion:
od -An -tx1 -c script.sh | head
git ls-files --eol -- script.sh
git check-attr text eol -- script.sh
In Git’s output, i/ describes the index copy and w/ the working-tree copy, which can differ under configured text conversion. If only some lines end in CRLF, a line-ending-aware converter can normalize those separators; tr -d '\r' is safe only when every CR byte in the file is unwanted. The conversion policy should be tested on the exact file, then checked in the resulting diff.
This check is especially useful when a script behaves differently locally and in CI. The index is what Git will commit; the working tree is what the current shell executes. A clean-looking editor buffer says little about either representation. If Git reports LF in the index and CRLF in the working tree, Git may normalize at commit while the local interpreter still reads CRLF. If both are LF but a remote checkout gets CRLF, examine attributes and Git configuration on that checkout rather than repeatedly converting the local copy.
Enforcing the same policy at the editor level, not just in Git
# .editorconfig
[*.sh]
end_of_line = lf
A supporting editor applies the .editorconfig rule when it saves a matching file; editors without EditorConfig support will not. Git attributes govern Git’s treatment of tracked content and checkout, but they do not force an editor to save a particular working-tree format before a commit. Using both addresses different points in the workflow, and Git’s index/working-tree inspection commands make it possible to verify the result rather than assume either policy took effect.
Keep line endings separate from other execution failures
After normalizing, confirm the shebang names an interpreter installed at that path, the file has the required execute permission if invoked directly, and the script parses under the intended shell. Do not replace a CRLF repair with chmod +x: permissions cannot remove a carriage return from the interpreter name. Likewise, converting endings will not make Bash arrays or another non-POSIX feature work under a smaller /bin/sh implementation. Diagnose one contract at a time: bytes, interpreter selection, file mode, then shell-language compatibility.
For a tracked file, a focused review can include git diff -- script.sh, git ls-files --eol -- script.sh, git check-attr text eol -- script.sh, and an interpreter syntax check. For an untracked generated file, use byte inspection and the generator’s documented line-ending setting; Git’s index report cannot describe a file that has not been added. These checks make the repair repeatable and avoid silently rewriting unrelated data.
Related:
- Fixing ‘Broken Pipe’ Errors in Shell Scripts and Pipelines
- Fixing a Shell Prompt That Hangs Inside Git Repositories
Sources: