Skip to content
Torna al blog
Tutorial

Fine riga CRLF e LF: cosa si rompe davvero

Uno script con CRLF esce con 0: il danno resta in una variabile. Quattro modi di fallimento, l'errore per shell e la matrice core.autocrlf, testati online.

14 min di lettura

Fine riga CRLF e LF: cosa si rompe davvero

Tra CRLF e LF c’è un byte di differenza. LF è un solo \n (0x0A) e chiude le righe su Linux e macOS. CRLF sono due byte, \r\n (0x0D 0x0A), e chiudono le righe su Windows.

Qui quasi tutti gli articoli prendono un abbaglio: uno script shell con fine riga CRLF di solito non fallisce. Gira, stampa quello che ti aspetti ed esce con 0. A rompersi è il valore che finisce dentro una variabile, perché l’assegnamento si è tenuto il \r finale e nessuno ha protestato.

Il comportamento misurato si divide in quattro modi di fallimento, elencati qui dal più difficile da notare al più vistoso:

  1. Successo silenzioso. L’output sembra corretto, una variabile si porta dietro un \r invisibile, codice di uscita 0.
  2. Un errore che non cambia niente. Su una riga vuota compare : command not found, lo script arriva fino in fondo, codice di uscita 0.
  3. Errore di sintassi. if, for e le definizioni di funzione si rompono, codice di uscita 2.
  4. bad interpreter. Lo shebang si porta dietro il \r, codice di uscita 126.

Prima di ogni altra cosa, esegui file yourfile. Una riga di output ti dice se hai CRLF o LF.

Tutto quello che segue è stato misurato su macOS (Darwin arm64) con bash 3.2.57(1)-release, zsh 5.9, dash, git 2.55.0, node v26.7.0 e Python 3.14.6, e verificato di nuovo su Linux tramite docker run --rm bash:5, cioè GNU bash 5.3.15(1)-release (aarch64-unknown-linux-musl).

1. CRLF, LF e CR: cosa sono davvero i byte

CRLF sta per carriage return line feed, ovvero due caratteri attaccati uno all’altro:

NomeEscapeByte
Carriage return (ritorno del carrello)\r0x0D
Line feed (avanzamento riga)\n0x0A
CRLF\r\n0x0D 0x0A

Cosa scrive ciascuna piattaforma:

PiattaformaFine riga
WindowsCRLF (\r\n)
Linux, macOS modernoLF (\n)
Mac Classic, OS 9 e precedentiCR (\r) da solo

I nomi vengono dalla meccanica. Su una telescrivente il carriage return riportava la testina di stampa al margine sinistro e il line feed faceva avanzare la carta di una riga. Windows ha tenuto tutti e due i movimenti come due byte; Unix ha deciso che uno bastava. Il CR isolato salta ancora fuori nei vecchi export, e alla maggior parte dei tool Unix un file che ne è pieno sembra un’unica riga sterminata.

Un solo comando ti dice quale hai

file legge i byte e dà un nome al terminatore:

$ 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

Il terzo caso è quello da tenere a mente. Un file con terminatori di riga misti vuol dire che ci hanno scritto a turno due strumenti configurati in modo diverso: un editor ha salvato in LF, uno script ha aggiunto CRLF in coda, oppure un merge ha ricucito insieme due versioni. od -c mostra la divisione byte per byte:

$ od -c d3.txt
0000000   a  \r  \n   b  \n
0000005

I caratteri di fine riga vivono a livello di byte, subito accanto alla codifica dei caratteri; la guida alle codifiche UTF-8 e UTF-16 racconta cosa succede un livello più sopra.

2. I quattro modi di fallimento, dal successo silenzioso all’uscita 126

Stesso file \r\n, cinque esiti diversi a seconda di cosa contiene la riga:

