Como adicionar uma nova linha ao final de um arquivo?


190

Usando sistemas de controle de versão, fico aborrecido com o barulho quando o diff diz No newline at end of file.

Então, eu estava pensando: como adicionar uma nova linha no final de um arquivo para se livrar dessas mensagens?



11
Boa solução abaixo, que limpa todos os arquivos recursivamente. Resposta por @Patrick Oscity
Qwerty


No futuro, os editores de texto geralmente têm opções para garantir que haja uma nova linha à direita que você e seus colaboradores possam usar para se manterem limpos.
Nick T

Respostas:


44

Para higienizar recursivamente um projeto, eu uso este oneliner:

git ls-files -z | while IFS= read -rd '' f; do tail -c1 < "$f" | read -r _ || echo >> "$f"; done

Explicação:

  • git ls-files -zlista arquivos no repositório. É necessário um padrão opcional como parâmetro adicional, que pode ser útil em alguns casos, se você quiser restringir a operação a determinados arquivos / diretórios. Como alternativa, você pode usar find -print0 ...ou programas semelhantes para listar os arquivos afetados - apenas certifique-se de que ele emite NULentradas delimitadas.

  • while IFS= read -rd '' f; do ... done itera pelas entradas, manipulando com segurança os nomes de arquivos que incluem espaços em branco e / ou novas linhas.

  • tail -c1 < "$f" lê o último caractere de um arquivo.

  • read -r _ sai com um status de saída diferente de zero se uma nova linha à direita estiver ausente.

  • || echo >> "$f" anexa uma nova linha ao arquivo se o status de saída do comando anterior for diferente de zero.


Você também pode fazer isso assim, se quiser apenas higienizar um subconjunto de seus arquivos:find -name \*.java | while read f; do tail -n1 $f | read -r _ || echo >> $f; done
Por Lundberg

@ StéphaneChazelas boas sugestões, tentará incorporar isso na minha resposta.
Patrick Oscity 22/03

@PerLundberg, você também pode passar um padrão para o git ls-filesqual ainda o salvará de editar arquivos que não são rastreados no controle de versão.
Patrick Oscity 22/03

@ StéphaneChazelas adicionando o IFS= para desarmar o separador é bom para preservar o espaço em branco ao redor. As entradas terminadas nulas são relevantes apenas se você tiver arquivos ou diretórios com uma nova linha em seu nome, o que parece meio improvável, mas é a maneira mais correta de lidar com o caso genérico, eu concordo. Como uma pequena ressalva: a -dopção para readnão está disponível no POSIX sh.
Patrick Oscity 22/03

Sim, daí o meu zsh / bash . Consulte também meu uso de tail -n1 < "$f"para evitar problemas com nomes de arquivos iniciados por -( tail -n1 -- "$f"não funciona para o arquivo chamado -). Você pode esclarecer que a resposta agora é específica para zsh / bash.
Stéphane Chazelas

203

Aqui você vai :

sed -i -e '$a\' file

E como alternativa para o OS X sed:

sed -i '' -e '$a\' file

Isso adiciona \nno final do arquivo apenas se ele ainda não terminar com uma nova linha. Portanto, se você executá-lo duas vezes, ele não adicionará outra nova linha:

$ cd "$(mktemp -d)"
$ printf foo > test.txt
$ sed -e '$a\' test.txt > test-with-eol.txt
$ diff test*
1c1
< foo
\ No newline at end of file
---
> foo
$ echo $?
1
$ sed -e '$a\' test-with-eol.txt > test-still-with-one-eol.txt
$ diff test-with-eol.txt test-still-with-one-eol.txt
$ echo $?
0

11
@jwd: De man sed: $ Match the last line.Mas talvez funcione apenas por acidente. Sua solução também funciona.
L0b0

11
Sua solução também é mais elegante, e eu a testei e a comprometi , mas como pode funcionar? Se $corresponde à última linha, por que não adiciona outra nova linha a uma string que contém uma nova linha?
L0b0

