Como posso excluir uma nova linha se este for o último caractere de um arquivo?

162

Eu tenho alguns arquivos que gostaria de excluir a última nova linha, se for o último caractere de um arquivo. od -cmostra que o comando que eu executo grava o arquivo com uma nova linha à direita:

0013600   n   t  >  \n

Eu tentei alguns truques com o sed, mas o melhor que pude pensar não está fazendo o truque:

sed -e '$s/\(.*\)\n$/\1/' abc

Alguma idéia de como fazer isso?

Todd Partridge 'Gen2ly'
fonte
4
newline é apenas um caractere para novas linhas unix. As novas linhas do DOS são dois caracteres. Obviamente, "\ n" literal tem dois caracteres. O que você está realmente procurando?
Pausado até novo aviso.
3
Embora a representação pode ser \n, no linux é é um personagem
pavium
10
Você pode explicar por que deseja fazer isso? Os arquivos de texto devem terminar com um final de linha, a menos que estejam totalmente vazios. Parece-me estranho que você gostaria de ter um arquivo tão truncado?
Thomas Padron-McCarthy
O motivo usual para fazer algo assim é excluir uma vírgula à direita da última linha de um arquivo CSV. O Sed funciona bem, mas as novas linhas precisam ser tratadas de maneira diferente.
pavium
9
@ ThomasPadron-McCarthy "Na computação, por todas as boas razões para fazer algo, existe uma boa razão para não fazê-lo e vice-versa." -Jesus - "você não deve fazer isso" é uma resposta horrível, independentemente da pergunta. O formato correto é: [como fazê-lo] mas [por que pode ser uma má ideia]. #sacrilege
Cory Mawhorter

Respostas:

223
perl -pe 'chomp if eof' filename >filename2

ou, para editar o arquivo no local:

perl -pi -e 'chomp if eof' filename

[Nota do editor: -pi -efoi originalmente -pie, mas, como observado por vários comentaristas e explicado por @hvd, o último não funciona.]

Isso foi descrito como uma 'blasfêmia perl' no site do awk que eu vi.

Mas, em um teste, funcionou.

pavium
fonte
11
Você pode torná-lo mais seguro usando chomp. E é melhor do que arrastar o arquivo.
Sinan
6
Por mais que blasfêmia seja, funciona muito bem. nome do arquivo perl -i -pe 'chomp if eof'. Obrigado.
Todd Partridge 'Gen2ly' 31/10/2009
13
O engraçado de blasfêmia e heresia é que geralmente é odiado porque está correto. :)
Éter
8
Pequena correção: você pode usar perl -pi -e 'chomp if eof' filename, para editar um arquivo no local em vez de criar um arquivo temporário
Romuald Brunet
7
perl -pie 'chomp if eof' filename-> Não é possível abrir o script perl "chomp if eof": Esse arquivo ou diretório não existe; perl -pi -e 'chomp if eof' filename-> funciona
aditsu foi encerrado porque o SE é MAU
56

Você pode tirar vantagem do fato de que as substituições de comandos do shell removem os caracteres de nova linha à direita :

Forma simples que funciona em bash, ksh, zsh:

printf %s "$(< in.txt)" > out.txt

Alternativa portátil (compatível com POSIX) (um pouco menos eficiente):

printf %s "$(cat in.txt)" > out.txt