Contenuto dello scriptCosa succede davveroUscita
echo hello e altri comandi sempliciSuccesso silenzioso, l’output è corretto0
assegnamento X=abcSuccesso silenzioso, ma il valore finisce con \r0
Righe vuote, continuazioni con \ finale: command not found, lo script continua fino in fondo0
if/fi, for/do/done, f() {syntax error near unexpected token2
Riga shebang con \rbad interpreter: No such file or directory126

Le prime due righe sono il motivo per cui questa pagina esiste. “CRLF causa command not found” viene ripetuto ovunque, e non è quello che succede. I comandi semplici non protestano affatto.

Il successo silenzioso è quello pericoloso

$ printf 'echo hello\r\necho world\r\n' > win.sh && bash win.sh
hello
world
[exit=0]

Due righe in ingresso, due in uscita, codice di uscita 0. Non c’è niente da debuggare e niente da cercare con grep nei log. Dai allo stesso script un confronto da fare e la faccenda cambia:

$ 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 contiene 1.2.3\r, non 1.2.3. Risultato identico su macOS e Linux. Controlli di versione, if [ "$ENV" = "prod" ], feature flag letti da un file: ognuno prende il ramo sbagliato, in silenzio, con codice di uscita 0. È il bug di fine riga più difficile da trovare in CI, perché la build è verde e il log è pulito.

L’errore che non ferma niente

Dentro un file CRLF una riga vuota è una riga che contiene solo \r, e la shell la tratta come un comando da eseguire. Fallisce, stampa un messaggio, poi lo script passa alla riga successiva e finisce con uscita 0. Il testo esatto dipende dalla shell che stai usando, e cambia molto più di quanto ti aspetti.

Le continuazioni con backslash finale si rompono allo stesso modo. Il \r si infila tra il backslash e il newline, quindi la continuazione smette di essere una continuazione e la riga successiva viene eseguita per conto suo.

Errori di sintassi, e il motivo per cui fi non è fi

c_if.sh: line 5: syntax error: unexpected end of file
[exit=2]

Su Linux bash 5.3.15 dice di più sullo stesso file:

i.sh: line 5: syntax error: unexpected end of file from `if' command on line 2

Cicli e definizioni di funzione invece falliscono alla riga 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() {'

Capito il meccanismo, tutta la categoria diventa prevedibile. Bash legge la parola di chiusura come fi\r, non come fi. fi\r non è la parola chiave fi, quindi il blocco if non si chiude mai e bash continua a leggere finché il file non finisce: ecco perché l’errore punta all’ultima riga invece che a quella rotta.

bad interpreter e uscita 126

$ ./s.sh
bash: ./s.sh: /bin/bash^M: bad interpreter: No such file or directory
[exit=126]

macOS e Linux stampano lo stesso testo ed escono tutti e due con 126. Leggi il percorso nel messaggio: /bin/bash^M. Il kernel prende come percorso dell’interprete tutto quello che sta dopo #! fino al newline, e il \r ne fa parte. Un file così non esiste, quindi l’exec fallisce prima che venga eseguita una sola riga del tuo script.

L’uscita 126 copre anche “trovato, ma non eseguibile”, quindi uno script che si rifiuta di partire non è automaticamente un problema di fine riga: anche i permessi dei file producono fallimenti della stessa famiglia. A distinguere i due casi è il ^M dentro il percorso.

Perché il \r non lo vedi mai

Leggere l’output non aiuta, perché quel byte non ti dà niente da guardare né da selezionare. Devi costringerlo a farsi vedere:

$ 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

Due byte \r, invisibili nell’output normale, tutti e due dentro una stringa che sta per essere usata come hostname. A volte il byte viaggia dentro un argomento e a prendersi la colpa è uno strumento che non c’entra niente:

$ bash v.sh          # the script pipes through: ... | head -2
head: illegal line count -- 2\r

head si sta comportando correttamente. Lo script gli ha passato 2\r. Qualunque messaggio di errore con un \r vagante dentro il valore citato è questo bug che si presenta sotto falso nome.

3. Perché il tuo messaggio di errore non somiglia per niente a quello che trovi online

Il file di prova è printf 'echo a\r\n\r\necho b\r\n', dove la riga 2 è vuota e contiene soltanto \r. Sotto quattro shell diverse i messaggi sono questi:

ShellVersioneMessaggio esatto
bash (in bundle con macOS)3.2.57(1)-releases_blank.sh: line 2: : command not found
bash (Linux mainstream)5.3.15(1)-releaset.sh: line 2: $'\r': command not found
zsh5.9s_blank.sh:2: command not found: ^M
dashs_blank.sh: 2: : not found

Quasi tutti i risultati di ricerca citano la seconda riga. Bash 4 e 5 stampano i caratteri non stampabili con l’ANSI-C quoting, che trasforma il carriage return in $'\r'. Il bash che arriva insieme a macOS è il 3.2 e non lo fa, quindi al posto del carattere ti ritrovi due punti e uno spazio, senza niente in mezzo. zsh stampa ^M. dash butta via del tutto la parola “command”.

Un solo guasto, quattro messaggi. Se hai incollato il tuo errore esatto in un motore di ricerca e non è tornato indietro niente di utile, il motivo è questo. Il : command not found senza niente davanti è lo stesso bug della versione più citata.

4. Git: cosa mette davvero core.autocrlf nel tuo repository

Quasi tutte le spiegazioni di git autocrlf si fermano alle definizioni. Serve invece sapere cosa finisce nel commit, e cosa si ritrovano i colleghi quando fanno il checkout. Misurato con git cat-file -p HEAD:f.txt per il blob salvato e con rm f.txt && git checkout -- f.txt per la copia di lavoro:

core.autocrlfFile di origineBlob nel repositoryCopia di lavoro dopo il checkout
trueCRLFLFCRLF
trueLFLFCRLF
inputCRLFLFLF
inputLFLFLF
falseCRLFCRLFCRLF
falseLFLFLF

Da quella tabella escono tre conclusioni:

  1. true e input garantiscono entrambi LF nel repository. L’unica differenza è il checkout: true riconverte in CRLF, input lascia stare il file.
  2. Solo false mette CRLF dentro un commit. Quando qualcuno chiede chi ha committato i carriage return, la risposta è questa riga.
  3. La sorpresa è la seconda riga. Con true, un file che su disco era LF torna indietro in CRLF dopo il checkout.

Git annuncia la riscrittura prima che avvenga, in una di queste due forme:

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

“Non ho toccato niente, ma git dice che è cambiato tutto il file”

Di nuovo la seconda riga. Con git config core.autocrlf true, il checkout riscrive in CRLF i file LF nella directory di lavoro. Adesso ogni riga differisce dal blob di un byte, quindi git diff segnala tutte le righe come modificate e la pull request mostra come completamente riscritto un file che nessuno ha toccato. È stato il filtro di checkout.

L’immagine speculare produce lo stesso rumore: un collega con false committa CRLF, tu sei su input, e nel tuo diff spuntano file che non hai mai aperto.

5. .gitattributes è la risposta a livello di team

core.autocrlf è un’impostazione di una singola macchina, invisibile a tutti gli altri. .gitattributes è un file dentro il repository, quindi viaggia con ogni clone. Quando i due sono in disaccordo, vincono gli attributi. Tutte e quattro le forme sono state provate:

.gitattributescore.autocrlfOrigineBlobDopo il checkoutVincitore
* text=autofalseCRLFLFLFattributi
* text eol=crlfinputLFLFCRLFattributi
* -texttrueCRLFCRLFCRLFattributi
* text eol=lftrueCRLFLFLFattributi

La terza riga vale la pena ricordarla: -text disattiva del tutto la conversione e batte comunque core.autocrlf=true. È così che proteggi i file i cui byte devono sopravvivere intatti.

Un .gitattributes da copiare

* 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 normalizza a LF nel repository tutto quello che Git riconosce come testo. Le righe eol=lf esplicite coprono i file che devono essere LF a prescindere da chi li ha scritti, perché un .sh con CRLF è un’uscita 126 che prima o poi arriva. I tipi di script Windows prendono eol=crlf per il motivo speculare, e i pattern binari prendono -text così non viene convertito proprio niente.

core.safecrlf e core.eol

Due impostazioni che compaiono accanto a core.autocrlf e fanno tutt’altro:

  • core.safecrlf è una guardia, non un convertitore. Quando una conversione non farebbe round-trip, per esempio su un file misto dove normalizzare perde informazione, true rifiuta l’operazione e warn la lascia passare con un avviso. Non cambia mai quali byte vengono salvati; si rifiuta soltanto di eseguire in silenzio le conversioni con perdita.
  • core.eol sceglie quale fine riga Git scrive nella directory di lavoro per i file marcati come text, quando core.autocrlf vale false. I valori sono lf, crlf e native. core.autocrlf ha la precedenza, ed è per questo che impostare core.eol su una macchina con autocrlf ancora attivo di solito sembra non aver fatto niente.

Modificare .gitattributes non sistema i file già committati

Gli attributi si applicano quando Git scrive o legge un file, quindi il contenuto esistente resta com’è finché qualcosa non lo riscrive. Forza tu stesso quel passaggio:

$ git add --renormalize .
$ git commit -m "Normalize line endings"

Aspettati un diff enorme: è proprio quello il punto. Fallo su un branch dedicato, uniscilo in un solo commit e avvisa tutti prima che arrivi.

6. Conversione da CRLF a LF, e ritorno

Quattro modi per togliere il \r, tutti verificati su Darwin:

ComandoRisultato
tr -d '\r' < f > f.outfunziona
perl -pi -e 's/\r\n/\n/g' ffunziona
sed -i '' -e 's/\r$//' f (forma BSD)funziona
sed -i -e 's/\r$//' f (forma GNU, eseguita su macOS)funziona, ma lascia dietro un file spazzatura

La trappola del sed -i GNU su macOS

Il sed BSD richiede che dopo -i arrivi un suffisso di backup. Copia un tutorial Linux alla lettera e il sed BSD si ingoia il -e come se fosse quel suffisso. La modifica viene fatta lo stesso, ma insieme ti ritrovi anche questo:

a5.txt
a5.txt-e

a5.txt-e è una copia di backup, creata perché sed ha letto -e come il suffisso che gli avevi chiesto. La forma corretta su macOS passa una stringa vuota esplicita: sed -i '' -e 's/\r$//' f. Un repository con dentro dei file -e committati è un repository in cui qualcuno ha eseguito un one-liner GNU su un Mac.

Su macOS non c’è dos2unix

command -v dos2unix non restituisce niente su un macOS appena installato; il binario arriva da brew install dos2unix. È per questo che la risposta più copiata di internet fallisce proprio sulla macchina su cui lavora una fetta enorme di sviluppatori. tr -d '\r' non richiede nessuna installazione e fa lo stesso lavoro.

Nella direzione opposta, unix2dos ha lo stesso problema di disponibilità, e sed -e 's/$/\r/' funziona come sostituto.

Una volta che un file è tornato a LF pulito, puoi di nuovo trattarlo tranquillamente come un elenco di righe. Questo conta per qualunque cosa confronti le righe come stringhe intere: ordina righe di testo e rimuovi righe duplicate leggono value\r e value come due righe diverse, quindi un carriage return vagante manda in fumo la deduplicazione senza dare nell’occhio.

Nel tuo editor

VS Code mostra la fine riga del file corrente come CRLF o LF nella barra di stato in basso a destra, e cliccandoci sopra converti il file. L’impostazione files.eol controlla il valore predefinito per i file nuovi, e impostarla per workspace tiene coerente un team eterogeneo. Altri editor espongono gli stessi due controlli sotto altri nomi. Quello che sfugge è che l’indicatore per file e l’impostazione predefinita sono due cose separate: cambiarne una non tocca l’altra.

7. Gestione dei caratteri di fine riga nel codice

Un solo file, line1\r\nline2\r\n, letto attraverso sette punti di ingresso:

Punto di ingressoCosa ottieni\r
Node fs.readFileSync(f,"utf8")"line1\r\nline2\r\n"conservato
Node, stesso valore + .split("\n")["line1\r","line2\r",""]conservato su ogni riga
Node readline con crlfDelay:Infinity["line1","line2"]rimosso
Python open(f), modalità predefinita'line1\nline2\n'convertito
Python open(f).readlines()['line1\n','line2\n']convertito
Python open(f, newline="")'line1\r\nline2\r\n'conservato
Python open(f,"rb")b'line1\r\nline2\r\n'conservato

Quella tabella chiude una segnalazione di bug che conosci già: in Python funziona e in Node si rompe. La modalità testo predefinita di Python applica le universal newline e traduce \r\n in \n prima ancora che tu lo veda. Node ti passa i byte così come sono. Nessuno dei due sbaglia, ma vanno in disaccordo nell’istante in cui leggono lo stesso file.

rstrip("\n") si lascia dietro il \r

original: 'line1\r\n'  | rstrip("\n"): 'line1\r'  | strip(): 'line1'

rstrip("\n") toglie esattamente i caratteri che hai elencato, e \r in quella lista non c’era. Il risultato non combacia con niente di quello a cui dovrebbe corrispondere, ed è la risposta onesta a “l’ho ripulito e continua a non essere uguale”. Usa strip(), oppure rstrip() senza argomenti, e se ne va tutto lo spazio bianco finale, carriage return compreso.

Dividere le righe senza sorprese su entrambe le piattaforme

In JavaScript, dividi su un pattern che tollera tutti e due i terminatori: text.split(/\r?\n/). In Python, o resti in modalità testo predefinita e lasci fare alle universal newline, oppure chiami splitlines(), che se la cava con \r\n, \n e anche con un \r isolato.

La scrittura è l’altra metà. Node scrive i byte che gli dai, quindi costruisci le stringhe con \n e lascia decidere a .gitattributes cosa finisce su disco. open(path, "w") di Python traduce \n nella fine riga della piattaforma, a meno che tu non passi newline="": è il flag che i writer CSV chiedono, esattamente per questo motivo.

Un carriage return alla fine di una riga e un byte-order mark all’inizio di un file sono lo stesso tipo di bug visto dalle due estremità del file: un byte invisibile che sopravvive al copia-incolla e fa saltare un confronto di uguaglianza. La guida ai problemi con il BOM UTF-8 copre l’altra estremità.

8. Dove altro mordono i caratteri di fine riga

CSV ed Excel. L’RFC 4180 indica CRLF come separatore di record, il che rende il CSV uno dei pochi posti in cui CRLF è corretto e non un difetto. I parser scritti per input solo LF lasciano un \r sull’ultimo campo di ogni riga, quindi se i valori usciti da una conversione da CSV a JSON sembrano giusti ma nei confronti risultano sbagliati, controlla prima quel byte.

Docker. Un .sh con CRLF copiato dentro un’immagine è il quarto modo di fallimento. COPY conserva i byte, lo shebang si tiene il suo \r, e il container esce con 126. Una sola riga *.sh text eol=lf in .gitattributes previene tutta la categoria.

Diff e pull request. Un file segnato come cambiato per intero senza nessuna modifica visibile è il meccanismo della sezione 4 che si presenta in code review. Confrontare le due versioni con text diff lo conferma in pochi secondi, e la guida al confronto di testi spiega come leggere il risultato.

File misti. ASCII text, with CRLF, LF line terminators vuol dire due strumenti con impostazioni diverse. Normalizza il file intero invece delle sole righe che ti è capitato di modificare, altrimenti il prossimo diff sarà rumoroso esattamente come questo.

FAQ

Qual è la differenza tra CRLF e LF?

CRLF sono due byte, \r\n (0x0D 0x0A). LF è un byte solo, \n (0x0A). Windows scrive CRLF, Linux e macOS scrivono LF, e tutti e due segnano la fine di una riga. Nell’editor il testo sembra identico; la differenza salta fuori solo nei byte, nei confronti tra stringhe e nei diff.

Il mio script ha fine riga CRLF. Perché non dà nessun errore?

Perché i comandi semplici sopravvivono al byte in più. echo hello con un \r finale viene eseguito ed esce con 0. Gli errori compaiono solo dove il parser ci fa caso: una riga vuota, una parola chiave come fi, oppure lo shebang. Gli assegnamenti sono il caso pericoloso, perché riescono e salvano il \r dentro la variabile.

Cosa significa $'\r': command not found, e perché io non lo vedo?

Significa che la shell ha provato a eseguire una riga che conteneva solo un carriage return. Bash 4 e 5 stampano quel carattere con l’ANSI-C quoting, e viene fuori $'\r'. Il bash 3.2 in bundle con macOS non stampa niente tra i due punti, e zsh stampa ^M al suo posto. Stesso guasto, tre messaggi diversi.

core.autocrlf va messo su true, input o false?

Usa input su Linux e macOS, true su Windows, e a entrambi preferisci .gitattributes. Alla prova dei fatti, true e input salvano tutti e due LF nel repository e solo false lascia entrare CRLF in un commit. In più, al checkout true riscrive in CRLF i file LF nella tua copia di lavoro.

Se .gitattributes e core.autocrlf sono in disaccordo, chi vince?

Vince .gitattributes. Tutte e quattro le forme testate (* text=auto, * text eol=crlf, * -text e * text eol=lf) hanno avuto la meglio sul valore locale di core.autocrlf. È questo l’argomento per usarlo: gli attributi vengono committati e valgono per tutti, mentre core.autocrlf è un’impostazione per macchina che non puoi né vedere né imporre.

Come faccio a capire se un file usa CRLF o LF?

Esegui file yourfile. Con CRLF stampa ASCII text, with CRLF line terminators, con LF puro stampa ASCII text senza nessun suffisso, e un file misto stampa with CRLF, LF line terminators. Per avere la certezza a livello di byte, esegui od -c e cerca i \r piazzati prima di ogni \n.

Quando conviene davvero usare CRLF?

Quando lo richiede un formato o un protocollo. L’RFC 4180 definisce CRLF come separatore di record per il CSV, e gli header HTTP e SMTP fanno lo stesso quando viaggiano in rete. Anche i file batch e PowerShell di Windows sono più sicuri con CRLF. In tutti gli altri casi (codice sorgente, script shell, file di configurazione) usa LF.

Tag: line-endings crlf git cross-platform shell

Articoli correlati

Vedi tutti gli articoli