27
Existem dois significados diferentes de $. Dentro de uma regex, como no formulário /<regex>/, ela tem o significado usual de "correspondência de final de linha". Caso contrário, usado como um endereço, sed atribui o significado especial "última linha no arquivo". O código funciona porque sed, por padrão, acrescenta uma nova linha à saída, se ainda não estiver lá. O código "$ a \" diz apenas "corresponde à última linha do arquivo e não adiciona nada a ele". Mas implicitamente, o sed adiciona a nova linha a cada linha que processa (como esta $linha), se ainda não estiver lá.
jwd

11
Em relação à página de manual: A cotação a que você está se referindo está na seção "Endereços". Colocá-lo dentro /regex/dá um significado diferente. As páginas de manual do FreeBSD são um pouco mais informativo, eu penso: freebsd.org/cgi/man.cgi?query=sed
jwd

2
Se o arquivo já terminar em uma nova linha, isso não será alterado, mas será reescrito e atualizado seu carimbo de data / hora. Isso pode ou não importar.
Keith Thompson

39

Dar uma olhada:

$ echo -n foo > foo 
$ cat foo
foo$
$ echo "" >> foo
$ cat foo
foo

então echo "" >> noeol-filedeve fazer o truque. (Ou você quis pedir para identificar esses arquivos e corrigi-los?)

edit removeu o ""de echo "" >> foo(veja o comentário de @ yuyichao) edit2 adicionou ""novamente ( mas veja o comentário de @Keith Thompson)


4
o ""não é necessário (pelo menos para bash) e tail -1 | wc -lpode ser usado para descobrir o arquivo sem uma nova linha no final
yuyichao

5
@yuyichao: O ""não é necessário para o bash, mas vi echoimplementações que não imprimem nada quando invocadas sem argumentos (embora nenhuma das que eu possa encontrar agora faça isso). echo "" >> noeol-fileé provavelmente um pouco mais robusto. printf "\n" >> noeol-fileé ainda mais.
21412 Keith Thompson

2
@KeithThompson, csh's echoé a única conhecida a saída nada quando não passou qualquer argumento. Mas então, se formos apoiar conchas que não sejam do tipo Bourne, deveríamos fazê-lo em echo ''vez de echo ""como echo ""seria o resultado ""<newline>com rcou espor exemplo.
Stéphane Chazelas

11
@ StéphaneChazelas: E tcsh, ao contrário csh, imprime uma nova linha quando invocada sem argumentos - independentemente da configuração de $echo_style.
Keith Thompson

16

Outra solução usando ed. Esta solução afeta apenas a última linha e somente se \nestiver ausente:

ed -s file <<< w

Funciona essencialmente abrindo o arquivo para edição através de um script, o script é o único wcomando que grava o arquivo de volta no disco. É baseado nesta frase encontrada na ed(1)página de manual:

LIMITAÇÕES
       (...)

       Se um arquivo de texto (não binário) não for finalizado por um caractere de nova linha,
       então ed acrescenta um na leitura / gravação. No caso de um binário
       O arquivo ed não acrescenta uma nova linha na leitura / gravação.

11
Isso não adiciona uma nova linha para mim.
Olhovsky

4
Funciona para mim; até imprime "Nova linha anexada" (ed-1.10-1 no Arch Linux).
Stefan Majewsky

12

Uma maneira simples, portátil e compatível com POSIX de adicionar uma nova linha final ausente a um arquivo de texto seria:

[ -n "$(tail -c1 file)" ] && echo >> file

Essa abordagem não precisa ler o arquivo inteiro; pode simplesmente procurar EOF e trabalhar a partir daí.

Essa abordagem também não precisa criar arquivos temporários nas suas costas (por exemplo, sed -i), para que os hardlinks não sejam afetados.

echo acrescenta uma nova linha ao arquivo somente quando o resultado da substituição do comando for uma sequência não vazia. Observe que isso só pode acontecer se o arquivo não estiver vazio e o último byte não for uma nova linha.

Se o último byte do arquivo for uma nova linha, tail retorna-o e a substituição de comando o remove; o resultado é uma sequência vazia. O teste -n falha e o eco não é executado.

Se o arquivo estiver vazio, o resultado da substituição do comando também será uma cadeia vazia e, novamente, o eco não será executado. Isso é desejável, porque um arquivo vazio não é um arquivo de texto inválido, nem é equivalente a um arquivo de texto não vazio com uma linha vazia.


