¿Cómo puedo eliminar una nueva línea si es el último carácter de un archivo?

162

Tengo algunos archivos que me gustaría eliminar la última línea nueva si es el último carácter de un archivo. od -cme muestra que el comando que ejecuto escribe el archivo con una nueva línea al final:

0013600   n   t  >  \n

He intentado algunos trucos con sed, pero lo mejor que se me ocurre no es hacer el truco:

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

¿Alguna idea de como hacer esto?

Todd Partridge 'Gen2ly'
fuente
44
nueva línea es solo un carácter para las nuevas líneas de Unix. Las nuevas líneas de DOS son dos caracteres. Por supuesto, el literal "\ n" tiene dos caracteres. ¿Qué estás buscando realmente?
Pausado hasta nuevo aviso.
3
Aunque la representación podría ser \n, en Linux es un personaje
pavium
10
¿Puedes explicar por qué quieres hacer esto? Se supone que los archivos de texto terminan con un final de línea, a menos que estén completamente vacíos. ¿Me parece extraño que quieras tener un archivo tan truncado?
Thomas Padron-McCarthy
La razón habitual para hacer algo como esto es eliminar una coma final de la última línea de un archivo CSV. Sed funciona bien, pero las líneas nuevas deben tratarse de manera diferente.
Pavium
9
@ ThomasPadron-McCarthy "En informática, por cada buena razón hay que hacer algo, existe una buena razón para no hacerlo y viceversa". -Jesús - "no deberías hacer eso" es una respuesta horrible, no importa la pregunta. El formato correcto es: [cómo hacerlo] pero [por qué puede ser una mala idea]. #sacrilege
Cory Mawhorter

Respuestas:

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

o, para editar el archivo en su lugar:

perl -pi -e 'chomp if eof' filename

[Nota del editor: -pi -efue originalmente -pie, pero, como lo señalaron varios comentaristas y lo explicó @hvd, este último no funciona].

Esto fue descrito como una 'blasfemia perl' en el sitio web awk que vi.

Pero, en una prueba, funcionó.

pavium
fuente
11
Puede hacerlo más seguro usando chomp. Y es mejor sorber el archivo.
Sinan Ünür el
66
Aunque sea una blasfemia, funciona muy bien. perl -i -pe 'chomp if eof' nombre de archivo. Gracias.
Todd Partridge 'Gen2ly'
13
Lo curioso de la blasfemia y la herejía es que generalmente es odiado porque es correcto. :)
Ether
8
Pequeña corrección: puede usar perl -pi -e 'chomp if eof' filename, para editar un archivo en el lugar en lugar de crear un archivo temporal
Romuald Brunet
77
perl -pie 'chomp if eof' filename-> No se puede abrir el script de perl "chomp if eof": No existe tal archivo o directorio; perl -pi -e 'chomp if eof' filename-> funciona
aditsu se retiró porque SE es MAL
56

Puede aprovechar el hecho de que las sustituciones de comandos de shell eliminan los caracteres de nueva línea finales :

Forma simple que funciona en bash, ksh, zsh:

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

Alternativa portátil (compatible con POSIX) (ligeramente menos eficiente):

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

