Como remover várias novas linhas no EOF?

25

Tenho arquivos que terminam em uma ou mais novas linhas e devem terminar em apenas uma nova linha. Como posso fazer isso com as ferramentas Bash / Unix / GNU?

Exemplo de arquivo incorreto:

1\n
\n
2\n
\n
\n
3\n
\n
\n
\n

Exemplo de arquivo corrigido:

1\n
\n
2\n
\n
\n
3\n

Em outras palavras: deve haver exatamente uma nova linha entre o EOF e o último caractere não-nova linha do arquivo.

Implementação de referência

Leia o conteúdo do arquivo, corte uma única nova linha até que não haja mais duas novas no final e escreva de volta:

#! /bin/python

import sys

with open(sys.argv[1]) as infile:
    lines = infile.read()

while lines.endswith("\n\n"):
    lines = lines[:-1]

with open(sys.argv[2], 'w') as outfile:
    for line in lines:
        outfile.write(line)

Esclarecimento: É claro que a tubulação é permitida, se isso for mais elegante.

Bengt
fonte

Respostas:

16
awk '/^$/ {nlstack=nlstack "\n";next;} {printf "%s",nlstack; nlstack=""; print;}' file
Hauke ​​Laging
fonte
2
+1: as soluções da awk são (quase) sempre elegantes e legíveis!
Olivier Dulac
@OlivierDulac De fato. Quando eu vi a sedproposta Eu apenas pensei OMG ...
Hauke Laging
11
isso não funciona no OSX Mavericks usando o awk mais recente disponível do Homebrew. Ele com erros awk: illegal statement. brew install mawke alterar o comando para mawkfunciona embora.
tjmcewan
@noname Eu não entendo a pergunta ...
Hauke Laging
Qualquer awk em que o script não funcione é um awk gravemente quebrado - pare de usá-lo e obtenha um novo awk, porque se não puder fazer isso, quem sabe que outras quebras ele possui.
Ed Morton
21

De scripts úteis de uma linha para sed .

# Delete all trailing blank lines at end of file (only).
sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba' file
Alexey Shmalko
fonte
4
Obrigado, usei o seguinte para fazer isso em vários arquivos: find . -type f -name '*.js' -exec sed --in-place -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
jakub.g
@ jakub.g no lugar e recursivo é exatamente o que eu precisava. obrigado.
precisa
Para adicionar o excelente comentário de @ jakub.g você pode invocar o comando como este no OS X:find . -type f -name '*.js' -exec sed -i '' -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
davejagoda
18

Como você já tem respostas com as ferramentas mais adequadas sed e awk; você pode aproveitar o fato de $(< file)remover linhas em branco à direita.

a=$(<file); printf '%s\n' "$a" > file

Esse hack barato não funcionaria para remover as linhas em branco à direita, que podem conter espaços ou outros caracteres não imprimíveis, apenas para remover as linhas vazias à direita. Também não funcionará se o arquivo contiver bytes nulos.

Em shells diferentes de bash e zsh, use em $(cat file)vez de $(<file).

llua
fonte
+1 para indicar o que parece um bug para mim: $ (<file) não está realmente lendo o arquivo? por que descartar novas linhas finais? (ele faz, eu só testado, obrigado por apontar isso!)
Olivier Dulac
2
@OlivierDulac $()descarta novas linhas à direita. Essa é uma decisão de design. Suponho que isso facilite a integração em outras strings: echo "On $(date ...) we will meet."seria ruim com a nova linha que quase todo comando shell gera no final.
Hauke ​​Laging
@HaukeLaging: bom ponto, é provavelmente a fonte de que o comportamento
Olivier Dulac
Eu adicionei um caso especial para evitar acrescentando "\ n" para esvaziar arquivos: [[ $a == '' ]] || printf '%s\n' "$a" >"$file".
Davidchambers
Para tirar várias novas linhas fora do início de um arquivo, inserir tac no processo (eu uso coreutils gnu no Mac, então GTAC para mim):a=$(gtac file.txt); printf '%s\n' "$a" | gtac > file.txt
r_alex_hall
5