11
Observe que ele não funciona yashse o último caractere no arquivo for um caractere de vários bytes (em localidades UTF-8, por exemplo), ou se o local for C e o último byte no arquivo tiver o oitavo bit definido. Com outros shells (exceto zsh), ele não adicionaria uma nova linha se o arquivo terminasse em um byte NUL (mas, novamente, isso significaria que a entrada seria sem texto mesmo após a adição de uma nova linha).
Stéphane Chazelas


11
É possível executar isso para todos os arquivos em uma pasta e subpastas?
Qwerty 24/03

12

Adicione nova linha, independentemente:

echo >> filename

Aqui está uma maneira de verificar se uma nova linha existe no final antes de adicionar uma, usando Python:

f=filename; python -c "import sys; sys.exit(open(\"$f\").read().endswith('\n'))" && echo >> $f

11
Eu não usaria a versão python em nenhum tipo de loop por causa do tempo lento de inicialização do python. Claro que você poderia fazer o loop em python, se quisesse.
Kevin Cox

2
O tempo de inicialização do Python é de 0,03 segundos aqui. Você realmente considera isso problemático?
Alexander

3
O tempo de inicialização importa se você chamar python em um loop, é por isso que eu disse que considere fazer o loop em python. Então você incorre no custo de inicialização apenas uma vez. Para mim, metade do custo da inicialização é mais da metade do tempo de todo o snipit, eu consideraria essa sobrecarga substancial. (Novamente, irrelevante se fazendo apenas um pequeno número de arquivos)
Kevin Cox

2
echo ""parece mais robusto que echo -n '\n'. Ou você pode usarprintf '\n'
Keith Thompson

2
Isso funcionou bem para mim
Daniel Gomez Rico

8

A solução mais rápida é:

[ -n "$(tail -c1 file)" ] && printf '\n' >>file 

  1. É muito rápido.
    Em um arquivo de tamanho médio, seq 99999999 >fileisso leva milissegundos.
    Outras soluções levam muito tempo:

    [ -n "$(tail -c1 file)" ] && printf '\n' >>file  0.013 sec
    vi -ecwq file                                    2.544 sec
    paste file 1<> file                             31.943 sec
    ed -s file <<< w                             1m  4.422 sec
    sed -i -e '$a\' file                         3m 20.931 sec
  2. Funciona em ash, bash, lksh, mksh, ksh93, attsh e zsh, mas não em yash.

  3. Não altera o registro de data e hora do arquivo se não for necessário adicionar uma nova linha.
    Todas as outras soluções apresentadas aqui alteram o registro de data e hora do arquivo.
  4. Todas as soluções acima são POSIX válidas.

Se você precisar de uma solução portátil para yash (e todos os outros shells listados acima), ela poderá ser um pouco mais complexa:

f=file
if       [ "$(tail -c1 "$f"; echo x)" != "$(printf '\nx')" ]
then     printf '\n' >>"$f"
fi

7

A maneira mais rápida de testar se o último byte de um arquivo é uma nova linha é ler apenas esse último byte. Isso poderia ser feito tail -c1 file. No entanto, a maneira simplista de testar se o valor do byte é uma nova linha, dependendo da remoção usual do shell de uma nova linha à direita dentro de uma expansão de comando falha (por exemplo) em yash, quando o último caractere do arquivo é UTF- 8 valor.

A maneira correta, compatível com POSIX, de todos os shells (razoáveis) para descobrir se o último byte de um arquivo é uma nova linha é usar xxd ou hexdump:

tail -c1 file | xxd -u -p
tail -c1 file | hexdump -v -e '/1 "%02X"'

Em seguida, comparar a saída acima para 0Afornecerá um teste robusto.
É útil evitar adicionar uma nova linha a um arquivo vazio.
Arquivo que falhará em fornecer um último caractere 0A, é claro:

f=file
a=$(tail -c1 "$f" | hexdump -v -e '/1 "%02X"')
[ -s "$f" -a "$a" != "0A" ] && echo >> "$f"

