Regeleindes CRLF en LF: wat er echt stukgaat
Het verschil tussen CRLF en LF is één byte. LF is een enkele \n (0x0A) en sluit regels af op Linux en macOS. CRLF is twee bytes, \r\n (0x0D 0x0A), en sluit regels af op Windows.
En dan het stuk waar de meeste artikelen de mist in gaan. Een shellscript met CRLF-regeleindes faalt meestal niet. Het draait, print wat je verwacht en eindigt met 0. Wat stukgaat, is de waarde in een variabele: de toekenning hield de \r aan het eind vast en niemand klaagde erover.
Het gemeten gedrag valt uiteen in vier vormen, op volgorde van hoe moeilijk ze te zien zijn:
- Stil succes. De output klopt, een variabele draagt een onzichtbare
\r, exitcode 0. - Een foutmelding die niets verandert.
: command not foundop een lege regel, het script draait door tot het eind, exitcode 0. - Syntaxfout.
if,foren functiedefinities gaan stuk, exitcode 2. bad interpreter. De shebang draagt de\rmee, exitcode 126.
Voer vóór alles file yourfile uit. Eén regel output vertelt je aan welke kant van CRLF en LF je zit.
Alles hieronder is gemeten op macOS (Darwin arm64) met bash 3.2.57(1)-release, zsh 5.9, dash, git 2.55.0, node v26.7.0 en Python 3.14.6. Dezelfde tests liepen op Linux via
docker run --rm bash:5, oftewel GNU bash 5.3.15(1)-release (aarch64-unknown-linux-musl).
1. CRLF, LF en CR: wat de bytes werkelijk zijn
CRLF is de afkorting van carriage return line feed, en dat zijn letterlijk twee tekens die aan elkaar geplakt zitten:
| Naam | Escape | Byte |
|---|---|---|
| Carriage return | \r | 0x0D |
| Line feed | \n | 0x0A |
| CRLF | \r\n | 0x0D 0x0A |
Wat een platform schrijft:
| Platform | Regeleinde |
|---|---|
| Windows | CRLF (\r\n) |
| Linux, moderne macOS | LF (\n) |
| Mac Classic, OS 9 en ouder | CR (\r) op zichzelf |
De namen komen uit de tijd van de teletype. Daar schoof carriage return de printkop terug naar de linkermarge en draaide line feed het papier één regel omhoog. Windows hield beide bewegingen aan als twee bytes; Unix vond er één genoeg. Een losse CR duikt nog altijd op in oude exports, en een bestand er vol mee ziet er voor de meeste Unix-tools uit als één enorme regel.
Eén commando vertelt je welke je hebt
file leest de bytes en benoemt de afsluiter:
$ file d1.txt
d1.txt: ASCII text, with CRLF line terminators
$ file d2.txt
d2.txt: ASCII text ← no suffix means pure LF
$ file d3.txt
d3.txt: ASCII text, with CRLF, LF line terminators ← mixed, file lists both
Onthoud vooral het derde geval. Een bestand met gemengde regeleindes betekent dat twee tools met verschillende instellingen er om beurten in hebben geschreven: een editor sloeg LF op, een script plakte er CRLF achter, een merge naaide twee versies aan elkaar. od -c laat het byte voor byte zien:
$ od -c d3.txt
0000000 a \r \n b \n
0000005
Regeleindes leven op de bytelaag, direct naast tekencodering; de gids over UTF-8 en UTF-16 behandelt wat er één laag hoger gebeurt.
2. De vier faalvormen, van stil succes tot exit 126
Hetzelfde \r\n-bestand, vijf verschillende uitkomsten afhankelijk van wat er op de regel staat:
| Inhoud van het script | Wat er werkelijk gebeurt | Exit |
|---|---|---|
echo hello en andere eenvoudige commando’s | Stil succes, de output klopt | 0 |
Toekenning X=abc | Stil succes, maar de waarde eindigt op \r | 0 |
Lege regels, regels die doorlopen met \ | : command not found, het script draait door tot het eind | 0 |
if/fi, for/do/done, f() { | syntax error near unexpected token | 2 |
Shebang-regel met \r | bad interpreter: No such file or directory | 126 |
De eerste twee rijen zijn de reden dat deze pagina bestaat. “CRLF veroorzaakt command not found” wordt overal herhaald, en zo werkt het niet. Eenvoudige commando’s klagen helemaal niet.
Stil succes is het gevaarlijke geval
$ printf 'echo hello\r\necho world\r\n' > win.sh && bash win.sh
hello
world
[exit=0]
Twee regels erin, twee regels eruit, exit 0. Niets om te debuggen en niets om met grep in het log op te zoeken. Geef hetzelfde script een vergelijking om uit te voeren en het slaat om:
$ printf 'V=1.2.3\r\ntest "$V" = "1.2.3" && echo MATCH || echo NO-MATCH\r\n' > v.sh
$ bash v.sh
NO-MATCH
[exit=0]
$V bevat 1.2.3\r, niet 1.2.3. Op macOS en Linux hetzelfde resultaat. Versiecontroles, if [ "$ENV" = "prod" ], feature flags die uit een bestand komen: ze kiezen stuk voor stuk de verkeerde tak, geruisloos, met exitcode 0. Dit is de lastigste bug rond regeleindes om in CI te vinden, want de build is groen en het log is schoon.
De foutmelding die niets tegenhoudt
Een regel die alleen \r bevat, is hoe een lege regel eruitziet in een CRLF-bestand, en de shell behandelt hem als een uit te voeren commando. Dat mislukt, er verschijnt een melding, en het script gaat verder met de volgende regel en eindigt met exit 0. De exacte formulering hangt af van je shell; daarover gaat de volgende paragraaf.
Regels die met een backslash doorlopen, gaan op dezelfde manier stuk. De \r zit tussen de backslash en de newline, waardoor de voortzetting geen voortzetting meer is en de volgende regel op zichzelf wordt uitgevoerd.
Syntaxfouten, en waarom fi geen fi is
c_if.sh: line 5: syntax error: unexpected end of file
[exit=2]
Linux bash 5.3.15 zegt meer over hetzelfde bestand:
i.sh: line 5: syntax error: unexpected end of file from `if' command on line 2
Lussen en functiedefinities falen juist op regel 1:
c_for.sh: line 1: syntax error near unexpected token `do'
c_for.sh: line 1: `for i in 1 2; do'
c_func.sh: line 1: syntax error near unexpected token `{'
c_func.sh: line 1: `f() {'
Het mechanisme maakt deze hele klasse voorspelbaar. Bash leest het afsluitende woord als fi\r, niet als fi. fi\r is niet het sleutelwoord fi, dus het if-blok sluit nooit en bash blijft doorlezen tot het bestand op is. Daarom wijst de fout naar de laatste regel in plaats van naar de kapotte regel.
bad interpreter en exit 126
$ ./s.sh
bash: ./s.sh: /bin/bash^M: bad interpreter: No such file or directory
[exit=126]
macOS en Linux printen dezelfde tekst en eindigen allebei met 126. Lees het pad in de melding: /bin/bash^M. De kernel neemt alles na #! tot aan de newline als het pad naar de interpreter, en de \r hoort daarbij. Zo’n bestand bestaat niet, dus de exec mislukt voordat ook maar één regel van je script draait.
Exit 126 dekt ook “gevonden, maar niet uitvoerbaar”, dus een script dat weigert te starten is niet automatisch een probleem met regeleindes; bestandsrechten leveren fouten uit dezelfde familie op. De ^M in het pad is wat de twee uit elkaar houdt.
Waarom je de \r nooit ziet
De output lezen helpt niet: de byte laat niets zien en je kunt hem niet eens selecteren. Je moet hem zichtbaar maken:
$ bash d.sh # script: HOST=example.com / echo "connecting to $HOST:8080"
connecting to example.com:8080 ← what you get
$ bash d.sh | cat -v
connecting to example.com^M:8080^M ← what is actually there
$ bash d.sh | od -c
0000000 c o n n e c t i n g t o e x
0000020 a m p l e . c o m \r : 8 0 8 0 \r
0000040 \n
Twee \r-bytes, onzichtbaar in normale output, allebei binnen een string die zo meteen dienstdoet als hostnaam. Soms reist de byte mee in een argument en krijgt een tool die er niets mee te maken heeft de schuld:
$ bash v.sh # the script pipes through: ... | head -2
head: illegal line count -- 2\r
head doet precies wat het moet doen. Het script gaf het 2\r mee. Elke foutmelding met een verdwaalde \r in de geciteerde waarde is deze bug onder andermans naam.
3. Waarom jouw foutmelding niet lijkt op die van internet
Eén bestand (printf 'echo a\r\n\r\necho b\r\n', waarin regel 2 een lege regel is die alleen \r bevat), uitgevoerd onder vier shells:
| Shell | Versie | Exacte melding |
|---|---|---|
| bash (meegeleverd met macOS) | 3.2.57(1)-release | s_blank.sh: line 2: : command not found |
| bash (gangbare Linux) | 5.3.15(1)-release | t.sh: line 2: $'\r': command not found |
| zsh | 5.9 | s_blank.sh:2: command not found: ^M |
| dash | — | s_blank.sh: 2: : not found |
Bijna elk zoekresultaat citeert de tweede rij. Bash 4 en 5 printen niet-afdrukbare tekens met ANSI-C quoting, waardoor de carriage return $'\r' wordt. De bash die met macOS meekomt is 3.2 en doet dat niet, dus krijg je een dubbele punt, een spatie en niets ertussen. zsh print ^M. dash laat het woord “command” helemaal weg.
Eén fout, vier meldingen. Als je jouw exacte foutmelding in een zoekbalk plakte en er niets bruikbaars terugkwam, is dat de reden. Het kale : command not found is dezelfde bug als de beroemde variant.
4. Git: wat core.autocrlf echt in je repository opslaat
De meeste uitleg over git autocrlf houdt op bij de definities. Interessanter is wat er in de commit belandt, en wat teamgenoten bij een checkout krijgen. Gemeten met git cat-file -p HEAD:f.txt voor de opgeslagen blob en rm f.txt && git checkout -- f.txt voor de werkkopie:
core.autocrlf | Bronbestand | Blob in de repository | Werkkopie na checkout |
|---|---|---|---|
true | CRLF | LF | CRLF |
true | LF | LF | CRLF |
input | CRLF | LF | LF |
input | LF | LF | LF |
false | CRLF | CRLF | CRLF |
false | LF | LF | LF |
Drie dingen vallen op in die tabel:
trueeninputgaranderen allebei LF in de repository. Het enige verschil zit in de checkout:truezet terug om naar CRLF,inputlaat het bestand met rust.- Alleen
falsezet CRLF in een commit. Als iemand vraagt wie die carriage returns heeft gecommit, is deze rij het antwoord. - Rij twee is de verrassing. Onder
truekomt een bestand dat LF op schijf stond, na checkout terug als CRLF.
Git kondigt de herschrijving aan voordat hij plaatsvindt, in een van twee vormen:
warning: in the working copy of 'f.txt', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'f.txt', CRLF will be replaced by LF the next time Git touches it
“Ik heb niets veranderd, maar git zegt dat het hele bestand is gewijzigd”
Weer rij twee. Met git config core.autocrlf true herschrijft de checkout LF-bestanden naar CRLF in de werkmap. Elke regel verschilt nu één byte van de blob, dus git diff meldt elke regel als gewijzigd en de pull request toont een bestand waar niemand aan gezeten heeft als volledig herschreven. Het checkout-filter heeft het gedaan.
Het spiegelbeeld levert dezelfde ruis op: een teamgenoot op false commit CRLF, jij zit op input, en bestanden die je nooit hebt geopend duiken op in je diff.
5. .gitattributes is het antwoord op teamniveau
core.autocrlf is een instelling op één machine, onzichtbaar voor alle anderen. .gitattributes is een bestand in de repository en reist dus mee met elke clone. Als de twee elkaar tegenspreken, winnen de attributen. Alle vier de vormen getest:
.gitattributes | core.autocrlf | Bron | Blob | Na checkout | Winnaar |
|---|---|---|---|---|---|
* text=auto | false | CRLF | LF | LF | attributen |
* text eol=crlf | input | LF | LF | CRLF | attributen |
* -text | true | CRLF | CRLF | CRLF | attributen |
* text eol=lf | true | CRLF | LF | LF | attributen |
Rij drie is het onthouden waard: -text schakelt conversie volledig uit en verslaat nog steeds core.autocrlf=true. Zo bescherm je bestanden waarvan de bytes ongeschonden moeten blijven.
Een .gitattributes die je kunt kopiëren
* text=auto
*.sh text eol=lf
*.bash text eol=lf
Makefile text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.png -text
*.jpg -text
*.pdf -text
*.zip -text
text=auto normaliseert alles wat Git als tekst herkent naar LF in de repository. De expliciete eol=lf-regels dekken bestanden die LF moeten zijn, ongeacht wie ze schreef, want een .sh met CRLF is een exit 126 die staat te wachten. Scripttypen van Windows krijgen eol=crlf om precies de omgekeerde reden, en binaire patronen krijgen -text zodat er helemaal niets omgezet wordt.
core.safecrlf en core.eol
Twee instellingen die naast core.autocrlf opduiken en iets anders doen:
core.safecrlfis een bewaker, geen omzetter. Als een conversie niet heen en terug hetzelfde oplevert (denk aan een gemengd bestand, waar normaliseren informatie kost), weigerttruede operatie en laatwarnhem door met een waarschuwing. Het verandert nooit welke bytes worden opgeslagen; het zorgt er alleen voor dat je zo’n conversie niet ongemerkt uitvoert.core.eolbepaalt welk regeleinde Git in de werkmap schrijft voor bestanden die alstextzijn gemarkeerd, wanneercore.autocrlfopfalsestaat. De waarden zijnlf,crlfennative.core.autocrlfoverschrijft het, en daarom lijktcore.eolinstellen op een machine waar autocrlf nog aanstaat meestal geen enkel effect te hebben.
.gitattributes aanpassen repareert de al gecommitte bestanden niet
Attributen gelden op het moment dat Git een bestand schrijft of leest, dus bestaande inhoud blijft zoals hij is totdat iets hem herschrijft. Forceer die ronde zelf:
$ git add --renormalize .
$ git commit -m "Normalize line endings"
Verwacht één enorme diff; dat is precies de bedoeling. Doe het op een eigen branch, voeg het samen in één commit en waarschuw iedereen voordat het binnenkomt.
6. CRLF omzetten naar LF, en terug
Vier manieren, alle vier op Darwin getest, en alle vier halen ze de \r weg:
| Commando | Resultaat |
|---|---|
tr -d '\r' < f > f.out | werkt |
perl -pi -e 's/\r\n/\n/g' f | werkt |
sed -i '' -e 's/\r$//' f (BSD-vorm) | werkt |
sed -i -e 's/\r$//' f (GNU-vorm, uitgevoerd op macOS) | werkt, en laat rommel achter |
De valkuil van GNU sed -i op macOS
BSD sed eist dat er na -i een achtervoegsel voor de back-up volgt. Kopieer je een Linux-tutorial letterlijk, dan slikt BSD sed de -e op als dat achtervoegsel. Je bewerking gaat gewoon door, en er blijft iets achter:
a5.txt
a5.txt-e
a5.txt-e is een back-upkopie, ontstaan doordat sed de -e las als het achtervoegsel waar je om vroeg. De juiste macOS-vorm geeft een expliciete lege string mee: sed -i '' -e 's/\r$//' f. Een repository met -e-bestanden erin gecommit is een repository waar iemand een GNU-oneliner op een Mac heeft uitgevoerd.
macOS heeft geen dos2unix
command -v dos2unix geeft niets terug op een kale macOS-installatie; de binary komt van brew install dos2unix. Daarom faalt het meest gekopieerde antwoord op internet juist op de machine waarop veel developers zitten te typen. tr -d '\r' heeft geen installatie nodig en doet hetzelfde werk.
De andere kant op heeft unix2dos hetzelfde beschikbaarheidsprobleem, en sed -e 's/$/\r/' werkt als vervanging.
Zodra een bestand weer schone LF heeft, kun je het veilig als een lijst regels behandelen. Dat telt voor alles wat regels als hele strings vergelijkt: tekstregels sorteren en dubbele regels verwijderen lezen value\r en value als twee verschillende regels, dus één verdwaalde carriage return maakt het ontdubbelen geruisloos ongedaan.
In je editor
VS Code toont het regeleinde van het huidige bestand als CRLF of LF in de statusbalk rechtsonder, en een klik erop wisselt het bestand om. De instelling files.eol bepaalt de standaard voor nieuwe bestanden, en die per workspace vastleggen houdt een gemengd team consistent. Andere editors bieden dezelfde twee knoppen onder andere namen. Wat mensen missen, is dat de indicator per bestand en de standaardinstelling los van elkaar staan: de een wijzigen raakt de ander niet.
7. Regeleindes afhandelen in code
Eén bestand, line1\r\nline2\r\n, gelezen via zeven ingangen:
| Ingang | Wat je krijgt | \r |
|---|---|---|
Node fs.readFileSync(f,"utf8") | "line1\r\nline2\r\n" | behouden |
Node, dezelfde waarde + .split("\n") | ["line1\r","line2\r",""] | op elke regel behouden |
Node readline met crlfDelay:Infinity | ["line1","line2"] | weggehaald |
Python open(f), standaardmodus | 'line1\nline2\n' | omgezet |
Python open(f).readlines() | ['line1\n','line2\n'] | omgezet |
Python open(f, newline="") | 'line1\r\nline2\r\n' | behouden |
Python open(f,"rb") | b'line1\r\nline2\r\n' | behouden |
Die tabel beslecht een bekend bugrapport: het werkt in Python en gaat stuk in Node. De standaard tekstmodus van Python past universal newlines toe en vertaalt \r\n naar \n voordat jij het ooit ziet. Node geeft je de bytes zoals ze zijn. Geen van beide is fout, maar ze spreken elkaar tegen zodra ze hetzelfde bestand lezen.
rstrip("\n") laat de \r staan
original: 'line1\r\n' | rstrip("\n"): 'line1\r' | strip(): 'line1'
rstrip("\n") verwijdert precies de tekens die je opsomde, en \r stond niet in die lijst. Het resultaat is daarna ongelijk aan alles waarmee het zou moeten overeenkomen, en dat is het eerlijke antwoord op “ik heb het weggehaald en het is nog steeds niet gelijk”. Gebruik strip(), of rstrip() zonder argument, dan verdwijnt alle witruimte aan het eind, de carriage return incluis.
Regels veilig splitsen op beide platforms
Splits in JavaScript op een patroon dat beide regeleindes verdraagt: text.split(/\r?\n/). Blijf in Python in de standaard tekstmodus en laat universal newlines het regelen, of roep splitlines() aan, dat overweg kan met \r\n, \n en een losse \r.
Schrijven is de andere helft. Node schrijft de bytes die je aanlevert, dus bouw je strings met \n en laat .gitattributes bepalen wat er op schijf belandt. open(path, "w") in Python vertaalt \n naar het regeleinde van het platform, tenzij je newline="" meegeeft. Dat is precies de vlag waar CSV-writers om vragen, en om exact deze reden.
Een carriage return aan het eind van een regel en een byte-order mark aan het begin van een bestand zijn dezelfde soort bug vanaf tegenovergestelde kanten: een onzichtbare byte die kopiëren en plakken overleeft en een gelijkheidscontrole breekt. De gids voor UTF-8 BOM-problemen behandelt die kant.
8. Waar regeleindes nog meer bijten
CSV en Excel. RFC 4180 schrijft CRLF voor als recordscheiding, waardoor CSV een van de weinige plekken is waar CRLF juist is in plaats van een defect. Parsers die alleen op LF-input zijn geschreven laten op het laatste veld van elke rij een \r staan, dus als waarden uit een conversie met CSV naar JSON er goed uitzien maar verkeerd vergelijken, controleer dan eerst die byte.
Docker. Een .sh met CRLF die je in een image kopieert, is faalvorm vier. COPY behoudt de bytes, de shebang houdt zijn \r, en de container eindigt met 126. Eén regel *.sh text eol=lf in .gitattributes voorkomt de hele klasse.
Diffs en pull requests. Een bestand dat als volledig gewijzigd wordt gemarkeerd zonder zichtbare bewerking is het mechanisme uit paragraaf 4 dat in de code review opduikt. De twee versies vergelijken met text diff bevestigt het in seconden, en de gids over text diff legt uit hoe je het resultaat leest.
Gemengde bestanden. ASCII text, with CRLF, LF line terminators betekent twee schrijvers met verschillende instellingen. Normaliseer het hele bestand in plaats van alleen de regels die je toevallig bewerkte, anders is de volgende diff net zo rumoerig.
FAQ
Wat is het verschil tussen CRLF en LF?
CRLF is twee bytes, \r\n (0x0D 0x0A). LF is één byte, \n (0x0A). Windows schrijft CRLF, Linux en macOS schrijven LF, en allebei markeren ze het einde van een regel. In een editor ziet de tekst er identiek uit; het verschil merk je pas in bytes, stringvergelijkingen en diffs.
Mijn script heeft CRLF-regeleindes. Waarom krijg ik geen foutmelding?
Omdat eenvoudige commando’s die extra byte overleven. echo hello met een \r erachter draait en eindigt met 0. Fouten verschijnen alleen waar de parser er iets om geeft: een lege regel, een sleutelwoord als fi, of de shebang. Toekenningen zijn het gevaarlijke geval, want die slagen en bewaren de \r in de variabele.
Wat betekent $'\r': command not found, en waarom zie ik het niet?
Het betekent dat de shell een regel probeerde uit te voeren die alleen een carriage return bevat. Bash 4 en 5 printen dat teken met ANSI-C quoting, wat $'\r' oplevert. De bash 3.2 die met macOS meekomt print niets tussen de dubbele punten, en zsh print in plaats daarvan ^M. Eén fout, drie verschillende meldingen.
Moet core.autocrlf op true, input of false staan?
Gebruik input op Linux en macOS, true op Windows, en geef aan .gitattributes de voorkeur boven allebei. In de metingen slaan true en input allebei LF op in de repository, en laat alleen false CRLF toe in een commit. true herschrijft daarbovenop LF-bestanden naar CRLF in je werkkopie bij een checkout.
Als .gitattributes en core.autocrlf elkaar tegenspreken, wie wint er?
.gitattributes wint. Alle vier de geteste vormen (* text=auto, * text eol=crlf, * -text en * text eol=lf) overschreven de lokale waarde van core.autocrlf. Dat is het argument om het te gebruiken: attributen worden gecommit en gelden voor iedereen, terwijl core.autocrlf een instelling per machine is die je niet kunt zien of afdwingen.
Hoe controleer ik of een bestand CRLF of LF gebruikt?
Voer file yourfile uit. Bij CRLF verschijnt ASCII text, with CRLF line terminators, bij kale LF verschijnt ASCII text zonder enig achtervoegsel, en bij een gemengd bestand with CRLF, LF line terminators. Wil je zekerheid op byteniveau, voer dan od -c uit en zoek naar een \r vlak voor elke \n.
Wanneer moet je CRLF juist wel gebruiken?
Wanneer een formaat of protocol het vereist. RFC 4180 definieert CRLF als de recordscheiding voor CSV, en HTTP-headers en SMTP doen hetzelfde op de lijn. Batch- en PowerShell-bestanden van Windows zijn ook veiliger met CRLF. Overal elders is LF de juiste keuze: broncode, shellscripts, configuratie.