Nota:

  • Si in.txtextremos con múltiples caracteres de nueva línea, la sustitución de comandos elimina todas ellas - gracias, @Sparhawk. (No elimina los caracteres de espacio en blanco que no sean líneas nuevas al final).
  • Dado que este enfoque lee todo el archivo de entrada en la memoria , solo es recomendable para archivos más pequeños.
  • printf %sgarantiza que no se agregue una nueva línea a la salida (es la alternativa compatible con POSIX a la no estándar echo -n; consulte http://pubs.opengroup.org/onlinepubs/009696799/utilities/echo.html y https: //unix.stackexchange. com / a / 65819 )

Una guía para las otras respuestas :

  • Si Perl está disponible, busque la respuesta aceptada : es simple y eficiente en la memoria (no lee todo el archivo de entrada de una vez).

  • De lo contrario, considere la respuesta Awk de ghostdog74 : es oscura, pero también eficiente en memoria ; un equivalente más legible (compatible con POSIX) es:

    • awk 'NR > 1 { print prev } { prev=$0 } END { ORS=""; print }' in.txt
    • La impresión se retrasa una línea para que la línea final se pueda manejar en el ENDbloque, donde se imprime sin un final \ndebido a la configuración del separador de registro de salida ( OFS) en una cadena vacía.
  • Si desea una solución detallada, rápida y robusta que realmente edite en el lugar (en lugar de crear un archivo temporal que luego reemplace el original), considere el script Perl de jrockway .

mklement0
fuente
3
Nota: si hay varias líneas nuevas al final del archivo, este comando las eliminará todas.
Sparhawk
47

Puede hacer esto con los headcoreutils de GNU, admite argumentos relacionados con el final del archivo. Entonces, para dejar de usar el último byte:

head -c -1

Para probar una nueva línea final, puede usar taily wc. El siguiente ejemplo guarda el resultado en un archivo temporal y posteriormente sobrescribe el original:

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

También puede usar spongedesde moreutilspara hacer la edición "in situ":

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

También puede hacer una función reutilizable general rellenando esto en su .bashrcarchivo:

# 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
}

Actualizar

Como señaló KarlWilbur en los comentarios y utilizado en la respuesta de Sorentar , truncate --size=-1puede reemplazar head -c-1y admite la edición en el lugar.

Thor
fuente
3
La mejor solución de todas hasta ahora. Utiliza una herramienta estándar que realmente tiene toda distribución de Linux, y es concisa y clara, sin ningún tipo de hechicería sed o perl.
Dakkaron
2
Buena solución Un cambio es que creo que lo usaría en truncate --size=-1lugar de hacerlo, head -c -1ya que solo cambia el tamaño del archivo de entrada en lugar de leerlo, escribirlo en otro archivo y luego reemplazar el original con el archivo de salida.
Karl Wilbur
1
Tenga en cuenta que head -c -1eliminará el último carácter independientemente de si es una nueva línea o no, es por eso que debe verificar si el último carácter es una nueva línea antes de eliminarlo.
wisbucky
Lamentablemente no funciona en Mac. Sospecho que no funciona en ninguna variante BSD.
Edward Falk
16
head -n -1 abc > newfile
tail -n 1 abc | tr -d '\n' >> newfile

Edición 2:

Aquí hay una awkversión (corregida) que no acumula una matriz potencialmente enorme:

awk '{if (línea) línea de impresión; line = $ 0} END {printf $ 0} 'abc

Pausado hasta nuevo aviso.
fuente
Buena forma original de pensarlo. Gracias Dennis
Todd Partridge 'Gen2ly'
Estás en lo correcto. Me remito a tu awkversión. Se necesitan dos compensaciones (y una prueba diferente) y solo usé una. Sin embargo, podría usar en printflugar de ORS.
Pausado hasta nuevo aviso.
puede convertir la salida en una tubería con sustitución de proceso:head -n -1 abc | cat <(tail -n 1 abc | tr -d '\n') | ...
BCoates
2
Usar -c en lugar de -n para cabeza y cola debería ser aún más rápido.
rudimeier
1
Para mí, head -n -1 abc eliminó la última línea real del archivo, dejando una nueva línea final; head -c -1 abc parecía funcionar mejor
ChrisV
10

papar moscas

   awk '{q=p;p=$0}NR>1{print q}END{ORS = ""; print p}' file
ghostdog74
fuente
Todavía me parecen muchos personajes ... aprendiéndolo lentamente :). Sin embargo, hace el trabajo. Gracias ghostdog.
Todd Partridge 'Gen2ly'
1
awk '{ prev_line = line; line = $0; } NR > 1 { print prev_line; } END { ORS = ""; print line; }' fileEsto debería ser más fácil de leer.
Yevhen Pavliuk
¿Qué tal: awk 'NR>1 {print p} {p=$0} END {printf $0}' file.
Isaac
@sorontar El primer argumento para printfes el argumento de formato . Por lo tanto, si el archivo de entrada tuviera algo que pudiera interpretarse como un especificador de formato %d, obtendría un error. Una solución sería cambiar aprintf "%s" $0
Robin A. Meade
9

