The two characters
LF is U+000A, one byte, 0A. CR is U+000D, one
byte, 0D. CRLF is the two of them in that order,
0D 0A — a sequence, not a character, which is why it has no code point of its
own. All three mean "start a new line"; the disagreement is purely about spelling.
The Unicode Standard uses the term newline function for the abstract operation and tabulates which characters represent it on which platform. Quoted from section 5.8 of the core specification, Table 5-2, read directly on unicode.org:
| Platform | Newline function | Bytes |
|---|---|---|
| MacOS 9.x and earlier | CR | 0D |
| MacOS X | LF | 0A |
| Unix | LF | 0A |
| Windows | CRLF | 0D 0A |
| EBCDIC-based OS | NEL | C2 85 (as UTF-8) |
A note on where CRLF came from
The usual explanation is mechanical: a carriage return moved the print head back to the left margin and a line feed advanced the paper by one line, so a teleprinter needed both instructions and the two-character sequence outlived the machines that required it.
That account is not sourced on this page. The Unicode newline section, which is the authority that was opened, tabulates the platform conventions and says nothing about where they came from, and we did not open a primary document that does. Take it as the received explanation, not as something this site checked.
What eleven tools on this machine actually wrote
Every writer below was asked for the same thing: the two lines alpha and
beta. The bytes column is what came back off the disk. Nothing is inferred —
each row was run 5 times and produced byte-identical output every time.
| Tool | Command | Bytes on disk | Endings |
|---|---|---|---|
| Node 22 · fs.writeFileSync No translation layer: the bytes you pass are the bytes on disk. | fs.writeFileSync(f, 'alpha\nbeta') | 61 6C 70 68 61 0A 62 65 74 61 10 bytes, no trailing terminator | LF×1 |
| Node 22 · join(os.EOL) os.EOL is the platform constant; on Windows it is CRLF. | fs.writeFileSync(f, ['alpha','beta'].join(os.EOL)) | 61 6C 70 68 61 0D 0A 62 65 74 61 11 bytes, no trailing terminator | CRLF×1 |
| Python 3.11 · text mode (default) Default newline=None translates every \n to os.linesep on write. | open(f,'w').write('alpha\nbeta') | 61 6C 70 68 61 0D 0A 62 65 74 61 11 bytes, no trailing terminator | CRLF×1 |
| Python 3.11 · newline="" Translation disabled — the documented way to keep LF on Windows. | open(f,'w',newline='').write('alpha\nbeta') | 61 6C 70 68 61 0A 62 65 74 61 10 bytes, no trailing terminator | LF×1 |
| Python 3.11 · binary mode Binary mode never translates, on any platform. | open(f,'wb').write(b'alpha\nbeta') | 61 6C 70 68 61 0A 62 65 74 61 10 bytes, no trailing terminator | LF×1 |
| PowerShell 7 · Set-Content (array) Each array element becomes a line, and a trailing terminator is added. | Set-Content f -Value @('alpha','beta') | 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A 13 bytes, ends with a terminator | CRLF×2 |
| PowerShell 7 · Set-Content (one string with LF) A single string is written as-is — the LF inside it is NOT converted. | Set-Content f -Value "alpha`nbeta" | 61 6C 70 68 61 0A 62 65 74 61 0D 0A 12 bytes, ends with a terminator | CRLF×1 + LF×1 |
| PowerShell 7 · Out-File The pipeline formats each object on its own line before writing. | @('alpha','beta') | Out-File f | 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A 13 bytes, ends with a terminator | CRLF×2 |
| PowerShell 7 · > redirection Redirection is Out-File under a different spelling. | @('alpha','beta') > f | 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A 13 bytes, ends with a terminator | CRLF×2 |
| cmd.exe · echo redirection The classic batch idiom, still the default in .bat files. | (echo alpha& echo beta) > f | 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A 13 bytes, ends with a terminator | CRLF×2 |
| Git Bash · printf A POSIX shell on Windows writes POSIX endings. | printf 'alpha\nbeta' > f | 61 6C 70 68 61 0A 62 65 74 61 10 bytes, no trailing terminator | LF×1 |
Three things in that table are worth more than the headline. 4 of the
11 writers produced plain LF on Windows, which is the point:
the line ending is a property of the program, not of the operating system. Python's default
text mode translated every \n into CRLF while its binary mode and its
newline="" mode did not — one language, three behaviours, all in the
same interpreter.
And one command produced a mixed file.
Set-Content f -Value "alpha`nbeta" wrote 61 6C 70 68 61 0A 62 65 74 61 0D 0A — CRLF×1 + LF×1. The LF inside the string was passed through untouched and a CRLF terminator was appended
after it, so a single command with no obvious ambiguity produced a file with two different
line endings in it. Mixed files are not exotic; they are one keystroke away.
What git does, measured
Git's documentation describes core.autocrlf like this — quoted from the
git-config documentation shipped inside Git for Windows 2.51.0.windows.1, read
directly from the installed distribution:
Setting this variable to "true" is the same as setting the text attribute to "auto" on all files and core.eol to "crlf". Set to true if you want to have CRLF line endings in your working directory and the repository has LF line endings. This variable can be set to input, in which case no output conversion is performed.
A description of a conversion is not the same thing as watching one happen, so the table
below is a measurement. For each setting, a throwaway repository was created, one file
authored with LF and one with CRLF, both staged and committed; then the bytes git stored in
the blob were read back with git cat-file, the working copies deleted, and the
files checked out again.
core.autocrlf | You wrote | Stored in the commit | Checked back out | Round trip |
|---|---|---|---|---|
| false | LF 61 6C 70 68 61 0A 62 65 74 61 0A | LF×2 61 6C 70 68 61 0A 62 65 74 61 0A | LF×2 61 6C 70 68 61 0A 62 65 74 61 0A | identical |
| false | CRLF 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A | CRLF×2 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A | CRLF×2 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A | identical |
| true | LF 61 6C 70 68 61 0A 62 65 74 61 0A | LF×2 61 6C 70 68 61 0A 62 65 74 61 0A | CRLF×2 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A | changed |
| true | CRLF 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A | LF×2 61 6C 70 68 61 0A 62 65 74 61 0A | CRLF×2 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A | identical |
| input | LF 61 6C 70 68 61 0A 62 65 74 61 0A | LF×2 61 6C 70 68 61 0A 62 65 74 61 0A | LF×2 61 6C 70 68 61 0A 62 65 74 61 0A | identical |
| input | CRLF 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A | LF×2 61 6C 70 68 61 0A 62 65 74 61 0A | LF×2 61 6C 70 68 61 0A 62 65 74 61 0A | changed |
Read the round-trip column first. 2 of the
6 combinations hand you back a different file from the one you
committed, and both are working exactly as documented. Neither is a bug.
With autocrlf=true, a file you authored with LF comes back as CRLF — git
normalised it to LF in the object database and then converted it on the way out, which is
the whole design. With autocrlf=input, a file you authored with CRLF comes back
as LF, because input normalises on the way in and performs no conversion on the way out.
With autocrlf=false, both files round-trip exactly: git stores the bytes you
gave it and returns them. That is why false plus a
.gitattributes file is the configuration most projects settle on — it moves the
decision from each developer's machine into the repository, where it can be reviewed.
Git also warns about the failure mode that measurement cannot show, because it needs a file that is already mixed:
CRLF conversion bears a slight chance of corrupting data. When it is enabled, Git will convert CRLF to LF during commit and LF to CRLF during checkout. A file that contains a mixture of LF and CRLF before the commit cannot be recreated by Git.
That is the practical reason to care. A mixed file — of which this page has already shown one being produced by a single innocuous command — cannot survive normalisation intact, because normalisation has no way to know which of your CRLFs were deliberate.
Which to choose, in practice
- Source code. LF in the repository, always. Set
core.autocrlf=falseand commit a.gitattributeswith* text=auto eol=lfso the decision travels with the project. - Data files consumed by scripts. LF. A trailing CR on the end of every field is exactly the kind of difference that makes a parser work on one machine and fail on another.
- Files that must satisfy a protocol. CRLF, because the protocol says so — HTTP/1.1 header lines under RFC 9112, CSV records under RFC 4180.
- Plain text for Windows users to open. CRLF is the safer default; some long-standing Windows tools still render an LF-only file as one long line.
- Anything you are about to diff. Whatever the other side uses. A whole-file diff caused by an ending change hides the one line you actually edited.
Fixing a file you already have
Paste it into the tool at the top of this page, which opens in ending-conversion mode with LF selected, and nothing but the bytes between lines will change. The byte-level table for every input and target — including what happens to NEL and U+2028 on the way — is on the conversion page. If what you actually have is a file that looks double-spaced, check for orphan carriage returns before deleting anything: that is a line-ending problem, and removing the blank lines would destroy the file it was meant to fix.