CRLF vs LF - What Line Endings Actually Break

The same text file opens as a single line on Windows, produces a Git diff covering every line, or refuses to run with bad interpreter. In most cases the cause is the same: line endings. Once you understand CRLF and LF, all of these collapse into one problem.

Key point: The characters that represent a newline differ by platform. Windows uses CRLF (two bytes); macOS and Linux use LF (one byte). They look identical on screen but are different bytes.

What CR and LF originally meant

Mechanical terminals needed both actions, so both bytes were sent. Windows inherited that pair as CRLF. Unix decided LF alone was enough. Neither is more correct; it is a historical split.

NameBytesWhere it is used
LF0ALinux, macOS, most programming languages
CRLF0D 0AWindows, HTTP headers, many network protocols
CR0DMac OS 9 and earlier (rare today)

Where it actually breaks

A shell script will not run

$ ./deploy.sh
bash: ./deploy.sh: /bin/bash^M: bad interpreter: No such file or directory

The first line is #!/bin/bash\r rather than #!/bin/bash, so the system looks for an executable named /bin/bash\r. The ^M in the message is the giveaway.

Git reports every line as changed

You changed nothing, yet the diff covers the whole file. An editor normalized the line endings on save. Diffs compare line by line, so changing every line ending marks every line as modified, which makes review impossible.

Invisible characters in config values

Saving a .env file as CRLF appends a carriage return to each value. PORT=8080\r does not compare equal to 8080. The configuration looks right but the program misbehaves, and nothing in the file shows why.

How to inspect and convert

# Identify the current line endings
$ file deploy.sh
deploy.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators

# Look at the bytes directly
$ head -1 deploy.sh | od -c | head -2
0000000   #   !   /   b   i   n   /   b   a   s   h  \r  \n
# Convert CRLF to LF
$ sed -i 's/\r$//' deploy.sh

# Or use dos2unix if available
$ dos2unix deploy.sh

Prevent recurrence in Git

# .gitattributes
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.png binary

text=auto eol=lf normalizes anything Git detects as text to LF in the repository. Windows batch files need CRLF, so they are listed as explicit exceptions. core.autocrlf is a per-user setting, and .gitattributes in the repository takes precedence. Use .gitattributes when you need the whole team to agree.

Note: Adding .gitattributes to an existing repository will show every file as modified once. Make that normalization its own commit so the history stays readable.

Frequently Asked Questions

Should I use CRLF or LF?

Standardize on LF inside the repository. Windows editors often save as CRLF, so most teams add a .gitattributes file with text=auto eol=lf so that committed content is normalized to LF. Keep exceptions for files that require CRLF, such as Windows batch files.

Why does my shell script fail with bad interpreter?

Because the file is saved with CRLF, the shebang line ends up as #!/bin/bash followed by a carriage return. The system then looks for an executable literally named /bin/bash\r, which does not exist. Converting the file to LF fixes it.

Why does Git show every line as changed?

Because the line endings were converted across the whole file. Diffs compare line by line, so if every line ending changes, every line is reported as modified even though the content is identical. Check for a line ending conversion before committing.

← Back to the tech blog index