¿Cómo eliminar varias líneas nuevas en EOF?

25

Tengo archivos que terminan en una o más líneas nuevas y deberían terminar en una sola línea nueva. ¿Cómo puedo hacer eso con las herramientas Bash / Unix / GNU?

Ejemplo de archivo incorrecto:

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

Archivo corregido de ejemplo:

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

En otras palabras: debe haber exactamente una nueva línea entre el EOF y el último carácter que no sea de nueva línea del archivo.

Implementación de referencia

Lea el contenido del archivo, corte una línea nueva hasta que no haya más líneas nuevas al final, escríbala de nuevo:

#! /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)

Aclaración: Por supuesto, se permite la tubería, si eso es más elegante.

Bengt
fuente

Respuestas:

16
awk '/^$/ {nlstack=nlstack "\n";next;} {printf "%s",nlstack; nlstack=""; print;}' file
Hauke ​​Laging
fuente
2
+1: ¡las soluciones de awk son (casi) siempre elegantes y legibles!
Olivier Dulac
@OlivierDulac De hecho. Cuando vi la sedpropuesta que acaba de ocurrir ... OMG
Hauke Laging
1
esto no funciona en OSX Mavericks usando el último awk disponible de Homebrew. Se equivoca con awk: illegal statement. brew install mawky cambiar el comando a mawkfunciona sin embargo.
tjmcewan
@noname Ni siquiera entiendo la pregunta ...
Hauke ​​Laging
Cualquier awk en el que el script no funcione es un awk gravemente roto: deja de usarlo y obtén un nuevo awk porque si no puede hacerlo, quién sabe qué otra rotura tiene.
Ed Morton
21

De útiles scripts de una línea para sed .

# Delete all trailing blank lines at end of file (only).
sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba' file
Alexey Shmalko
fuente
44
Gracias, utilicé lo siguiente para hacerlo en su lugar para varios archivos: find . -type f -name '*.js' -exec sed --in-place -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
jakub.g
@ jakub.g en su lugar y recursivo es exactamente lo que necesitaba. gracias.
Buttle Butkus
Para agregar al excelente comentario de @ jakub.g puede invocar el comando como este en OS X:find . -type f -name '*.js' -exec sed -i '' -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
davejagoda
18

Dado que ya tiene respuestas con las herramientas más adecuadas sed y awk; podría aprovechar el hecho de que $(< file)elimina las líneas en blanco al final.

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

Ese truco barato no funcionaría para eliminar las líneas en blanco finales que pueden contener espacios u otros caracteres que no se imprimen, solo para eliminar las líneas vacías finales. Tampoco funcionará si el archivo contiene bytes nulos.

En shells que no sean bash y zsh, use en $(cat file)lugar de $(<file).

llua
fuente
+1 para señalar lo que me parece un error: $ (<archivo) ¿realmente no está leyendo el archivo? ¿Por qué descarta las nuevas líneas finales? (lo hace, lo acabo de probar, ¡gracias por señalarlo!)
Olivier Dulac
2
@OlivierDulac $()descarta las nuevas líneas finales. Esa es una decisión de diseño. Supongo que esto facilitará la integración en otras cadenas: echo "On $(date ...) we will meet."sería malo con la nueva línea que casi todos los comandos de shell salen al final.
Hauke ​​Laging
@HaukeLaging: buen punto, probablemente sea la fuente de ese comportamiento
Olivier Dulac
He añadido un caso especial para evitar añadiendo "\ n" para vaciar archivos: [[ $a == '' ]] || printf '%s\n' "$a" >"$file".
davidchambers
Para quitar varias líneas nuevas del inicio de un archivo, inserte tac en el proceso (uso gnu coreutils en Mac, así que gtac para mí):a=$(gtac file.txt); printf '%s\n' "$a" | gtac > file.txt
r_alex_hall
4

Esta pregunta está etiquetada con , pero nadie ha propuesto uned solución.

Aquí hay uno:

ed -s file <<'ED_END'
a

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

o equivalente,

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

ed lo colocará en la última línea del búfer de edición de forma predeterminada al inicio.

El primer comando ( a) agrega una línea vacía al final del búfer (la línea vacía en el script de edición es esta línea y el punto (. ) es solo para volver al modo de comando).

El segundo comando (? ) busca la línea anterior más cercana que contiene algo (incluso caracteres de espacio en blanco), y luego elimina todo al final del búfer desde la siguiente línea.

El tercer comando (w ) vuelve a escribir el archivo en el disco.

La línea vacía agregada protege el resto del archivo para que no se elimine en caso de que no haya líneas vacías al final del archivo original.

Kusalananda
fuente
3

Aquí hay una solución de Perl que no requiere leer más de una línea en la memoria a la vez:

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

o, como una línea:

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

Esto lee el archivo línea por línea y verifica cada línea para ver si contiene un carácter que no sea de línea nueva. Si no lo hace, incrementa un contador; si lo hace, imprime el número de líneas nuevas indicadas por el contador, seguido de la línea misma, y ​​luego restablece el contador.

Técnicamente, incluso el almacenamiento en búfer de una sola línea en la memoria es innecesario; Sería posible resolver este problema utilizando una cantidad constante de memoria leyendo el archivo en fragmentos de longitud fija y procesándolo carácter por carácter utilizando una máquina de estado. Sin embargo, sospecho que sería innecesariamente complicado para el caso de uso típico.

Ilmari Karonen
fuente
1

Si su archivo es lo suficientemente pequeño como para deslizarse en la memoria, puede usar esto

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

En python (sé que no es lo que quieres, pero es mucho mejor ya que está optimizado y es un preludio de la versión bash) sin reescribir el archivo y sin leer todo el archivo (lo cual es bueno si el archivo es muy 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()

Tenga en cuenta que no funciona en archivos donde el carácter EOL no es '\ n'.

jfg956
fuente
0

Una versión bash, que implementa el algoritmo python, pero menos eficiente ya que necesita muchos procesos:

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

Este es rápido de escribir y, si sabe sed, fácil de recordar:

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

Se utiliza la secuencia de comandos sed para eliminar las principales líneas en blanco de uno útil para scripts de línea de sed , referenciados por Alexey, arriba, y tac (cat inversa).

En una prueba rápida, en un archivo de 18 MB y 64,000 líneas, el enfoque de Alexey fue más rápido (0.036 vs 0.046 segundos).

freeB
fuente