Como posso excluir uma nova linha à direita no bash?

10

Estou procurando por algo que se comporte como o de Perl chomp. Estou procurando um comando que simplesmente imprima sua entrada, menos o último caractere se for uma nova linha:

$ printf "one\ntwo\n" | COMMAND_IM_LOOKING_FOR ; echo " done"
one
two done
$ printf "one\ntwo" | COMMAND_IM_LOOKING_FOR ; echo " done"
one
two done

(A substituição de comandos no Bash e no Zsh exclui todas as novas linhas à direita, mas estou procurando por algo que exclua uma nova linha no máximo.)

Flimm
fonte

Respostas:

9

Isso deve funcionar:

printf "one\ntwo\n" | awk 'NR>1{print PREV} {PREV=$0} END{printf("%s",$0)}' ; echo " done"

O script sempre imprime a linha anterior em vez da atual, e a última linha é tratada de maneira diferente.

O que faz com mais detalhes:

  1. NR>1{print PREV} Imprimir linha anterior (exceto a primeira vez).
  2. {PREV=$0}Armazena a linha atual na PREVvariável.
  3. END{printf("%s",$0)} Por fim, imprima a última linha sem interromper a linha.

Observe também que isso removeria no máximo uma linha vazia no final (não há suporte para remoção "one\ntwo\n\n\n").

LatinSuD
fonte
15

Você pode usar perlsem chomp:

$ printf "one\ntwo\n" | perl -0 -pe 's/\n\Z//'; echo " done"
one
two done

$ printf "one\ntwo" | perl -0 -pe 's/\n\Z//'; echo " done"
one
two done

Mas por que não usar a chompsi mesmo:

$ printf "one\ntwo\n" | perl -pe 'chomp if eof'; echo " done"
cuonglm
fonte
4

Se você quer um equivalente exato chomp, o primeiro método que me vem à mente é a solução awk que o LatinSuD já publicou . Vou adicionar alguns outros métodos que não são implementados, chompmas implementam algumas tarefas comuns chompusadas com frequência.

Quando você coloca algum texto em uma variável, todas as novas linhas no final são removidas. Portanto, todos esses comandos produzem a mesma saída de linha única:

echo "$(printf 'one\ntwo') done"
echo "$(printf 'one\ntwo\n') done"
echo "$(printf 'one\ntwo\n\n') done"
echo "$(printf 'one\ntwo\n\n\n\n\n\n\n\n\n\n') done"

Se você quiser acrescentar algum texto à última linha de um arquivo ou da saída de um comando, sedpode ser conveniente. Com o GNU sed e a maioria das outras implementações modernas, isso funciona mesmo que a entrada não termine em uma nova linha¹; no entanto, isso não adicionará uma nova linha se ainda não houver uma.

sed '$ s/$/ done/'

¹ No entanto, isso não funciona com todas as implementações do sed: sed é uma ferramenta de processamento de texto e um arquivo que não está vazio e não termina com um caractere de nova linha não é um arquivo de texto.

Gilles 'SO- parar de ser mau'
fonte
Isso não é exatamente equivalente a chomp, pois chompapenas exclui no máximo uma nova linha à direita.
Flimm
@Limlim Sim, o equivalente exato mais óbvio chompseria a solução awk que o LatinSuD já publicou. Mas, em muitos casos, chompé apenas uma ferramenta para realizar um trabalho, e eu forneço maneiras de executar algumas tarefas comuns. Deixe-me atualizar minha resposta para esclarecer isso.
Gilles 'SO- stop be evil'
1

Outra perlabordagem. Este lê toda a entrada na memória, por isso pode não ser uma boa ideia para grandes quantidades de dados (use o cuonglm ou a awkabordagem para isso):

$ printf "one\ntwo\n" | perl -0777pe 's/\n$//'; echo " done"
one
two done
terdon
fonte
Obrigado, @ StéphaneChazelas, corrigido. Por alguma razão, essa opção sempre me confunde !
terdon
0

Eu peguei isso de um repositório do github em algum lugar, mas não consigo encontrar onde