Você pode usar esse truque com cat& printf:

$ printf '%s\n' "`cat file`"

Por exemplo

$ printf '%s\n' "`cat ifile`" > ofile
$ cat -e ofile
1$
$
2$
$
$
3$

O $indica o final de uma linha.

Referências

slm
fonte
4

Esta pergunta está marcada com , mas ninguém propôs uma edsolução.

Aqui está um:

ed -s file <<'ED_END'
a

.
?^..*?+1,.d
w
ED_END

ou equivalente,

printf '%s\n' a '' . '?^..*?+1,.d' w | ed -s file

ed colocará você na última linha do buffer de edição por padrão na inicialização.

O primeiro comando ( a) adiciona uma linha vazia ao final do buffer (a linha vazia no script de edição é essa linha e o ponto ( .) é apenas para retornar ao modo de comando).

O segundo comando ( ?) procura a linha anterior mais próxima que contém algo (até caracteres de espaço em branco) e exclui tudo até o final do buffer a partir da próxima linha.

O terceiro comando ( w) grava o arquivo de volta no disco.

A linha vazia adicionada protege o restante do arquivo de ser excluído no caso de não haver linhas vazias no final do arquivo original.

Kusalananda
fonte
3

Aqui está uma solução Perl que não requer a leitura de mais de uma linha na memória por vez:

my $n = 0;
while (<>) {
    if (/./) {
        print "\n" x $n, $_;
        $n = 0;
    } else {
        $n++;
    }
}

ou, como uma linha:

perl -ne 'if (/./) { print "\n" x $n, $_; $n = 0 } else { $n++ }'

Isso lê o arquivo uma linha de cada vez e verifica cada linha para ver se contém um caractere que não seja de nova linha. Caso contrário, incrementa um contador; se o fizer, imprime o número de novas linhas indicadas pelo contador, seguidas pela própria linha e, em seguida, redefine o contador.

Tecnicamente, até o buffer de uma única linha na memória é desnecessário; seria possível resolver esse problema usando uma quantidade constante de memória lendo o arquivo em pedaços de comprimento fixo e processando-o caractere por caractere usando uma máquina de estado. No entanto, suspeito que seria desnecessariamente complicado para o caso de uso típico.

Ilmari Karonen
fonte
1

Se o seu arquivo for pequeno o suficiente para armazenar na memória, você poderá usar este

perl -e 'local($/);$f=<>; $f=~s/\n*$/\n/;print $f;' file
terdon
fonte
0

Em python (eu sei que não é o que você deseja, mas é muito melhor, pois é otimizado e um prelúdio para a versão do bash) sem reescrever o arquivo e sem ler todo o arquivo (o que é bom se o arquivo for muito grande):

#!/bin/python
import sys
infile = open(sys.argv[1], 'r+')
infile.seek(-1, 2)
while infile.read(1) == '\n':
  infile.seek(-2, 1)
infile.seek(1, 1)
infile.truncate()
infile.close()

Observe que ele não funciona em arquivos onde o caractere EOL não é '\ n'.

jfg956
fonte
0

Uma versão bash, implementando o algoritmo python, mas menos eficiente, pois precisa de muitos processos:

#!/bin/bash
n=1
while test "$(tail -n $n "$1")" == ""; do
  ((n++))
done
((n--))
truncate -s $(($(stat -c "%s" "$1") - $n)) "$1"
jfg956
fonte
0

Este é rápido de digitar e, se você conhece o sed, fácil de lembrar:

tac < file | sed '/[^[:blank:]]/,$!d' | tac

Ele usa o script sed para excluir as principais linhas em branco dos scripts úteis de uma linha para sed , referenciados por Alexey acima e tac (reverse cat).

Em um teste rápido, em um arquivo de linha de 18MB e 64.000 linhas, a abordagem de Alexey foi mais rápida (0,036 vs 0,046 segundos).

freeB
fonte