Nota:

  • Se in.txtterminar com vários caracteres de nova linha, a substituição do comando removerá todos eles - obrigado, @Sparhawk. (Ele não remove caracteres de espaço em branco que não sejam linhas novas à direita).
  • Como essa abordagem lê todo o arquivo de entrada na memória , é recomendável apenas para arquivos menores.
  • printf %sgarante que nenhuma nova linha seja anexada à saída (é a alternativa compatível com POSIX para a não-padrão echo -n; consulte http://pubs.opengroup.org/onlinepubs/009696799/utilities/echo.html e https: //unix.stackexchange. com / a / 65819 )

Um guia para as outras respostas :

  • Se o Perl estiver disponível, escolha a resposta aceita - é simples e economiza memória (não lê o arquivo de entrada inteiro de uma vez).

  • Caso contrário, considere a resposta Awk de ghostdog74 - é obscura, mas também eficiente em termos de memória ; um equivalente mais legível (compatível com POSIX) é:

    • awk 'NR > 1 { print prev } { prev=$0 } END { ORS=""; print }' in.txt
    • A impressão está atrasada em uma linha, para que a linha final possa ser manuseada no ENDbloco, onde é impressa sem deixar rasto, \ndevido à definição do separador de registros de saída ( OFS) como uma sequência vazia.
  • Se você deseja uma solução detalhada, porém rápida e robusta, que realmente edite no local (em vez de criar um arquivo temporário que substitui o original), considere o script Perl do jrockway .

mklement0
fonte
3
Nota: se houver várias novas linhas no final do arquivo, este comando excluirá todas elas.
Sparhawk
47

Você pode fazer isso com o headGNU coreutils, ele suporta argumentos relativos ao final do arquivo. Portanto, para deixar de fora o último byte, use:

head -c -1

Para testar uma nova linha final, você pode usar taile wc. O exemplo a seguir salva o resultado em um arquivo temporário e sobrescreve o original:

if [[ $(tail -c1 file | wc -l) == 1 ]]; then
  head -c -1 file > file.tmp
  mv file.tmp file
fi

Você também pode usar spongefrom moreutilspara fazer a edição "no local":

[[ $(tail -c1 file | wc -l) == 1 ]] && head -c -1 file | sponge file

Você também pode criar uma função reutilizável geral colocando isso em seu .bashrcarquivo:

# Example:  remove-last-newline < multiline.txt
function remove-last-newline(){
    local file=$(mktemp)
    cat > $file
    if [[ $(tail -c1 $file | wc -l) == 1 ]]; then
        head -c -1 $file > $file.tmp
        mv $file.tmp $file
    fi
    cat $file
}

Atualizar

Conforme observado por KarlWilbur nos comentários e usado na resposta de Sorentar , truncate --size=-1pode substituir head -c-1e oferecer suporte à edição no local.

Thor
fonte
3
Melhor solução de todas até agora. Usa uma ferramenta padrão que realmente toda distribuição Linux possui, e é concisa e clara, sem nenhuma magia sed ou perl.
Dakkaron
2
Ótima solução. Uma mudança é que eu acho que usaria em truncate --size=-1vez de head -c -1redimensionar o arquivo de entrada em vez de ler no arquivo de entrada, gravá-lo em outro arquivo e substituir o original pelo arquivo de saída.
Karl Wilbur
1
Observe que head -c -1removerá o último caractere, independentemente de ser uma nova linha ou não, é por isso que você deve verificar se o último caractere é uma nova linha antes de removê-lo.
wisbucky
Infelizmente não funciona no Mac. Eu suspeito que não funciona em nenhuma variante BSD.
Edward Falk
16
head -n -1 abc > newfile
tail -n 1 abc | tr -d '\n' >> newfile

Edição 2:

Aqui está uma awkversão (corrigida) que não acumula uma matriz potencialmente enorme:

awk '{if (linha) linha de impressão; line = $ 0} END {printf $ 0} 'abc

Pausado até novo aviso.
fonte
Boa maneira original de pensar sobre isso. Obrigado Dennis.
Todd Partridge 'Gen2ly' 31/10/2009
Você está certo. Eu adoro a sua awkversão. São necessárias duas compensações (e um teste diferente) e usei apenas uma. No entanto, você pode usar em printfvez de ORS.
Pausado até novo aviso.
você pode fazer da saída um pipe com substituição de processo:head -n -1 abc | cat <(tail -n 1 abc | tr -d '\n') | ...
BCoates
2
Usar -c em vez de -n para cabeça e cauda deve ser ainda mais rápido.
Rudimeier
1
Para mim, head -n -1 abc removeu a última linha real do arquivo, deixando uma nova linha à direita; cabeça -c -1 abc parecia funcionar melhor
ChrisV
10

gawk

   awk '{q=p;p=$0}NR>1{print q}END{ORS = ""; print p}' file
ghostdog74
fonte
Ainda me parece um monte de personagens ... aprendendo devagar :). Faz o trabalho embora. Obrigado ghostdog.
Todd Partridge 'Gen2ly' 31/10/2009
1
awk '{ prev_line = line; line = $0; } NR > 1 { print prev_line; } END { ORS = ""; print line; }' fileisso deve ser mais fácil de ler.
Yevhen Pavliuk
Como sobre: awk 'NR>1 {print p} {p=$0} END {printf $0}' file.
Isaac
@sorontar O primeiro argumento para printfé o argumento de formato . Portanto, se o arquivo de entrada tiver algo que possa ser interpretado como um especificador de formato %d, você receberá um erro. Uma correção seria alterá-lo paraprintf "%s" $0
Robin A. Meade
9

