¿Cómo puedo eliminar una nueva línea final en bash?

10

Estoy buscando algo que se comporte como el de Perl chomp. Estoy buscando un comando que simplemente imprima su entrada, menos el último carácter si es una nueva línea:

$ 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

(La sustitución de comandos en Bash y Zsh elimina todas las líneas nuevas finales, pero estoy buscando algo que elimine una línea nueva como máximo).

Flimm
fuente

Respuestas:

9

Esto debería funcionar:

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

El script siempre imprime la línea anterior en lugar de la actual, y la última línea se trata de manera diferente.

Lo que hace con más detalle:

  1. NR>1{print PREV} Imprimir línea anterior (excepto la primera vez).
  2. {PREV=$0}Almacena la línea actual en PREVvariable.
  3. END{printf("%s",$0)} Finalmente, imprima la última línea sin saltos de línea.

También tenga en cuenta que esto eliminaría como máximo una línea vacía al final (no hay soporte para eliminar "one\ntwo\n\n\n").

LatinSuD
fuente
15

Puedes usar perlsin 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

Pero por qué no usarlo chompsolo:

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

Si desea un equivalente exacto para chomp, el primer método que se me ocurre es la solución awk que LatinSuD ya publicó . Agregaré algunos otros métodos que no implementan chomppero implementan algunas tareas comunes que a chompmenudo se usan.

Cuando inserta algo de texto en una variable, se eliminan todas las líneas nuevas al final. Entonces, todos estos comandos producen la misma salida de una línea:

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"

Si desea agregar algún texto a la última línea de un archivo o de la salida de un comando, sedpuede ser conveniente. Con GNU sed y la mayoría de las otras implementaciones modernas, esto funciona incluso si la entrada no termina en una nueva línea¹; sin embargo, esto no agregará una nueva línea si aún no había una.

sed '$ s/$/ done/'

¹ Sin embargo, esto no funciona con todas las implementaciones de sed: sed es una herramienta de procesamiento de texto, y un archivo que no está vacío y no termina con un carácter de nueva línea no es un archivo de texto.

Gilles 'SO- deja de ser malvado'
fuente
Esto no es exactamente equivalente a chomp, ya que chompsolo elimina a lo sumo una nueva línea final.
Flimm
@Flimm Sí, el equivalente exacto más obvio chompsería la solución awk que LatinSuD ya ha publicado. Pero en muchos casos chompes solo una herramienta para hacer un trabajo, y proporciono formas de hacer algunas tareas comunes. Permítanme actualizar mi respuesta para aclarar esto.
Gilles 'SO- deja de ser malvado'
1

Otro perlenfoque Este lee toda la entrada en la memoria, por lo que podría no ser una buena idea para grandes cantidades de datos (use cuonglm's o el awkenfoque para eso):

$ printf "one\ntwo\n" | perl -0777pe 's/\n$//'; echo " done"
one
two done
terdon
fuente
Gracias, @ StéphaneChazelas, arreglado. ¡Por alguna razón, este cambio siempre me confunde !
terdon
0

Enganché esto de un repositorio de Github en alguna parte, pero no puedo encontrar dónde

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
fuente
0

resumen

Imprima líneas sin nueva línea, agregue una nueva línea solo si hay otra línea para imprimir.

$ printf 'one\ntwo\n' | 

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

one
two done

Otras soluciones

Si estuviéramos trabajando con un archivo, podemos truncar un carácter de él (si termina en una nueva línea):

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

Esa es una solución rápida, ya que necesita leer solo un carácter del archivo y luego eliminarlo directamente ( truncate) sin leer todo el archivo.

Sin embargo, al trabajar con datos de stdin (una secuencia), los datos deben leerse, todo. Y, se "consume" tan pronto como se lee. Sin retroceso (como con truncar). Para encontrar el final de una secuencia necesitamos leer hasta el final de la secuencia. En ese punto, no hay forma de retroceder en la secuencia de entrada, los datos ya se han "consumido". Esto significa que los datos deben almacenarse en alguna forma de búfer hasta que coincidamos con el final de la secuencia y luego hacer algo con los datos en el búfer.

La solución más obvia es convertir la secuencia en un archivo y procesar ese archivo. Pero la pregunta pide algún tipo de filtro de la corriente. No se trata del uso de archivos adicionales.

variable

La solución ingenua sería capturar toda la entrada en una variable:

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

memoria

Es posible cargar un archivo completo en la memoria con sed. En sed es imposible evitar la nueva línea final en la última línea. GNU sed podría evitar imprimir una nueva línea final, pero solo si el archivo fuente ya no se encuentra. Entonces, no, sed simple no puede ayudar.

Excepto en GNU awk con la -zopción:

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

Con awk (cualquier awk), sorbe toda la secuencia y printfsin la nueva línea final.

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

Cargar un archivo completo en la memoria puede no ser una buena idea, puede consumir mucha memoria.

Dos líneas en la memoria

En awk, podemos procesar dos líneas por ciclo almacenando la línea anterior en una variable e imprimiendo la actual:

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

Procesamiento directo

Pero podríamos hacerlo mejor.

Si imprimimos la línea actual sin una nueva línea e imprimimos una nueva línea solo cuando existe una línea siguiente, procesamos una línea a la vez y la última línea no tendrá una nueva línea final:

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

O, escrito de alguna otra manera:

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

O:

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

Entonces:

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