Curto e grosso. Isso leva muito pouco tempo, pois apenas lê o último byte (procure EOF). Não importa se o arquivo é grande. Em seguida, adicione apenas um byte, se necessário.

Nenhum arquivo temporário é necessário nem usado. Não há hardlinks afetados.

Se esse teste for executado duas vezes, ele não adicionará outra nova linha.


11
@ Crw Eu acredito que ele adiciona informações úteis.
sorontar 14/01

2
Observe que xxdnem hexdumpos utilitários POSIX nem são. No baú da ferramenta POSIX, existe od -An -tx1o valor hexadecimal de um byte.
Stéphane Chazelas

@ StéphaneChazelas Por favor, poste isso como resposta; Eu vim aqui procurando esse comentário muitas vezes :)
kelvin


Observe que o POSIX não garante que o valor de LF seja 0x0a. Ainda existem sistemas POSIX onde não são (baseados em EBCDIC), embora sejam extremamente raros atualmente.
Stéphane Chazelas 20/09

4

É melhor corrigir o editor do usuário que editou o arquivo pela última vez. Se você é a última pessoa a editar o arquivo - que editor você está usando, eu acho que ele tem um colega de texto ..?


2
Vim é o editor em questão. Mas, em geral, você está certo, eu deveria não só corrigir os sintomas;)
k0pernikus

6
para o vim, você precisa se esforçar e fazer a dança do arquivo binário ao salvar para que o vim não adicione uma nova linha no final do arquivo - apenas não faça essa dança. OR, arquivos simplesmente corretos existentes abri-los no vim e salve o arquivo e vim irá 'consertar' a nova linha faltando para você (pode ser facilmente script para vários arquivos)
AD7six

3
Meu emacsnão adiciona uma nova linha no final do arquivo.
enzotib

2
Obrigado pelo comentário @ AD7six, eu continuo recebendo relatórios fantasmas de diffs quando eu comprometo as coisas, sobre como o arquivo original não tem uma nova linha no final. Não importa como edite um arquivo com o vim, não consigo colocar uma nova linha lá. Então é só vim fazer isso.
Steven Lu

