Quebras de linha CRLF e LF: o que realmente quebra
CRLF e LF diferem por um único byte. LF é um \n (0x0A) sozinho e termina as linhas no Linux e no macOS. CRLF são dois bytes, \r\n (0x0D 0x0A), e terminam as linhas no Windows.
Agora a parte que a maioria dos artigos erra. Um script de shell com quebras de linha CRLF normalmente não falha. Ele roda, imprime o que você espera e sai com 0. O que quebra é o valor guardado numa variável, porque a atribuição manteve o \r do fim e ninguém reclamou disso.
O que foi medido se divide em quatro modos, do mais difícil de notar ao mais evidente:
- Sucesso silencioso: a saída parece correta, uma variável carrega um
\rinvisível e o script sai com 0. - Um erro que não muda nada:
: command not foundnuma linha em branco, o script roda até o fim e sai com 0. - Erro de sintaxe:
if,fore definições de função quebram, e o código de saída é 2. bad interpreter: o shebang carrega o\r, e o código de saída é 126.
Antes de qualquer outra coisa, rode file yourfile. Uma linha de saída já diz de que lado do CRLF vs LF você está.
Tudo o que vem abaixo foi medido no macOS (Darwin arm64) com bash 3.2.57(1)-release, zsh 5.9, dash, git 2.55.0, node v26.7.0 e Python 3.14.6, e conferido no Linux via
docker run --rm bash:5, que é o GNU bash 5.3.15(1)-release (aarch64-unknown-linux-musl).
1. CRLF, LF e CR: o que os bytes realmente são
CRLF é a sigla de carriage return line feed, ou retorno de carro e avanço de linha. São dois caracteres colados um no outro:
| Nome | Escape | Byte |
|---|---|---|
| Retorno de carro (carriage return) | \r | 0x0D |
| Avanço de linha (line feed) | \n | 0x0A |
| CRLF | \r\n | 0x0D 0x0A |
O que cada plataforma escreve:
| Plataforma | Quebra de linha |
|---|---|
| Windows | CRLF (\r\n) |
| Linux, macOS moderno | LF (\n) |
| Mac Classic, OS 9 e anteriores | CR (\r) sozinho |
Os nomes vêm da mecânica. Num teletipo, o retorno de carro trazia a cabeça de impressão de volta à margem esquerda e o avanço de linha girava o papel uma linha para cima. O Windows manteve os dois movimentos como dois bytes; o Unix decidiu que um bastava. O CR sozinho ainda aparece em exportações antigas, e um arquivo cheio deles parece uma única linha gigantesca para a maioria das ferramentas Unix.
Um comando diz qual dos dois você tem
O file lê os bytes e dá nome ao terminador:
$ 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
O terceiro caso é o que vale aprender. Um arquivo com quebras de linha mistas significa que duas ferramentas com configurações diferentes escreveram nele, uma depois da outra: um editor salvou em LF e um script acrescentou CRLF, ou um merge costurou duas versões. O od -c mostra a divisão byte a byte:
$ od -c d3.txt
0000000 a \r \n b \n
0000005
As quebras de linha vivem na camada de bytes, logo ao lado da codificação de caracteres; o guia de codificação UTF-8 e UTF-16 trata do que acontece um nível acima.
2. Os quatro modos de falha, do sucesso silencioso ao exit 126
O mesmo arquivo com \r\n, cinco desfechos diferentes conforme o que a linha contém:
| Conteúdo do script | O que realmente acontece | Saída |
|---|---|---|
echo hello e outros comandos simples | Sucesso silencioso, a saída está correta | 0 |
Atribuição X=abc | Sucesso silencioso, mas o valor termina em \r | 0 |
Linhas em branco, continuações com \ no fim | : command not found, o script continua até o fim | 0 |
if/fi, for/do/done, f() { | syntax error near unexpected token | 2 |
Linha shebang com \r | bad interpreter: No such file or directory | 126 |
As duas primeiras linhas são a razão de esta página existir. A frase “CRLF causa command not found” se repete em todo lugar, e não é isso que acontece. Comandos simples não reclamam de nada.
O sucesso silencioso é o perigoso
$ printf 'echo hello\r\necho world\r\n' > win.sh && bash win.sh
hello
world
[exit=0]
Duas linhas na entrada, duas na saída, exit 0. Não há o que depurar nem o que procurar no log com grep. Agora dê ao mesmo script uma comparação para fazer:
$ 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 guarda 1.2.3\r, não 1.2.3. Resultado idêntico no macOS e no Linux. Travas de versão, if [ "$ENV" = "prod" ], feature flags lidas de um arquivo: todas seguem pelo caminho errado, em silêncio, com código de saída 0. Esse é o bug de quebra de linha mais difícil de achar na CI, porque o build fica verde e o log fica limpo.
O erro que não interrompe nada
Dentro de um arquivo CRLF, uma linha em branco é na verdade uma linha que contém só \r, e o shell tenta executá-la como comando. Ela falha, imprime uma mensagem, o script segue para a linha seguinte e termina com exit 0. O texto exato depende do seu shell, que é o assunto da próxima seção.
As continuações com barra invertida no fim quebram do mesmo jeito. O \r fica entre a barra invertida e a nova linha, então a continuação deixa de ser continuação e a linha seguinte roda por conta própria.
Erros de sintaxe, e por que fi não é fi
c_if.sh: line 5: syntax error: unexpected end of file
[exit=2]
O bash 5.3.15 do Linux diz mais sobre o mesmo arquivo:
i.sh: line 5: syntax error: unexpected end of file from `if' command on line 2
Já os laços e as definições de função falham na linha 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() {'
O mecanismo torna a classe inteira previsível. O bash lê a palavra de fechamento como fi\r, e fi\r não é a palavra-chave fi. O bloco if nunca fecha, o bash segue lendo até o arquivo acabar, e por isso o erro aponta para a última linha em vez da linha quebrada.
bad interpreter e o exit 126
$ ./s.sh
bash: ./s.sh: /bin/bash^M: bad interpreter: No such file or directory
[exit=126]
macOS e Linux imprimem o mesmo texto e ambos saem com 126. Leia o caminho na mensagem: /bin/bash^M. O kernel toma tudo o que vem depois de #! até a nova linha como caminho do interpretador, e o \r faz parte disso. Esse arquivo não existe, então o exec falha antes que uma única linha do seu script rode.
O exit 126 também cobre “encontrado, mas não executável”, então um script que se recusa a iniciar não é automaticamente um problema de quebra de linha; permissões de arquivo produzem falhas da mesma família. O ^M dentro do caminho é o que distingue as duas.
Por que você nunca vê o \r
Ler a saída não ajuda, porque o byte não deixa nada para olhar nem para selecionar. Você precisa forçá-lo a aparecer:
$ 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
Dois bytes \r, invisíveis na saída normal, os dois dentro de uma string que está prestes a ser usada como nome de host. Às vezes o byte viaja para dentro de um argumento e uma ferramenta sem nada a ver leva a culpa:
$ bash v.sh # the script pipes through: ... | head -2
head: illegal line count -- 2\r
O head está se comportando corretamente. O script entregou a ele 2\r. Qualquer mensagem de erro com um \r perdido dentro do valor citado é esse bug usando o nome de outra pessoa.
3. Por que a sua mensagem de erro não se parece com a que está na internet
Um único arquivo, printf 'echo a\r\n\r\necho b\r\n', em que a linha 2 é uma linha em branco contendo apenas \r, executado em quatro shells:
| Shell | Versão | Mensagem exata |
|---|---|---|
| bash (o que vem com o macOS) | 3.2.57(1)-release | s_blank.sh: line 2: : command not found |
| bash (Linux convencional) | 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 |
Quase todo resultado de busca cita a segunda linha. O bash 4 e o 5 imprimem caracteres não imprimíveis com ANSI-C quoting, o que transforma o retorno de carro em $'\r'. O bash que acompanha o macOS é o 3.2 e não faz isso, então você recebe dois-pontos, um espaço e nada no meio. O zsh imprime ^M. O dash descarta de vez a palavra “command”.
Uma falha, quatro mensagens. Se você colou o seu erro exato numa caixa de busca e não veio nada de útil, a razão é essa. O : command not found pelado é o mesmo bug que a mensagem famosa.
4. Git: o que o core.autocrlf realmente guarda no seu repositório
A maioria das explicações sobre git autocrlf não passa das definições. A pergunta útil é outra: o que acaba indo para o commit, e o que os colegas recebem quando fazem checkout? Medido com git cat-file -p HEAD:f.txt para o blob armazenado e rm f.txt && git checkout -- f.txt para a cópia de trabalho:
core.autocrlf | Arquivo de origem | Blob no repositório | Cópia de trabalho após o 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 |
Três conclusões saem dessa tabela:
trueeinputgarantem LF no repositório. A única diferença está no checkout:trueconverte de volta para CRLF,inputdeixa o arquivo em paz.- Só o
falsecoloca CRLF dentro de um commit. Quando alguém perguntar quem comitou os retornos de carro, a resposta é essa linha. - A segunda linha é a surpresa. Com
true, um arquivo que estava em LF no disco volta em CRLF depois do checkout.
O Git anuncia a reescrita antes de ela acontecer, numa de duas formas:
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
“Eu não mudei nada, mas o git diz que o arquivo inteiro mudou”
A segunda linha de novo. Com git config core.autocrlf true, o checkout reescreve arquivos LF para CRLF no diretório de trabalho. Agora cada linha difere do blob por um byte, então o git diff marca todas as linhas como modificadas e o pull request exibe um arquivo em que ninguém tocou como se ele tivesse sido reescrito inteiro. Quem fez isso foi o filtro de checkout.
O caso inverso produz o mesmo ruído: um colega em false comita CRLF, você está em input, e arquivos que você nunca abriu aparecem no seu diff.
5. O .gitattributes é a resposta em nível de equipe
O core.autocrlf é uma configuração de uma máquina só, invisível para todo o resto do time. O .gitattributes é um arquivo dentro do repositório, então viaja em cada clone. Quando os dois discordam, os atributos vencem. As quatro formas testadas:
.gitattributes | core.autocrlf | Origem | Blob | Após o checkout | Vencedor |
|---|---|---|---|---|---|
* text=auto | false | CRLF | LF | LF | atributos |
* text eol=crlf | input | LF | LF | CRLF | atributos |
* -text | true | CRLF | CRLF | CRLF | atributos |
* text eol=lf | true | CRLF | LF | LF | atributos |
Vale guardar a terceira linha: -text desliga a conversão por completo e ainda assim vence o core.autocrlf=true. É assim que você protege arquivos cujos bytes precisam sobreviver intactos.
Um .gitattributes para copiar
* 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
O text=auto normaliza para LF no repositório tudo o que o Git detectar como texto. As linhas explícitas com eol=lf cobrem arquivos que precisam ser LF independentemente de quem os escreveu, porque um .sh com CRLF é um exit 126 esperando para acontecer. Os tipos de script do Windows recebem eol=crlf pelo motivo inverso, e os padrões binários recebem -text para que nada seja convertido.
core.safecrlf e core.eol
Duas configurações que aparecem ao lado do core.autocrlf e fazem outra coisa:
- O
core.safecrlfé uma trava, não um conversor. Quando uma conversão não sobreviveria a uma ida e volta, como num arquivo misto em que normalizar perde informação,truerejeita a operação ewarndeixa passar com um aviso. Ele nunca muda quais bytes são armazenados; só se recusa a fazer em silêncio as conversões que perdem dados. - O
core.eolescolhe qual terminador o Git escreve no diretório de trabalho para arquivos marcados comotext, quando ocore.autocrlfestá emfalse. Os valores sãolf,crlfenative. Ocore.autocrlftem precedência sobre ele, e é por isso que definircore.eolnuma máquina com o autocrlf ainda ligado costuma parecer que não fez nada.
Mudar o .gitattributes não conserta os arquivos já comitados
Os atributos valem no momento em que o Git escreve ou lê um arquivo, então o conteúdo existente fica como está até que algo o reescreva. Force essa passada você mesmo:
$ git add --renormalize .
$ git commit -m "Normalize line endings"
Espere um diff enorme, que é justamente o objetivo. Faça isso num branch separado, mescle num único commit e avise o time antes que ele caia no branch principal.
6. Convertendo CRLF para LF, e de volta
Quatro maneiras, todas verificadas removendo o \r no Darwin:
| Comando | Resultado |
|---|---|
tr -d '\r' < f > f.out | funciona |
perl -pi -e 's/\r\n/\n/g' f | funciona |
sed -i '' -e 's/\r$//' f (forma BSD) | funciona |
sed -i -e 's/\r$//' f (forma GNU, rodada no macOS) | funciona, e deixa um arquivo de lixo para trás |
A armadilha do sed -i do GNU no macOS
O sed do BSD exige que -i seja seguido de um sufixo de backup. Copie um tutorial de Linux ao pé da letra e o sed do BSD engole o -e como se fosse esse sufixo. A sua edição acontece assim mesmo, e isto também:
a5.txt
a5.txt-e
O a5.txt-e é uma cópia de backup, criada porque o sed leu -e como o sufixo que você pediu. A forma correta no macOS passa uma string vazia explícita: sed -i '' -e 's/\r$//' f. Um repositório com arquivos -e comitados é um repositório onde alguém rodou um one-liner GNU num Mac.
O macOS não tem dos2unix
command -v dos2unix não retorna nada num macOS de fábrica; o binário vem do brew install dos2unix. É por isso que a resposta mais copiada da internet falha justamente na máquina em que boa parte dos desenvolvedores está digitando. O tr -d '\r' não precisa de instalação e faz o mesmo serviço.
No sentido inverso, o unix2dos tem o mesmo problema de disponibilidade, e sed -e 's/$/\r/' serve de substituto.
Depois que o arquivo volta a ser LF limpo, dá para tratá-lo de novo com segurança como uma lista de linhas. Isso importa para qualquer coisa que compare linhas como strings inteiras: ordenar linhas de texto e remover linhas duplicadas leem value\r e value como duas linhas diferentes, então um único retorno de carro perdido derrota a deduplicação sem fazer barulho.
No seu editor
O VS Code mostra a quebra de linha do arquivo atual como CRLF ou LF na barra de status, no canto inferior direito, e clicar nela troca o arquivo. A configuração files.eol controla o padrão para arquivos novos, e defini-la por workspace mantém um time misto alinhado. Outros editores expõem os mesmos dois controles com outros nomes. O que passa despercebido é que o indicador por arquivo e a configuração padrão são coisas separadas: mudar um não mexe no outro.
7. Lidando com quebras de linha no código
Um arquivo, line1\r\nline2\r\n, lido por sete pontos de entrada:
| Ponto de entrada | O que você recebe | \r |
|---|---|---|
Node fs.readFileSync(f,"utf8") | "line1\r\nline2\r\n" | mantido |
Node, mesmo valor + .split("\n") | ["line1\r","line2\r",""] | mantido em todas as linhas |
Node readline com crlfDelay:Infinity | ["line1","line2"] | removido |
Python open(f), modo padrão | 'line1\nline2\n' | convertido |
Python open(f).readlines() | ['line1\n','line2\n'] | convertido |
Python open(f, newline="") | 'line1\r\nline2\r\n' | mantido |
Python open(f,"rb") | b'line1\r\nline2\r\n' | mantido |
Essa tabela resolve um relatório de bug bem conhecido: funciona em Python e quebra em Node. O modo texto padrão do Python aplica universal newlines e traduz \r\n para \n antes de você ver qualquer coisa. O Node entrega os bytes como estão. Nenhum dos dois está errado, mas os dois discordam no instante em que leem o mesmo arquivo.
rstrip("\n") deixa o \r para trás
original: 'line1\r\n' | rstrip("\n"): 'line1\r' | strip(): 'line1'
O rstrip("\n") remove exatamente os caracteres que você listou, e o \r não estava na lista. O resultado deixa de bater com aquilo que deveria, e essa é a resposta para “eu tirei e continua diferente”. Use strip(), ou rstrip() sem argumento, e todo o espaço em branco final vai embora, retorno de carro incluído.
Dividindo linhas com segurança nas duas plataformas
Em JavaScript, divida por um padrão que tolere os dois terminadores: text.split(/\r?\n/). Em Python, ou fique no modo texto padrão e deixe as universal newlines resolverem, ou chame splitlines(), que dá conta de \r\n, de \n e de um \r sozinho.
Escrever é a outra metade. O Node grava os bytes que você der a ele, então monte as strings com \n e deixe o .gitattributes decidir o que cai no disco. Já o open(path, "w") do Python traduz \n para o terminador da plataforma, a menos que você passe newline="". É por isso que os escritores de CSV pedem esse parâmetro.
Um retorno de carro no fim de uma linha e um byte-order mark no começo de um arquivo são a mesma categoria de bug vista de pontas opostas: um byte invisível que sobrevive ao copiar e colar e quebra uma comparação de igualdade. O guia de solução do BOM UTF-8 cobre a outra ponta.
8. Onde mais as quebras de linha mordem
CSV e Excel. A RFC 4180 especifica o CRLF como separador de registros, o que faz do CSV um dos poucos lugares em que o CRLF está certo em vez de ser um defeito. Parsers escritos para entrada só com LF deixam um \r no último campo de cada linha, então, se os valores de uma conversão de CSV para JSON parecem certos mas não batem na comparação, confira esse byte primeiro.
Docker. Um .sh com CRLF copiado para dentro de uma imagem é o modo de falha número quatro. O COPY preserva os bytes, o shebang mantém o seu \r, e o contêiner sai com 126. Uma linha *.sh text eol=lf no .gitattributes previne a classe inteira.
Diffs e pull requests. Um arquivo marcado como totalmente alterado sem nenhuma edição visível é o mecanismo da seção 4 chegando ao code review. Comparar as duas versões com o comparador de texto confirma isso em segundos, e o guia de comparação de texto mostra como ler o resultado.
Arquivos mistos. ASCII text, with CRLF, LF line terminators significa dois autores com configurações diferentes. Normalize o arquivo inteiro em vez das linhas que você por acaso editou, ou o próximo diff será igualmente barulhento.
FAQ
Qual é a diferença entre CRLF e LF?
CRLF são dois bytes, \r\n (0x0D 0x0A). LF é um byte, \n (0x0A). O Windows escreve CRLF, o Linux e o macOS escrevem LF, e os dois marcam o fim de uma linha. O texto parece idêntico num editor; a diferença só aparece nos bytes, nas comparações de string e nos diffs.
Meu script está com quebras de linha CRLF. Por que não aparece nenhum erro?
Porque comandos simples sobrevivem ao byte extra. echo hello com um \r no fim roda e sai com 0. Os erros só aparecem onde o parser se importa: uma linha em branco, uma palavra-chave como fi, ou o shebang. As atribuições são o caso perigoso, porque elas dão certo e guardam o \r dentro da variável.
O que significa $'\r': command not found e por que eu não vejo essa mensagem?
Significa que o shell tentou executar uma linha que continha só um retorno de carro. O bash 4 e o 5 imprimem esse caractere com ANSI-C quoting, o que dá $'\r'. O bash 3.2 que vem com o macOS não imprime nada entre os dois-pontos, e o zsh imprime ^M no lugar. A mesma falha, três mensagens diferentes.
O core.autocrlf deve ficar em true, input ou false?
Use input no Linux e no macOS, true no Windows, e prefira o .gitattributes a qualquer um dos dois. Na medição, true e input guardam LF no repositório e só o false deixa CRLF entrar num commit. O true ainda reescreve arquivos LF para CRLF na sua cópia de trabalho durante o checkout.
Se o .gitattributes e o core.autocrlf discordarem, qual vence?
O .gitattributes vence. As quatro formas testadas, * text=auto, * text eol=crlf, * -text e * text eol=lf, sobrepuseram o valor local de core.autocrlf. Esse é o argumento para usá-lo: os atributos são comitados e valem para todo mundo, enquanto o core.autocrlf é uma configuração por máquina que você não consegue ver nem impor.
Como eu descubro se um arquivo usa CRLF ou LF?
Rode file yourfile. Com CRLF ele imprime ASCII text, with CRLF line terminators, com LF puro imprime ASCII text sem sufixo nenhum, e um arquivo misto imprime with CRLF, LF line terminators. Para ter certeza no nível do byte, rode od -c e procure um \r logo antes de cada \n.
Quando realmente se deve usar CRLF?
Quando um formato ou protocolo exigir. A RFC 4180 define o CRLF como separador de registros do CSV, e os cabeçalhos HTTP e o SMTP fazem o mesmo na rede. Arquivos batch e PowerShell do Windows também ficam mais seguros com CRLF. Em todo o resto, código-fonte, scripts de shell, configuração, use LF.