Un método muy simple para archivos de una sola línea, que requiere eco GNU de coreutils:

/bin/echo -n $(cat $file)
otro
fuente
Esta es una forma decente si no es demasiado costosa (repetitiva).
Esto tiene problemas cuando \nestá presente. A medida que se convierte en una nueva línea.
Chris Stryczynski
También parece funcionar para archivos de varias líneas que $(...)se cita
Thor
definitivamente necesito citar eso ... /bin/echo -n "$(cat infile)" Además, no estoy seguro de cuál sería el máximo de len echoo el shell en las versiones de os / shell / distros (solo buscaba en Google esto y era un agujero de conejo), así que estoy no estoy seguro de qué tan portátil (o eficiente) sería en realidad para cualquier cosa que no sean archivos pequeños, pero para archivos pequeños, genial.
michael
8

Si quieres hacerlo bien, necesitas algo como esto:

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 el archivo para leer y agregar; abrir para agregar significa que ya estamos seekeditados al final del archivo. Luego obtenemos la posición numérica del final del archivo con tell. Usamos ese número para buscar un carácter, y luego leemos ese carácter. Si se trata de una nueva línea, truncamos el archivo al carácter antes de esa nueva línea; de lo contrario, no hacemos nada.

Esto se ejecuta en tiempo constante y espacio constante para cualquier entrada, y tampoco requiere más espacio en disco.

jrockway
fuente
2
pero que tiene el inconveniente de no reseting propiedad / permisos para el archivo ... err, espera ...
ysth
1
Detallado, pero rápido y robusto, parece ser la única respuesta verdadera de edición de archivos en el lugar aquí (y dado que puede no ser obvio para todos: este es un script de Perl ).
mklement0
6

Aquí hay una buena y ordenada solución de Python. No hice ningún intento de ser conciso aquí.

Esto modifica el archivo en el lugar, en lugar de hacer una copia del archivo y quitar la nueva línea de la última línea de la copia. Si el archivo es grande, será mucho más rápido que la solución Perl elegida como la mejor respuesta.

Trunca un archivo en dos bytes si los dos últimos bytes son CR / LF, o en un byte si el último byte es LF. No intenta modificar el archivo si los últimos bytes no son (CR) LF. Maneja errores. Probado en Python 2.6.

Ponga esto en un archivo llamado "striplast" y 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)

PD: En el espíritu del "Perl golf", aquí está mi solución Python más corta. Extrae todo el archivo de la entrada estándar a la memoria, elimina todas las líneas nuevas del final y escribe el resultado en la salida estándar. No tan conciso como el Perl; simplemente no puedes vencer a Perl por pequeñas cosas rápidas como esta.

Elimine el "\ n" de la llamada .rstrip()y eliminará todo el espacio en blanco desde el final del archivo, incluidas varias líneas en blanco.

Pon esto en "slurp_and_chomp.py" y luego ejecuta python slurp_and_chomp.py < inputfile > outputfile.

import sys

sys.stdout.write(sys.stdin.read().rstrip("\n"))
steveha
fuente
os.path.isfile () le informará sobre la presencia de archivos. Usar try / except puede detectar muchos errores diferentes :)
Denis Barmenkov
5

Una solución rápida es usar la utilidad gnu truncate:

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

La prueba será verdadera si el archivo tiene una nueva línea final.

La eliminación es muy rápida, realmente en su lugar, no se necesita ningún archivo nuevo y la búsqueda también lee desde el final solo un byte ( tail -c1).

Isaac
fuente
1
truncado: operando de archivo faltante
Brian Hannay
2
solo falta el nombre de archivo final en el ejemplo, es decir, [ -z $(tail -c1 filename) ] && truncate -s -1 filename(también, en respuesta al otro comentario, el truncatecomando no funciona con stdin, se requiere un nombre de archivo)
michael
4

Otro perl WTDI:

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

Consulte también Hacer coincidir cualquier carácter (incluidas las nuevas líneas) en sed .

