Znaki końca linii CRLF i LF: co naprawdę się psuje
Różnica między CRLF a LF to jeden bajt. LF jest pojedynczym \n (0x0A) i kończy linie w Linuksie oraz macOS. CRLF ma dwa bajty, \r\n (0x0D 0x0A), i kończy linie w Windows.
W tym miejscu większość artykułów się myli. Skrypt powłoki z zakończeniami CRLF zwykle nie kończy się błędem. Uruchamia się, wypisuje dokładnie to, co powinien, i wychodzi z kodem 0. Psuje się wartość siedząca w zmiennej: przypisanie zachowało końcowy \r, a nic tego nie zgłosiło.
Zmierzone zachowanie rozpada się na cztery tryby, uszeregowane według tego, jak trudno je zauważyć:
- Ciche powodzenie — wynik wygląda poprawnie, zmienna niesie niewidoczny
\r, kod wyjścia 0. - Błąd, który niczego nie zmienia —
: command not foundna pustej linii, skrypt dobiega do końca, kod wyjścia 0. - Błąd składni —
if,fori definicje funkcji się rozpadają, kod wyjścia 2. bad interpreter— shebang niesie\r, kod wyjścia 126.
Zawsze zaczyna się od file yourfile. Jedna linia wyniku rozstrzyga, po której stronie sporu CRLF–LF leży plik.
Wszystko poniżej zmierzono na macOS (Darwin arm64): bash 3.2.57(1)-release, zsh 5.9, dash, git 2.55.0, node v26.7.0 i Python 3.14.6. Wyniki sprawdzono krzyżowo na Linuksie przez
docker run --rm bash:5, czyli GNU bash 5.3.15(1)-release (aarch64-unknown-linux-musl).
1. CRLF, LF i CR: co siedzi w tych bajtach
CRLF to skrót od carriage return line feed i dosłownie oznacza dwa znaki sklejone ze sobą:
| Nazwa | Sekwencja | Bajt |
|---|---|---|
| Powrót karetki (carriage return) | \r | 0x0D |
| Wysuw wiersza (line feed) | \n | 0x0A |
| CRLF | \r\n | 0x0D 0x0A |
Co zapisuje która platforma:
| Platforma | Znak końca linii |
|---|---|
| Windows | CRLF (\r\n) |
| Linux, współczesny macOS | LF (\n) |
| Mac Classic, OS 9 i starsze | samotny CR (\r) |
Nazwy są czysto mechaniczne. W dalekopisie powrót karetki cofał głowicę drukującą do lewego marginesu, a wysuw wiersza przewijał papier o jeden rząd. Windows zachował oba ruchy jako dwa bajty; Unix uznał, że jeden wystarczy. Samotny CR wciąż wypływa w starych eksportach, a plik pełen takich znaków wygląda dla większości narzędzi uniksowych jak jedna gigantyczna linia.
Jedno polecenie rozstrzyga, który to wariant
file czyta bajty i nazywa terminator po imieniu:
$ 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
Brak przyrostka w drugim przypadku oznacza czyste LF. Trzeci wiersz wypisuje oba terminatory naraz i to on jest wart zapamiętania. Plik o mieszanych zakończeniach linii powstaje wtedy, gdy pisały do niego kolejno dwa narzędzia z różnymi ustawieniami: edytor zapisał LF, skrypt dopisał CRLF, a czasem po prostu scalenie zszyło ze sobą dwie wersje. od -c pokazuje ten rozłam bajt po bajcie:
$ od -c d3.txt
0000000 a \r \n b \n
0000005
Znaki końca linii mieszkają w warstwie bajtów, tuż obok kodowania znaków; przewodnik po UTF-8 i UTF-16 opisuje, co dzieje się piętro wyżej.
2. Cztery tryby awarii, od cichego powodzenia do kodu 126
Ten sam plik z \r\n, pięć różnych rezultatów zależnie od tego, co zawiera linia:
| Zawartość skryptu | Co się naprawdę dzieje | Kod |
|---|---|---|
echo hello i inne proste polecenia | Ciche powodzenie, wynik jest poprawny | 0 |
przypisanie X=abc | Ciche powodzenie, ale wartość kończy się na \r | 0 |
puste linie, kontynuacje z końcowym \ | : command not found, skrypt biegnie dalej do końca | 0 |
if/fi, for/do/done, f() { | syntax error near unexpected token | 2 |
linia shebang z \r | bad interpreter: No such file or directory | 126 |
Dwa pierwsze wiersze giną w większości poradników. Zdanie „CRLF powoduje command not found” powtarza się wszędzie i nie opisuje tego, co faktycznie zachodzi. Proste polecenia w ogóle nie protestują.
Najgroźniejsze jest ciche powodzenie
$ printf 'echo hello\r\necho world\r\n' > win.sh && bash win.sh
hello
world
[exit=0]
Dwie linie na wejściu, dwie na wyjściu, kod 0. Nie ma czego debugować ani czego szukać w logu. Wystarczy dać temu samemu skryptowi coś do porównania, a obraz się zmienia:
$ 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 przechowuje 1.2.3\r, a nie 1.2.3. Identyczny wynik na macOS i na Linuksie. Bramki wersji, if [ "$ENV" = "prod" ], flagi funkcji czytane z pliku: każde z nich po cichu wybiera złą gałąź, z kodem wyjścia 0. To najtrudniejszy do wytropienia błąd końca linii w CI, bo build świeci na zielono, a log jest czysty.
Błąd, który niczego nie zatrzymuje
Pusta linia w pliku CRLF nie jest pusta — zostaje w niej sam \r, a powłoka traktuje ją jak polecenie do uruchomienia. Polecenie zawodzi, wypisuje komunikat, a skrypt przechodzi do następnej linii i kończy się kodem 0. Dokładne brzmienie zależy od powłoki, o czym jest następny rozdział.
Kontynuacje z końcowym ukośnikiem odwrotnym psują się tak samo. \r siedzi między ukośnikiem a znakiem nowej linii, więc kontynuacja przestaje być kontynuacją, a kolejna linia wykonuje się samodzielnie.
Błędy składni i powód, dla którego fi to nie fi
c_if.sh: line 5: syntax error: unexpected end of file
[exit=2]
Linuksowy bash 5.3.15 mówi o tym samym pliku więcej:
i.sh: line 5: syntax error: unexpected end of file from `if' command on line 2
Pętle i definicje funkcji wywracają się natomiast w linii 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() {'
Mechanizm jest za każdym razem ten sam. Bash odczytuje zamykające słowo jako fi\r, a nie fi. fi\r nie jest słowem kluczowym fi, więc blok if nigdy się nie zamyka i bash czyta dalej, aż plik się skończy — dlatego komunikat wskazuje ostatnią linię zamiast tej zepsutej.
bad interpreter i kod wyjścia 126
$ ./s.sh
bash: ./s.sh: /bin/bash^M: bad interpreter: No such file or directory
[exit=126]
macOS i Linux wypisują ten sam tekst i oba kończą się kodem 126. Warto przeczytać ścieżkę w komunikacie: /bin/bash^M. Jądro bierze wszystko po #! aż do znaku nowej linii jako ścieżkę interpretera, a \r jest jej częścią. Taki plik nie istnieje, więc exec zawodzi, zanim wykona się choćby jedna linia skryptu.
Kod 126 obejmuje też przypadek „znaleziono, ale nie da się wykonać”, więc skrypt odmawiający startu nie jest automatycznie problemem z końcami linii; uprawnienia plików produkują awarie z tej samej rodziny. To ^M wewnątrz ścieżki odróżnia jedno od drugiego.
Dlaczego tego \r nigdy nie widać
Czytanie wyniku nic nie daje, bo ten bajt nie zostawia niczego, na co można spojrzeć albo co da się zaznaczyć. Trzeba go wywlec na wierzch siłą:
$ 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
Dwa bajty \r, niewidoczne w normalnym wyniku, oba wewnątrz łańcucha, który za chwilę posłuży jako nazwa hosta. Czasem taki bajt wędruje do argumentu, a winę bierze na siebie zupełnie niezwiązane narzędzie:
$ bash v.sh # the script pipes through: ... | head -2
head: illegal line count -- 2\r
head zachowuje się bez zarzutu. To skrypt podał mu 2\r. Każdy komunikat błędu z zabłąkanym \r w cytowanej wartości to ten sam błąd występujący pod cudzym nazwiskiem.
3. Dlaczego komunikat błędu w niczym nie przypomina tego z internetu
Jeden plik (printf 'echo a\r\n\r\necho b\r\n', w którym linia 2 jest pustą linią zawierającą wyłącznie \r) uruchomiony pod czterema powłokami:
| Powłoka | Wersja | Dokładny komunikat |
|---|---|---|
| bash (dołączony do macOS) | 3.2.57(1)-release | s_blank.sh: line 2: : command not found |
| bash (typowy 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 |
Niemal każdy wynik wyszukiwania cytuje drugi wiersz. Bash 4 i 5 wypisują znaki niedrukowalne w cudzysłowie ANSI-C, co zamienia powrót karetki w $'\r'. Bash dostarczany z macOS ma wersję 3.2 i tego nie robi, więc zostaje dwukropek, spacja i nic pomiędzy. zsh wypisuje ^M. dash w ogóle pomija słowo „command”.
Jedna usterka, cztery komunikaty. Jeśli wklejenie dokładnej treści błędu w wyszukiwarkę nie zwróciło niczego sensownego, to właśnie dlatego. Gołe : command not found jest tym samym błędem co ten słynny.
4. Git: co core.autocrlf zapisuje w repozytorium
Większość wyjaśnień git autocrlf kończy się na definicjach, a w pracy potrzebna jest odpowiedź na dwa pytania: co ląduje w commicie i co dostają koledzy z zespołu, gdy zrobią checkout. Zmierzone przez git cat-file -p HEAD:f.txt dla zapisanego blobu i rm f.txt && git checkout -- f.txt dla kopii roboczej:
core.autocrlf | Plik źródłowy | Blob w repozytorium | Kopia robocza po checkoucie |
|---|---|---|---|
true | CRLF | LF | CRLF |
true | LF | LF | CRLF |
input | CRLF | LF | LF |
input | LF | LF | LF |
false | CRLF | CRLF | CRLF |
false | LF | LF | LF |
Z tej tabeli wypadają trzy wnioski:
trueiinputgwarantują LF w repozytorium. Różnica dotyczy wyłącznie checkoutu:truekonwertuje z powrotem na CRLF,inputzostawia plik w spokoju.- Tylko
falsewpuszcza CRLF do commita. Kiedy ktoś pyta, kto zacommitował powroty karetki, odpowiedzią jest ten wiersz. - Drugi wiersz to niespodzianka. Przy
trueplik, który na dysku był LF, wraca po checkoucie jako CRLF.
Git zapowiada przepisanie, zanim do niego dojdzie, w jednej z dwóch form:
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
„Niczego nie zmieniałem, a Git twierdzi, że zmienił się cały plik”
Znowu drugi wiersz. Przy git config core.autocrlf true checkout przepisuje pliki LF na CRLF w katalogu roboczym. Każda linia różni się teraz od blobu o jeden bajt, więc git diff raportuje każdą z nich jako zmodyfikowaną, a pull request pokazuje plik, którego nikt nie tknął, jako przepisany od zera. Zrobił to filtr checkoutu.
Lustrzane odbicie generuje ten sam szum: kolega ustawiony na false commituje CRLF, my siedzimy na input i w naszym diffie wyskakują pliki, których nigdy nie otwieraliśmy.
5. .gitattributes to odpowiedź na poziomie zespołu
core.autocrlf jest ustawieniem jednej maszyny, niewidocznym dla nikogo innego. .gitattributes jest plikiem wewnątrz repozytorium, więc podróżuje z każdym klonem. Kiedy oba są sprzeczne, wygrywają atrybuty. Przetestowano wszystkie cztery formy:
.gitattributes | core.autocrlf | Źródło | Blob | Po checkoucie | Zwycięzca |
|---|---|---|---|---|---|
* text=auto | false | CRLF | LF | LF | atrybuty |
* text eol=crlf | input | LF | LF | CRLF | atrybuty |
* -text | true | CRLF | CRLF | CRLF | atrybuty |
* text eol=lf | true | CRLF | LF | LF | atrybuty |
Trzeci wiersz warto zapamiętać: -text całkowicie wyłącza konwersję i mimo to bije core.autocrlf=true. Tak chroni się pliki, których bajty muszą przetrwać nietknięte.
.gitattributes do skopiowania
* 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 normalizuje do LF w repozytorium wszystko, co Git rozpozna jako tekst. Jawne linie eol=lf obejmują pliki, które muszą być LF niezależnie od tego, kto je napisał, bo .sh z CRLF to czekający na swoją kolej kod 126. Windowsowe typy skryptów dostają eol=crlf z lustrzanego powodu, a wzorce binarne dostają -text, żeby nie konwertować niczego.
core.safecrlf i core.eol
Dwa ustawienia, które kręcą się obok core.autocrlf i robią coś zupełnie innego:
core.safecrlfjest strażnikiem, nie konwerterem. Kiedy konwersja nie byłaby odwracalna (plik mieszany, w którym normalizacja gubi informację),trueodrzuca operację, awarnprzepuszcza ją z ostrzeżeniem. Nigdy nie zmienia tego, które bajty zostaną zapisane; odmawia jedynie cichego wykonywania operacji stratnych.core.eolwybiera, jakie zakończenie Git zapisuje w katalogu roboczym dla plików oznaczonych jakotext, gdycore.autocrlfma wartośćfalse. Dopuszczalne wartości tolf,crlfinative.core.autocrlfje nadpisuje, dlatego ustawieniecore.eolna maszynie z wciąż włączonym autocrlf zwykle wygląda, jakby nie zrobiło nic.
Zmiana .gitattributes nie naprawia plików już zacommitowanych
Atrybuty działają w momencie, w którym Git zapisuje albo czyta plik, więc istniejąca treść zostaje taka, jaka jest, dopóki coś jej nie przepisze. Ten przebieg trzeba wymusić samodzielnie:
$ git add --renormalize .
$ git commit -m "Normalize line endings"
Diff wyjdzie gigantyczny i o to chodzi. Najlepiej zrobić to na osobnej gałęzi, scalić jednym commitem i uprzedzić zespół, zanim wyląduje.
6. Konwersja CRLF na LF i z powrotem
Cztery sposoby, wszystkie sprawdzone pod kątem usuwania \r na Darwinie:
| Polecenie | Rezultat |
|---|---|
tr -d '\r' < f > f.out | działa |
perl -pi -e 's/\r\n/\n/g' f | działa |
sed -i '' -e 's/\r$//' f (forma BSD) | działa |
sed -i -e 's/\r$//' f (forma GNU, uruchomiona na macOS) | działa, ale zostawia po sobie plik-śmiecia |
Pułapka GNU sed -i na macOS
BSD-owy sed wymaga, żeby zaraz po -i szedł przyrostek kopii zapasowej. Wystarczy skopiować linuksowy poradnik dosłownie, a sed weźmie -e za ten przyrostek. Edycja i tak się wykona, a razem z nią to:
a5.txt
a5.txt-e
a5.txt-e jest kopią zapasową, utworzoną dlatego, że sed odczytał -e jako zamówiony przyrostek. Poprawna forma dla macOS podaje jawny pusty łańcuch: sed -i '' -e 's/\r$//' f. Repozytorium z zacommitowanymi plikami -e to repozytorium, w którym ktoś uruchomił GNU-owy jednolinijkowiec na Macu.
macOS nie ma dos2unix
command -v dos2unix nie zwraca niczego na standardowym systemie macOS; binarka pochodzi z brew install dos2unix. Dlatego najczęściej kopiowana odpowiedź w internecie zawodzi akurat na maszynie, przy której stuka spora część programistów. tr -d '\r' nie wymaga instalacji i robi dokładnie to samo.
W drugą stronę unix2dos ma ten sam problem z dostępnością, a sed -e 's/$/\r/' zastępuje go w zupełności.
Kiedy plik wróci do czystego LF, znów można bezpiecznie traktować go jak listę linii. Ma to znaczenie wszędzie tam, gdzie linie porównuje się jako całe łańcuchy: sortowanie linii tekstu i usuwanie zduplikowanych linii czytają value\r i value jako dwie różne linie, więc jeden zabłąkany powrót karetki po cichu unieważnia deduplikację.
W edytorze
VS Code pokazuje znak końca linii bieżącego pliku jako CRLF albo LF na pasku stanu w prawym dolnym rogu, a kliknięcie przełącza plik. Ustawienie files.eol steruje domyślną wartością dla nowych plików, a ustawione per workspace utrzymuje spójność w mieszanym zespole. Inne edytory wystawiają te same dwa pokrętła pod innymi nazwami. Łatwo przeoczyć, że wskaźnik dla pojedynczego pliku i ustawienie domyślne są od siebie niezależne: zmiana jednego nie rusza drugiego.
7. Obsługa znaków końca linii w kodzie
Jeden plik, line1\r\nline2\r\n, odczytany przez siedem punktów wejścia:
| Punkt wejścia | Co wraca | \r |
|---|---|---|
Node fs.readFileSync(f,"utf8") | "line1\r\nline2\r\n" | zachowany |
Node, ta sama wartość + .split("\n") | ["line1\r","line2\r",""] | zachowany w każdej linii |
Node readline z crlfDelay:Infinity | ["line1","line2"] | usunięty |
Python open(f), tryb domyślny | 'line1\nline2\n' | przekonwertowany |
Python open(f).readlines() | ['line1\n','line2\n'] | przekonwertowany |
Python open(f, newline="") | 'line1\r\nline2\r\n' | zachowany |
Python open(f,"rb") | b'line1\r\nline2\r\n' | zachowany |
Ta tabela rozstrzyga znajome zgłoszenie błędu: działa w Pythonie, psuje się w Node. Domyślny tryb tekstowy Pythona stosuje uniwersalne znaki nowej linii i tłumaczy \r\n na \n, zanim cokolwiek zdąży się pokazać. Node podaje bajty takimi, jakie są. Żadne z tych zachowań nie jest błędne, ale oba rozjeżdżają się w chwili, gdy czytają ten sam plik.
rstrip("\n") zostawia \r na miejscu
original: 'line1\r\n' | rstrip("\n"): 'line1\r' | strip(): 'line1'
rstrip("\n") usuwa dokładnie te znaki, które wymieniono, a \r nie znalazł się na liście. Wynik porównuje się potem jako różny od wszystkiego, do czego powinien pasować — i to jest uczciwa odpowiedź na „obciąłem to, a i tak się nie równa”. Wystarczy sięgnąć po strip() albo rstrip() bez argumentu, a znikną wszystkie końcowe białe znaki, w tym powrót karetki.
Bezpieczny podział na linie na obu platformach
W JavaScripcie dzieli się po wzorcu tolerującym oba zakończenia: text.split(/\r?\n/). W Pythonie albo zostajemy w domyślnym trybie tekstowym i pozwalamy uniwersalnym znakom nowej linii wykonać robotę, albo wołamy splitlines(), które radzi sobie z \r\n, \n i samotnym \r.
Zapis to druga połowa sprawy. Node zapisuje te bajty, które dostanie, więc łańcuchy warto budować z \n i pozwolić .gitattributes zdecydować, co wyląduje na dysku. Pythonowe open(path, "w") tłumaczy \n na zakończenie właściwe dla platformy, chyba że przekaże się newline="", czyli flagę, o którą moduły zapisujące CSV proszą dokładnie z tego powodu.
Powrót karetki na końcu linii i znacznik kolejności bajtów na początku pliku to ten sam gatunek błędu widziany z dwóch stron: niewidoczny bajt, który przeżywa kopiuj-wklej i psuje sprawdzenie równości. Ten drugi koniec opisuje przewodnik po błędach z BOM w UTF-8.
8. Gdzie jeszcze znaki końca linii dają się we znaki
CSV i Excel. RFC 4180 wskazuje CRLF jako separator rekordów, co czyni CSV jednym z niewielu miejsc, gdzie CRLF jest poprawne, a nie wadliwe. Parsery pisane pod wejście wyłącznie z LF zostawiają \r na ostatnim polu każdego wiersza, więc jeśli wartości z konwersji CSV na JSON wyglądają dobrze, ale porównują się źle, najpierw warto sprawdzić ten bajt.
Docker. Plik .sh z CRLF skopiowany do obrazu to czwarty tryb awarii. COPY zachowuje bajty, shebang zatrzymuje swój \r, a kontener kończy pracę z kodem 126. Jedna linia *.sh text eol=lf w .gitattributes blokuje całą tę klasę.
Diffy i pull requesty. Plik oznaczony jako zmieniony w całości, bez widocznej edycji, to mechanizm z rozdziału 4 docierający do code review. Zestawienie obu wersji przez porównanie tekstu potwierdza to w kilka sekund, a przewodnik po text diff prowadzi krok po kroku przez czytanie wyniku.
Pliki mieszane. ASCII text, with CRLF, LF line terminators oznacza dwóch zapisujących z różnymi ustawieniami. Normalizować trzeba cały plik, a nie te linie, które akurat zostały wyedytowane, bo następny diff będzie równie hałaśliwy.
FAQ
Czym różni się CRLF od LF?
CRLF to dwa bajty, \r\n (0x0D 0x0A). LF to jeden bajt, \n (0x0A). Windows zapisuje CRLF, Linux i macOS zapisują LF, a oba oznaczają koniec linii. W edytorze tekst wygląda identycznie; różnica ujawnia się dopiero w bajtach, porównaniach łańcuchów i diffach.
Dlaczego skrypt z zakończeniami CRLF nie zgłasza żadnego błędu?
Bo proste polecenia przeżywają dodatkowy bajt. echo hello z końcowym \r uruchamia się i kończy kodem 0. Błędy pojawiają się tylko tam, gdzie parserowi na tym zależy: przy pustej linii, przy słowie kluczowym takim jak fi albo przy shebangu. Przypisania są przypadkiem groźnym, bo kończą się powodzeniem i zapisują \r w zmiennej.
Co oznacza $'\r': command not found i dlaczego tego komunikatu nie widać?
Oznacza, że powłoka spróbowała uruchomić linię zawierającą wyłącznie powrót karetki. Bash 4 i 5 wypisują ten znak w cudzysłowie ANSI-C, dając $'\r'. Bash 3.2 dołączany do macOS nie wypisuje nic pomiędzy dwukropkami, a zsh pokazuje zamiast tego ^M. Jedna usterka, trzy różne komunikaty.
Czy core.autocrlf powinno mieć wartość true, input czy false?
Na Linuksie i macOS input, na Windows true, ale nad jedno i drugie warto przedłożyć .gitattributes. Z pomiarów wynika, że true i input zapisują w repozytorium LF, a tylko false wpuszcza CRLF do commita. true dodatkowo przepisuje przy checkoucie pliki LF na CRLF w kopii roboczej.
Kiedy .gitattributes i core.autocrlf są sprzeczne, co wygrywa?
Wygrywa .gitattributes. Wszystkie cztery przetestowane formy (* text=auto, * text eol=crlf, * -text i * text eol=lf) nadpisały lokalną wartość core.autocrlf. To główny argument za jego używaniem: atrybuty są commitowane i obowiązują wszystkich, podczas gdy core.autocrlf jest ustawieniem osobnym dla każdej maszyny, którego nie da się ani zobaczyć, ani wyegzekwować.
Jak sprawdzić, czy plik używa CRLF czy LF?
Wystarczy uruchomić file yourfile. Dla CRLF wypisze ASCII text, with CRLF line terminators, dla czystego LF ASCII text bez żadnego przyrostka, a dla pliku mieszanego with CRLF, LF line terminators. Dla pewności na poziomie bajtów można uruchomić od -c i poszukać \r siedzącego przed każdym \n.
Kiedy naprawdę warto używać CRLF?
Wtedy, gdy wymaga tego format albo protokół. RFC 4180 definiuje CRLF jako separator rekordów w CSV, a nagłówki HTTP i SMTP robią to samo w transmisji. Windowsowe pliki batch i PowerShell też są bezpieczniejsze z CRLF. Wszędzie indziej, czyli w kodzie źródłowym, skryptach powłoki i plikach konfiguracyjnych, zostaje LF.