Um método muito simples para arquivos de linha única, exigindo o eco GNU do coreutils:

/bin/echo -n $(cat $file)
outro
fonte
Esta é uma maneira decente se não for muito cara (repetitiva).
Isso tem problemas quando \nestá presente. À medida que é convertido em uma nova linha.
22817 Chris Stryczynski
Também parece funcionar para arquivos multi-linha que o $(...)é citado
Thor
Definitivamente preciso citar isso ... /bin/echo -n "$(cat infile)" Além disso, não tenho certeza de qual seria o tamanho máximo echoou o shell entre as versões / distros do OS / shell (eu estava apenas pesquisando isso e era uma toca de coelho), então estou não tenho certeza de quão portátil (ou de alto desempenho) seria para algo que não seja arquivos pequenos - mas para arquivos pequenos, ótimo.
31918 Michael
8

Se você quiser fazer o que é certo, precisará de algo como isto:

use autodie qw(open sysseek sysread truncate);

my $file = shift;
open my $fh, '+>>', $file;
my $pos = tell $fh;
sysseek $fh, $pos - 1, 0;
sysread $fh, my $buf, 1 or die 'No data to read?';

if($buf eq "\n"){
    truncate $fh, $pos - 1;
}

Abrimos o arquivo para leitura e anexação; abrir para anexar significa que já estamos seekno final do arquivo. Em seguida, obtemos a posição numérica do final do arquivo com tell. Usamos esse número para procurar um caractere e depois lemos esse caractere. Se for uma nova linha, truncamos o arquivo para o caractere antes dessa nova linha, caso contrário, não fazemos nada.

Isso é executado em tempo constante e espaço constante para qualquer entrada e também não requer mais espaço em disco.

jrockway
fonte
2
mas que tem a desvantagem de não zerando propriedade / permissões para o arquivo ... err, espera ...
ysth
1
Detalhada, mas rápida e robusta - parece ser a única resposta verdadeira de edição de arquivos no local aqui (e já que pode não ser óbvio para todos: este é um script Perl ).
usar o seguinte comando
6

Aqui está uma solução Python agradável e arrumada. Não tentei ser conciso aqui.

Isso modifica o arquivo no local, em vez de fazer uma cópia do arquivo e remover a nova linha da última linha da cópia. Se o arquivo for grande, será muito mais rápido que a solução Perl que foi escolhida como a melhor resposta.

Ele trunca um arquivo em dois bytes se os últimos dois bytes forem CR / LF ou em um byte se o último byte for LF. Ele não tenta modificar o arquivo se o último byte (s) não for (CR) LF. Ele lida com erros. Testado em Python 2.6.

Coloque isso em um arquivo chamado "striplast" e chmod +x striplast.

#!/usr/bin/python

# strip newline from last line of a file


import sys

def trunc(filename, new_len):
    try:
        # open with mode "append" so we have permission to modify
        # cannot open with mode "write" because that clobbers the file!
        f = open(filename, "ab")
        f.truncate(new_len)
        f.close()
    except IOError:
        print "cannot write to file:", filename
        sys.exit(2)

# get input argument
if len(sys.argv) == 2:
    filename = sys.argv[1]
else:
    filename = "--help"  # wrong number of arguments so print help

if filename == "--help" or filename == "-h" or filename == "/?":
    print "Usage: %s <filename>" % sys.argv[0]
    print "Strips a newline off the last line of a file."
    sys.exit(1)


try:
    # must have mode "b" (binary) to allow f.seek() with negative offset
    f = open(filename, "rb")