Sinan Ünür
fuente
1
Eso elimina todas las nuevas líneas. Equivalente atr -d '\n'
pausa hasta nuevo aviso.
Esto también funciona bien, probablemente menos blasfemo que los pavios.
Todd Partridge 'Gen2ly'
Sinan, aunque Linux y Unix pueden definir archivos de texto para terminar con una nueva línea, Windows no plantea tal requisito. El Bloc de notas, por ejemplo, escribirá solo los caracteres que escriba sin agregar nada adicional al final. Los compiladores de C pueden requerir un archivo de origen para terminar con un salto de línea, pero los archivos de origen de C no son "solo" archivos de texto, por lo que pueden tener requisitos adicionales.
Rob Kennedy el
en ese sentido, la mayoría de los minificadores javascript / css eliminarán las nuevas líneas finales y, sin embargo, producirán archivos de texto.
Ysth
@Rob Kennedy y @ysth: Existe un argumento interesante sobre por qué tales archivos no son realmente archivos de texto y tal.
Sinan Ünür el
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
fuente
2
perl -pi -e 's/\n$// if(eof)' your_file
Vijay
fuente
Efectivamente lo mismo que la respuesta aceptada, pero podría decirse que es más claro en concepto para los usuarios que no son de Perl. Tenga en cuenta que no hay ninguna necesidad de que las go los paréntesis alrededor eof: perl -pi -e 's/\n$// if eof' your_file.
mklement0
2

Asumiendo el tipo de archivo Unix y solo desea la última línea nueva, esto funciona.

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

No funcionará en múltiples líneas nuevas ...

* Funciona solo si la última línea es una línea en blanco.

LoranceStinson
fuente
Aquí hay una sedsolución que funciona incluso para una última línea que no está en blanco: stackoverflow.com/a/52047796
wisbucky
1

Sin embargo, otra respuesta FTR (¡y mi favorita!): Echo / cat lo que quieres quitar y capturar la salida a través de backticks. La nueva línea final será eliminada. Por ejemplo:

# 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
fuente
1
Encontré el combo cat-printf por accidente (estaba tratando de obtener el comportamiento opuesto). Tenga en cuenta que esto eliminará TODAS las nuevas líneas finales, no solo la última.
technosaurus
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
fuente
Creo que esto solo lo eliminará si la última línea está en blanco. No eliminará la nueva línea final si la última línea no está en blanco. Por ejemplo, echo -en 'a\nb\n' | sed '${/^$/d}'no eliminará nada. echo -en 'a\nb\n\n' | sed '${/^$/d}'se eliminará ya que toda la última línea está en blanco.
wisbucky
1

Esta es una buena solución si necesita que funcione con tuberías / redirección en lugar de lectura / salida desde o hacia un archivo. Esto funciona con líneas simples o múltiples. Funciona si hay una nueva línea final o no.

# 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

Detalles:

  • head -c -1trunca el último carácter de la cadena, independientemente de cuál sea el carácter. Entonces, si la cadena no termina con una nueva línea, entonces estaría perdiendo un carácter.
  • Así que para resolver ese problema, se añade otro comando que va a añadir un salto de línea final si no hay uno: sed '$s/$//'. El primer $medio solo aplica el comando a la última línea. s/$//significa sustituir el "final de la línea" con "nada", que básicamente no hace nada. Pero tiene un efecto secundario de agregar una nueva línea final si no hay una.

Nota: el valor predeterminado de Mac headno admite la -copción. Puedes hacer brew install coreutilsy usar gheaden su lugar.

wisbucky
fuente
0

La única vez que he querido hacer esto es para el código golf, y luego simplemente copié mi código del archivo y lo pegué en una echo -n 'content'>filedeclaración.

dlamblin
fuente
A mitad de camino; enfoque completo aquí .
mklement0
0
sed ':a;/^\n*$/{$d;N;};/\n$/ba' file
ghostdog74
fuente
Funciona, pero elimina todas las nuevas líneas finales.
mklement0
0

Tuve un problema similar, pero estaba trabajando con un archivo de Windows y necesito mantener esos CRLF: mi solución en Linux:

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

Debería eliminar cualquier última aparición de \ n en el archivo. No funciona en archivos grandes (debido a la limitación del búfer de sed)

NeronLeVelu
fuente
0

rubí:

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

o:

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