11
@enzotib: Eu tenho (setq require-final-newline 'ask)na minha.emacs
Keith Thompson

3

Se você deseja adicionar rapidamente uma nova linha ao processar algum pipeline, use o seguinte:

outputting_program | { cat ; echo ; }

também é compatível com POSIX.

Então, é claro, você pode redirecioná-lo para um arquivo.


2
O fato de eu poder usar isso em um pipeline é útil. Isso me permite contar o número de linhas em um arquivo CSV, excluindo o cabeçalho. E ajuda a obter uma contagem precisa de linhas nos arquivos do Windows que não terminam com uma nova linha ou retorno de carro. cat file.csv | tr "\r" "\n" | { cat; echo; } | sed "/^[[:space:]]*$/d" | tail -n +2 | wc -l
Kyle Tolle

3

Desde que não haja nulos na entrada:

paste - <>infile >&0

... seria suficiente sempre acrescentar apenas uma nova linha ao final de um infile se ele ainda não tivesse um. E ele só precisa ler o arquivo de entrada uma vez para acertar.


Isso não funcionará assim, pois stdin e stdout compartilham a mesma descrição de arquivo aberto (portanto, o cursor dentro do arquivo). Você precisaria em seu paste infile 1<> infilelugar.
Stéphane Chazelas

2

Embora não responda diretamente à pergunta, aqui está um script relacionado que escrevi para detectar arquivos que não terminam em nova linha. É muito rápido.

find . -type f | # sort |        # sort file names if you like
/usr/bin/perl -lne '
   open FH, "<", $_ or do { print " error: $_"; next };
   $pos = sysseek FH, 0, 2;                     # seek to EOF
   if (!defined $pos)     { print " error: $_"; next }
   if ($pos == 0)         { print " empty: $_"; next }
   $pos = sysseek FH, -1, 1;                    # seek to last char
   if (!defined $pos)     { print " error: $_"; next }
   $cnt = sysread FH, $c, 1;
   if (!$cnt)             { print " error: $_"; next }
   if ($c eq "\n")        { print "   EOL: $_"; next }
   else                   { print "no EOL: $_"; next }
'

O script perl lê uma lista de nomes de arquivo (opcionalmente classificados) do stdin e para cada arquivo lê o último byte para determinar se o arquivo termina em uma nova linha ou não. É muito rápido porque evita a leitura de todo o conteúdo de cada arquivo. Ele gera uma linha para cada arquivo que lê, prefixado com "error:" se ocorrer algum tipo de erro, "empty:" se o arquivo estiver vazio (não termina com nova linha!), "EOL:" ("final de line ") se o arquivo terminar com nova linha e" no EOL: "se o arquivo não terminar com nova linha.

Nota: o script não trata nomes de arquivos que contêm novas linhas. Se você estiver em um sistema GNU ou BSD, poderá lidar com todos os nomes de arquivos possíveis adicionando -print0 para encontrar, -z para classificar e -0 para perl, assim:

find . -type f -print0 | sort -z |
/usr/bin/perl -ln0e '
   open FH, "<", $_ or do { print " error: $_"; next };
   $pos = sysseek FH, 0, 2;                     # seek to EOF
   if (!defined $pos)     { print " error: $_"; next }
   if ($pos == 0)         { print " empty: $_"; next }
   $pos = sysseek FH, -1, 1;                    # seek to last char
   if (!defined $pos)     { print " error: $_"; next }
   $cnt = sysread FH, $c, 1;
   if (!$cnt)             { print " error: $_"; next }
   if ($c eq "\n")        { print "   EOL: $_"; next }
   else                   { print "no EOL: $_"; next }
'

Obviamente, você ainda precisará criar uma maneira de codificar os nomes dos arquivos com novas linhas na saída (deixadas como um exercício para o leitor).

A saída pode ser filtrada, se desejado, para acrescentar uma nova linha aos arquivos que não possuem uma, simplesmente com

 echo >> "$filename"

A falta de uma nova linha final pode causar erros nos scripts, já que algumas versões do shell e outros utilitários não tratam adequadamente uma nova linha final ausente ao ler esse arquivo.

Na minha experiência, a falta de uma nova linha final é causada pelo uso de vários utilitários do Windows para editar arquivos. Eu nunca vi o vim causar uma nova linha final ausente ao editar um arquivo, embora ele relate esses arquivos.

Por fim, existem scripts muito mais curtos (mas mais lentos) que podem fazer um loop sobre as entradas de nome de arquivo para imprimir os arquivos que não terminam em nova linha, como:

/usr/bin/perl -ne 'print "$ARGV\n" if /.\z/' -- FILE1 FILE2 ...

1

Os editores vi/ vim/ são exadicionados automaticamente <EOL>ao EOF, a menos que o arquivo já o possua.

Então tente:

vi -ecwq foo.txt

que é equivalente a:

ex -cwq foo.txt

Testando:

$ printf foo > foo.txt && wc foo.txt
0 1 3 foo.txt
$ ex -scwq foo.txt && wc foo.txt
1 1 4 foo.txt

Para corrigir vários arquivos, verifique: Como corrigir 'Nenhuma nova linha no final do arquivo' para muitos arquivos? na SO

Por que isso é tão importante? Para manter nossos arquivos compatíveis com POSIX .


0

Para aplicar a resposta aceita a todos os arquivos no diretório atual (mais subdiretórios):

$ find . -type f -exec sed -i -e '$a\' {} \;

Isso funciona no Linux (Ubuntu). No OS X, você provavelmente precisará usar -i ''(não testado).


4
Observe que find .lista todos os arquivos, incluindo os arquivos em .git. Para excluir:find . -type f -not -path './.git/*' -exec sed -i -e '$a\' {} \;
friederbluemle 14/07/2015

Gostaria de ter lido este comentário / pensamento antes de executá-lo. Ah bem.
kstev

0

Pelo menos nas versões GNU, simplesmente grep ''ouawk 1 canoniza sua entrada, adicionando uma nova linha final, se ainda não estiver presente. Eles copiam o arquivo no processo, o que leva tempo se for grande (mas a fonte não deve ser muito grande para ser lida de qualquer maneira?) E atualiza o modtime, a menos que você faça algo como

 mv file old; grep '' <old >file; touch -r old file

(embora isso possa estar bem em um arquivo que você está fazendo check-in porque o modificou) e perde links físicos, permissões não padrão e ACLs, etc., a menos que você seja ainda mais cuidadoso.


Ou apenas grep '' file 1<> file, apesar de ainda assim ler e gravar o arquivo completamente.
Stéphane Chazelas

-1

Isso funciona no AIX ksh:

lastchar=`tail -c 1 *filename*`
if [ `echo "$lastchar" | wc -c` -gt "1" ]
then
    echo "/n" >> *filename*
fi

No meu caso, se o arquivo estiver faltando a nova linha, o wccomando retornará um valor de 2e escreveremos uma nova linha.


O feedback virá em forma de votação positiva ou negativa, ou você será solicitado nos comentários para descrever mais suas respostas / perguntas, sem motivo para solicitá-lo no corpo da resposta. Mantenha-o direto ao ponto, bem-vindo ao stackexchange!
K0pernikus

-1

Adicionando a resposta de Patrick Oscity , se você quiser aplicá-la a um diretório específico, também poderá usar:

find -type f | while read f; do tail -n1 $f | read -r _ || echo >> $f; done

Execute isso dentro do diretório ao qual você gostaria de adicionar novas linhas.


-1

echo $'' >> <FILE_NAME> adicionará uma linha em branco ao final do arquivo.

echo $'\n\n' >> <FILE_NAME> adicionará 3 linhas em branco ao final do arquivo.


O Stackexchange tem uma formatação engraçado, eu fixa-lo para você :-)
peterh