delete-trailing-blank-lines-sed

#!/bin/bash
#
# Delete all trailing blank lines.
# From http://sed.sourceforge.net/sed1line.txt
#
# Version: 1.3.0
# Created: 2011-01-02
# Updated: 2015-01-25
# Contact: Joel Parker Henderson ([email protected])
# License: GPL
##
set -euf
sed -e :a -e '/^\n*$/{$d;N;ba' -e '}'
Brad Parks
fonte
0

resumo

Imprimir linhas sem nova linha, adicione uma nova linha somente se houver outra linha para imprimir.

$ printf 'one\ntwo\n' | 

     awk '{ printf( "%s%s" , NR>1?"\n":"" , $0 ) }';   echo " done"

one
two done

Outras soluções

Se estivéssemos trabalhando com um arquivo, podemos truncar apenas um caractere (se terminar em uma nova linha):

removeTrailNewline () {[[$ (tail -c 1 "$ 1")]] || truncar -s-1 "$ 1"; }

Essa é uma solução rápida, pois precisa ler apenas um caractere do arquivo e removê-lo diretamente ( truncate) sem ler o arquivo inteiro.

No entanto, ao trabalhar com dados de stdin (um fluxo), os dados devem ser lidos, todos eles. E é "consumido" assim que é lido. Sem retorno (como no truncado). Para encontrar o final de um fluxo, precisamos ler o final do fluxo. Nesse ponto, não há como voltar ao fluxo de entrada, os dados já foram "consumidos". Isso significa que os dados devem ser armazenados em alguma forma de buffer até correspondermos ao final do fluxo e, em seguida, fazer algo com os dados no buffer.

A solução mais óbvia é converter o fluxo em um arquivo e processá-lo. Mas a pergunta pede algum tipo de filtro do fluxo. Não é sobre o uso de arquivos adicionais.

variável

A solução ingênua seria capturar toda a entrada em uma variável:

FilterOne(){ filecontents=$(cat; echo "x");        # capture the whole input
             filecontents=${filecontents%x};       # Remove the "x" added above.
             nl=$'\n';                             # use a variable for newline.
             printf '%s' "${filecontents%"$nl"}";  # Remove newline (if it exists).
       }

printf 'one\ntwo'     | FilterOne ; echo 1done
printf 'one\ntwo\n'   | FilterOne ; echo 2done
printf 'one\ntwo\n\n' | FilterOne ; echo 3done

memória

É possível carregar um arquivo inteiro na memória com o sed. No sed, é impossível evitar a nova linha à direita na última linha. O GNU sed pode evitar a impressão de uma nova linha à direita, mas apenas se o arquivo de origem já estiver em falta. Portanto, não, o simples sed não pode ajudar.

Exceto no GNU awk com a -zopção:

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

Com awk (qualquer awk), solte o fluxo inteiro e printfsem a nova linha à direita.

awk '    { content = content $0 RS } 
     END { gsub( "\n$", "", content ); printf( "%s", content ) }
    '

Carregar um arquivo inteiro na memória pode não ser uma boa ideia, pode consumir muita memória.

Duas linhas na memória

No awk, podemos processar duas linhas por loop armazenando a linha anterior em uma variável e imprimindo a atual:

awk 'NR>1{print previous} {previous=$0} END {printf("%s",$0)}'

Processamento direto

Mas poderíamos fazer melhor.

Se imprimirmos a linha atual sem uma nova linha e só imprimirmos quando houver uma linha seguinte, processaremos uma linha por vez e a última linha não terá uma nova linha à direita:

awk 'NR == 1 {printf ("% s", $ 0); próximo}; {printf ("\ n% s", $ 0)} '

Ou, escrito de alguma outra maneira:

awk 'NR>1{ print "" }; { printf( "%s", $0 ) }'

Ou:

awk '{ printf( "%s%s" , NR>1?"\n":"" , $0 ) }'

Assim:

$ printf 'one\ntwo\n' | awk '{ printf( "%s%s" , NR>1?"\n":"" , $0 ) }'; echo " done"
one
two done
Isaac
fonte