Converting Line Endings: CRLF, LF, and the Invisible Carriage Return
Why CRLF and LF line endings break shell scripts, bloat git diffs, and confuse editors, plus how to normalize newlines locally and lock the rule in with .gitattributes.
Converting Line Endings: CRLF, LF, and the Invisible Carriage Return
A text file looks the same in your editor whether it ends each line with one byte or two. That is the trap. Windows ends a line with CRLF, which is a carriage return followed by a line feed (\r\n, bytes 0x0D 0x0A). Unix, Linux, and modern macOS end a line with a single LF (\n, byte 0x0A). Old classic Mac OS used a bare CR (\r). The characters that separate your lines are invisible, but the bytes underneath are not, and the difference between one byte and two is enough to break a shell script, confuse a parser, or turn a one-line fix into a 400-line git diff.
CRLF, LF, and CR: what the bytes actually are
The naming goes back to teletype machines. A carriage return moved the print head back to the left margin; a line feed advanced the paper by one line. Early systems disagreed about which character, or which pair, marks the end of a line, and that disagreement never fully went away.
So today you have three conventions in active use:
- CRLF (
\r\n) on Windows, in many HTTP headers, and in some CSV exports. - LF (
\n) on Linux, macOS, and almost everything a developer touches: code, JSON, Markdown, YAML, shell scripts. - CR (
\r) alone, a relic of pre-2002 Macs that still shows up in files exported by old tools.
When a file moves between editors, operating systems, and export pipelines, these styles get mixed inside a single file. One block of lines ends in LF, another in CRLF, because someone edited the file on a different machine than the one that created it. Mixed endings are where the real pain starts.
The invisible carriage return that breaks your script
Here is the concrete failure that sends most people looking for a fix. You write a shell script on Windows, save it, and copy it to a Linux server. You run it and get something like:
./deploy.sh: line 2: $'\r': command not found
Or worse, a variable that should hold production actually holds production\r, so a path check silently fails and your script deploys nothing. The reason is that Windows saved every line with a trailing \r. On Unix, \n ends the line and the \r is left dangling as the last character of the line content. The shell tries to execute it.
You usually cannot see the \r at all. In vim it surfaces as a red ^M at the end of each line. In git diff or cat -A it shows up as ^M too. That ^M is the carriage return your editor hid from you. Until you remove it, the file is subtly wrong in a way no amount of staring at the source will reveal.
A worked example
Take a file count.sh saved on Windows. On disk, line one is #!/bin/bash\r\n and line two is for i in 1 2 3; do echo $i; done\r\n. Run it on Linux and bash reads the shebang as #!/bin/bash\r, looks for an interpreter literally named bash\r, and fails with bad interpreter: No such file or directory.
Now convert the file from CRLF to LF. Every \r\n becomes a plain \n, the shebang becomes a clean #!/bin/bash, and the loop body loses its trailing carriage returns. Run it again and it prints 1, 2, 3. Nothing about the visible text changed. Only the line-ending bytes changed, and that was the entire bug. You can do exactly this conversion in the browser with the Text File Line Ending Converter: paste the script or upload the file, pick LF, and copy the cleaned result. The conversion runs locally, so the file never leaves your machine.
Why mixed endings bloat git diffs
The second cost is noisier and slower to notice. Suppose a repository was created on macOS with LF endings, and a teammate on Windows opens a file, changes one word, and their editor rewrites every line with CRLF on save. Git compares bytes, not glyphs. To git, every single line changed, because every line gained a \r. Your one-word fix becomes a diff that touches the whole file.
That does three bad things. Code review becomes impossible, because the reviewer cannot find the real change in a wall of whitespace-only edits. git blame becomes useless, because the last person to touch each line is now whoever flipped the endings. And merge conflicts multiply, because two branches that touched the same file with different endings collide on lines neither developer meaningfully changed.
Normalizing every file to one style, almost always LF, makes this disappear. When everyone commits LF, a one-word change is a one-line diff again.
Lock it in with .gitattributes
Converting a file once fixes that file. To stop the problem coming back, tell git the rule with a .gitattributes file at the repository root:
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.png binary
The first line says: treat files as text and normalize them to LF in the repository, regardless of what the contributor's machine uses on checkout. The .bat and .cmd lines are the important exception, because Windows batch files genuinely can need CRLF to run, so you pin those to CRLF. The binary line keeps git from ever touching your images.
After adding .gitattributes, run git add --renormalize . once and commit. From then on, anyone who clones the repo gets consistent endings no matter their OS, and the editor-flips-everything diff stops happening. The .gitattributes rule is the durable fix; the converter is what you reach for on files that already went wrong, or on text that is not in a git repo at all, like a one-off CSV export or a log file pasted from a Windows box.
My own version of this
I lost an afternoon to this exact problem early in my career. A cron job on a Linux box silently stopped working, no error in the obvious logs, the script looked perfect when I opened it. I had edited it once from a Windows laptop over SCP, and my editor helpfully converted the whole thing to CRLF on save. The job had been failing on a \r in a path comparison for days before anyone noticed the output was stale. The fix was three seconds of converting CRLF to LF; the diagnosis took three hours because the bug was literally invisible in every tool I trusted. Now the first thing I check when a working script suddenly stops is cat -A for a trailing ^M.
One rule per project, then automate it
The practical advice is short. Pick LF for almost everything. Reserve CRLF for the narrow cases that demand it, mainly Windows batch files. Add a .gitattributes file so the rule enforces itself on every checkout. And when you hit a file that is already a mess of mixed endings, normalize it in one pass instead of hand-editing.
Cleaning up line endings often comes alongside other text tidying. If the file picked up duplicate rows from a merge, run it through the Text Deduplicator afterward so the cleanup is a single quick chore rather than a recurring headache. Line endings are a small detail, but they sit underneath everything, and getting them consistent removes a whole category of bugs you would otherwise spend afternoons chasing.
Made by Toolora · Updated 2026-06-13