-1

Se seu arquivo for finalizado com finais de linha do Windows\r\n e você estiver no Linux, poderá usar este sedcomando. Ele só será adicionado \r\nà última linha se ainda não estiver lá:

sed -i -e '$s/\([^\r]\)$/\1\r\n/'

Explicação:

-i    replace in place
-e    script to run
$     matches last line of a file
s     substitute
\([^\r]\)$    search the last character in the line which is not a \r
\1\r\n    replace it with itself and add \r\n

Se a última linha já contiver um \r\n, a regexp de pesquisa não corresponderá, portanto nada acontecerá.


-1

Você pode escrever um fix-non-delimited-linescript como:

#! /bin/zsh -
zmodload zsh/system || exit
ret=0
for file do
  if sysopen -rwu0 -- "$file"; then
    if sysseek -w end -1; then
      read -r x || print -u0
    else
      syserror -p "Can't seek in $file before the last byte: "
      ret=1
    fi
  else
    ret=1
  fi
done
exit $ret

Ao contrário de algumas das soluções fornecidas aqui,

  • deve ser eficiente, pois não bifurca nenhum processo, apenas lê um byte para cada arquivo e não reescreve o arquivo (apenas acrescenta uma nova linha)
  • não quebrará links simbólicos / hardlinks ou afetará metadados (também, o ctime / mtime é atualizado somente quando uma nova linha é adicionada)
  • deve funcionar bem, mesmo que o último byte seja um NUL ou faça parte de um caractere de vários bytes.
  • deve funcionar bem, independentemente de quais caracteres ou não-caracteres os nomes de arquivo podem conter
  • Deve manipular arquivos corretamente ilegíveis, graváveis ​​ou inaceitáveis ​​(e relatar erros de acordo)
  • Não deve adicionar uma nova linha aos arquivos vazios (nesse caso, relata um erro sobre uma busca inválida)

Você pode usá-lo, por exemplo, como:

that-script *.txt

ou:

git ls-files -z | xargs -0 that-script

POSIX, você poderia fazer algo funcionalmente equivalente a

export LC_ALL=C
ret=0
for file do
  [ -s "$file" ] || continue
  {
    c=$(tail -c 1 | od -An -vtc)
    case $c in
      (*'\n'*) ;;
      (*[![:space:]]*) printf '\n' >&0 || ret=$?;;
      (*) ret=1;; # tail likely failed
    esac
  } 0<> "$file" || ret=$? # record failure to open
done
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.