except IOError:
    print "file does not exist:", filename
    sys.exit(2)


SEEK_EOF = 2
f.seek(-2, SEEK_EOF)  # seek to two bytes before end of file

end_pos = f.tell()

line = f.read()
f.close()

if line.endswith("\r\n"):
    trunc(filename, end_pos)
elif line.endswith("\n"):
    trunc(filename, end_pos + 1)

PS No espírito do "Perl golf", aqui está a minha solução Python mais curta. Ele retira o arquivo inteiro da entrada padrão para a memória, retira todas as novas linhas do final e grava o resultado na saída padrão. Não é tão conciso quanto o Perl; você simplesmente não pode vencer o Perl por coisas rápidas e complicadas como essa.

Remova o "\ n" da chamada .rstrip()e removerá todo o espaço em branco do final do arquivo, incluindo várias linhas em branco.

Coloque isso em "slurp_and_chomp.py" e execute python slurp_and_chomp.py < inputfile > outputfile.

import sys

sys.stdout.write(sys.stdin.read().rstrip("\n"))
steveha
fonte
os.path.isfile () informará sobre a presença do arquivo. Usando try / except pode pegar um monte de erros diferentes :)
Denis Barmenkov
5

Uma solução rápida é usar o utilitário gnu truncate:

[ -z $(tail -c1 file) ] && truncate -s-1 file

O teste será verdadeiro se o arquivo tiver uma nova linha à direita.

A remoção é muito rápida, realmente implementada, nenhum novo arquivo é necessário e a pesquisa também está lendo no final apenas um byte ( tail -c1).

Isaac
fonte
1
truncar: operando de arquivo ausente
Brian Hannay
2
é só faltando o nome do arquivo arrastando no exemplo, ou seja, [ -z $(tail -c1 filename) ] && truncate -s -1 filename(também, em resposta a outro comentário, o truncatecomando não funciona com stdin, um nome de arquivo é necessário)
michael
4

Ainda outro perl WTDI:

perl -i -p0777we's/\n\z//' filename
ysth
fonte
3
$ perl -e 'local $ /; $ _ = <>; s / \ n $ //; print 'a-text-file.txt

Consulte também Corresponder qualquer caractere (incluindo novas linhas) no sed .

Sinan Ünür
fonte
1
Isso remove todas as novas linhas. Equivalente atr -d '\n'
Pausado até novo aviso.
Isso funciona bem também, provavelmente menos blasfêmia do que os pavimentos.
Todd Partridge 'Gen2ly' 31/10/2009
Sinan, embora o Linux e o Unix possam definir arquivos de texto para terminar com uma nova linha, o Windows não apresenta esse requisito. O bloco de notas, por exemplo, escreverá apenas os caracteres digitados sem adicionar nada extra no final. Os compiladores C podem exigir que um arquivo de origem termine com uma quebra de linha, mas os arquivos de origem C não são "apenas" arquivos de texto, para que possam ter requisitos adicionais.
Rob Kennedy
nesse sentido, a maioria dos minificadores de javascript / css remove as novas linhas à direita e ainda produz arquivos de texto.
ysth 02/11/2009
@ Robert Kennedy e @ysth: Há um argumento interessante sobre o motivo pelo qual esses arquivos não são realmente arquivos de texto e tal.
Sinan Ünür 2/11/2009
2

Usando dd:

file='/path/to/file'
[[ "$(tail -c 1 "${file}" | tr -dc '\n' | wc -c)" -eq 1 ]] && \
    printf "" | dd  of="${file}" seek=$(($(stat -f "%z" "${file}") - 1)) bs=1 count=1
    #printf "" | dd  of="${file}" seek=$(($(wc -c < "${file}") - 1)) bs=1 count=1
cpit
fonte
2
perl -pi -e 's/\n$// if(eof)' your_file
Vijay
fonte
Efetivamente o mesmo que a resposta aceita, mas sem dúvida um conceito mais claro para usuários não-Perl. Note que não há nenhuma necessidade para os gou os parênteses em torno eof: perl -pi -e 's/\n$// if eof' your_file.
precisa saber é o seguinte
2

Assumindo o tipo de arquivo Unix e você deseja apenas a última nova linha que funcione.

sed -e '${/^$/d}'

Não funcionará em várias novas linhas ...

* Funciona apenas se a última linha for uma linha em branco.

LoranceStinson
fonte
Aqui está uma sedsolução que funciona mesmo para uma última linha não-branco: stackoverflow.com/a/52047796
wisbucky
1

Outra resposta FTR (e a minha favorita!): Faz eco / cat a coisa que você deseja retirar e capturar a saída através de backticks. A nova linha final será removida. Por exemplo:

# Sadly, outputs newline, and we have to feed the newline to sed to be portable
echo thingy | sed -e 's/thing/sill/'

# No newline! Happy.
out=`echo thingy | sed -e 's/thing/sill/'`
printf %s "$out"

# Similarly for files:
file=`cat file_ending_in_newline`
printf %s "$file" > file_no_newline
Nicholas Wilson
fonte
1
Encontrei o combo gato-impressão por acidente (estava tentando obter o comportamento oposto). Observe que isso removerá TODAS as novas linhas finais, não apenas a última.
technosaurus 27/09
1

POSIX SED:

'$ {/ ^ $ / d}'

$ - match last line


{ COMMANDS } - A group of commands may be enclosed between { and } characters. This is particularly useful when you want a group of commands to be triggered by a single address (or address-range) match.
Oleg Mazko
fonte
Eu acho que isso só irá removê-lo se a última linha estiver em branco. Não removerá a nova linha à direita se a última linha não estiver em branco. Por exemplo, echo -en 'a\nb\n' | sed '${/^$/d}'não removerá nada. echo -en 'a\nb\n\n' | sed '${/^$/d}'será removido porque a última linha inteira está em branco.
wisbucky
1

Essa é uma boa solução se você precisar trabalhar com pipes / redirecionamentos em vez de ler / produzir um ou para um arquivo. Isso funciona com uma ou várias linhas. Funciona se existe uma nova linha à direita ou não.

# with trailing newline
echo -en 'foo\nbar\n' | sed '$s/$//' | head -c -1

# still works without trailing newline
echo -en 'foo\nbar' | sed '$s/$//' | head -c -1

# read from a file
sed '$s/$//' myfile.txt | head -c -1

Detalhes:

  • head -c -1trunca o último caractere da sequência, independentemente de qual seja o caractere. Portanto, se a sequência não terminar com uma nova linha, você estaria perdendo um caractere.
  • Então, para resolver esse problema, nós adicionamos um outro comando que irá adicionar uma nova linha de fuga se não houver um: sed '$s/$//'. O primeiro $meio aplica apenas o comando à última linha. s/$//significa substituir o "fim da linha" por "nada", que basicamente não está fazendo nada. Mas tem um efeito colateral de adicionar uma nova linha à direita, se não houver uma.

Nota: O padrão do Mac headnão suporta a -copção. Você pode fazer brew install coreutilse usar em seu gheadlugar.

wisbucky
fonte
0

A única vez que eu queria fazer isso é no código de golfe e, em seguida, copiei meu código do arquivo e colei em uma echo -n 'content'>fileinstrução.

dlamblin
fonte
No meio do caminho; abordagem completa aqui .
precisa saber é o seguinte
0
sed ':a;/^\n*$/{$d;N;};/\n$/ba' file
ghostdog74
fonte
Funciona, mas remove todas as novas linhas finais.
precisa saber é o seguinte
0

Eu tive um problema semelhante, mas estava trabalhando com um arquivo do Windows e precisava manter os CRLF - minha solução no linux:

sed 's/\r//g' orig | awk '{if (NR>1) printf("\r\n"); printf("%s",$0)}' > tweaked
Cadrian
fonte
0
sed -n "1 x;1 !H
$ {x;s/\n*$//p;}
" YourFile

Deve remover qualquer última ocorrência de \ n no arquivo. Não funciona em arquivos grandes (devido à limitação do buffer sed)

NeronLeVelu
fonte
0

rubi:

ruby -ne 'print $stdin.eof ? $_.strip : $_'

ou:

ruby -ane 'q=p;p=$_;puts q if $.>1;END{print p.strip!}'
pico
fonte