¿Cómo agregar una nueva línea al final de un archivo?

190

Al usar los sistemas de control de versiones, me molesta el ruido cuando dice el diff No newline at end of file.

Entonces me preguntaba: ¿Cómo agregar una nueva línea al final de un archivo para deshacerme de esos mensajes?

k0pernikus
fuente
1
ver también / q / 10082204/155090
RubyTuesdayDONO
1
Buena solución a continuación que desinfecta todos los archivos de forma recursiva. Respuestas de @Patrick Oscity
Qwerty
En el futuro, los editores de texto a menudo tienen opciones para garantizar que haya una nueva línea final que usted y sus colaboradores puedan usar para mantenerse limpios.
Nick T

Respuestas:

44

Para desinfectar recursivamente un proyecto, uso este oneliner:

git ls-files -z | while IFS= read -rd '' f; do tail -c1 < "$f" | read -r _ || echo >> "$f"; done

Explicación:

  • git ls-files -zenumera los archivos en el repositorio. Toma un patrón opcional como parámetro adicional que puede ser útil en algunos casos si desea restringir la operación a ciertos archivos / directorios. Como alternativa, puede usar find -print0 ...programas similares para enumerar los archivos afectados, solo asegúrese de que emita NULentradas delimitadas.

  • while IFS= read -rd '' f; do ... done itera a través de las entradas, manejando de manera segura los nombres de archivo que incluyen espacios en blanco y / o nuevas líneas.

  • tail -c1 < "$f" lee el último carácter de un archivo.

  • read -r _ sale con un estado de salida distinto de cero si falta una nueva línea final.

  • || echo >> "$f" agrega una nueva línea al archivo si el estado de salida del comando anterior era distinto de cero.

Patrick Oscity
fuente
También puede hacerlo así si solo desea desinfectar un subconjunto de sus archivos:find -name \*.java | while read f; do tail -n1 $f | read -r _ || echo >> $f; done
Por Lundberg
@ StéphaneChazelas buenas sugerencias, intentaré incorporar esto en mi respuesta.
Patrick Oscity
@PerLundberg también puede pasar un patrón al git ls-filesque aún le ahorrará editar archivos que no se rastrean en el control de versiones.
Patrick Oscity
@ StéphaneChazelas agregar el IFS= para desarmar el separador es bueno para preservar el espacio en blanco circundante. Las entradas terminadas en nulo solo son relevantes si tiene archivos o directorios con una nueva línea en su nombre, lo que parece un poco descabellado, pero es la forma más correcta de manejar el caso genérico, estoy de acuerdo. Como una pequeña advertencia: la -dopción readno está disponible en POSIX sh.
Patrick Oscity
Sí, de ahí mi zsh / bash . Consulte también mi uso de tail -n1 < "$f"para evitar problemas con los nombres de archivo que comienzan con -( tail -n1 -- "$f"no funciona para el archivo llamado -). Es posible que desee aclarar que la respuesta ahora es específica de zsh / bash.
Stéphane Chazelas
203

Aquí tiene :

sed -i -e '$a\' file

Y alternativamente para OS X sed:

sed -i '' -e '$a\' file

Esto se agrega \nal final del archivo solo si aún no termina con una nueva línea. Entonces, si lo ejecuta dos veces, no agregará otra nueva línea:

$ cd "$(mktemp -d)"
$ printf foo > test.txt
$ sed -e '$a\' test.txt > test-with-eol.txt
$ diff test*
1c1
< foo
\ No newline at end of file
---
> foo
$ echo $?
1
$ sed -e '$a\' test-with-eol.txt > test-still-with-one-eol.txt
$ diff test-with-eol.txt test-still-with-one-eol.txt
$ echo $?
0
l0b0
fuente
1
@jwd: De man sed: $ Match the last line.Pero tal vez solo funciona por accidente. Tu solución también funciona.
l0b0
1
Su solución también es más elegante, y la he probado y comprometido , pero ¿cómo puede funcionar? Si $coincide con la última línea, ¿por qué no agrega otra nueva línea a una cadena que ya contiene una nueva línea?
l0b0
27
Hay dos significados diferentes de $. Dentro de una expresión regular, como con el formulario /<regex>/, tiene el significado habitual de "final de línea de coincidencia". De lo contrario, se usa como dirección, sed le da el significado especial de "última línea en el archivo". El código funciona porque sed por defecto agrega una nueva línea a su salida si aún no está allí. El código "$ a \" solo dice "coincide con la última línea del archivo y no le agrega nada". Pero implícitamente, sed agrega la nueva línea a cada línea que procesa (como esta $línea) si aún no está allí.
jwd
1
Con respecto a la página de manual: La cita a la que se refiere está en la sección "Direcciones". Ponerlo adentro /regex/le da un significado diferente. Las páginas de manual de FreeBSD son un poco más informativo, pienso: freebsd.org/cgi/man.cgi?query=sed
Jwd
2
Si el archivo ya termina en una nueva línea, esto no lo cambia, pero lo reescribe y actualiza su marca de tiempo. Eso puede o no importar.
Keith Thompson
39

Echar un vistazo:

$ echo -n foo > foo 
$ cat foo
foo$
$ echo "" >> foo
$ cat foo
foo

entonces echo "" >> noeol-filedebería hacer el truco. (¿O quiso pedir identificar estos archivos y corregirlos?)

editar retira el ""de echo "" >> foo(ver el comentario de @ yuyichao) Edit2 añadió el ""nuevo ( pero ver el comentario de @Keith Thompson)

sr_
fuente
44
esto ""no es necesario (al menos para bash) y tail -1 | wc -lpuede usarse para encontrar el archivo sin una nueva línea al final
yuyichao
55
@yuyichao: No ""es necesario para bash, pero he visto echoimplementaciones que no imprimen nada cuando se invoca sin argumentos (aunque ninguno de los que puedo encontrar ahora hace esto). echo "" >> noeol-fileEs probablemente un poco más robusto. printf "\n" >> noeol-filees aún más.
Keith Thompson
2
@KeithThompson, csh's echoes la que se conoce a la salida de nada cuando no se pasa ningún argumento. Pero entonces, si vamos a apoyar conchas no Bourne-como, debemos hacer que echo ''en lugar de echo ""como echo ""sería ouput ""<newline>con rco espor ejemplo.
Stéphane Chazelas
1
@ StéphaneChazelas: Y tcsh, a diferencia csh, imprime una nueva línea cuando se invoca sin argumentos, independientemente de la configuración de $echo_style.
Keith Thompson
16

Otra solución usando ed. Esta solución solo afecta a la última línea y solo si \nfalta:

ed -s file <<< w

Básicamente funciona abriendo el archivo para editarlo a través de un script, el script es el wcomando único que escribe el archivo nuevamente en el disco. Se basa en esta oración que se encuentra en la ed(1)página del manual:

Limitaciones
       (...)

       Si un archivo de texto (no binario) no termina con un carácter de nueva línea,
       luego ed agrega uno para leerlo / escribirlo. En el caso de un binario
       file, ed no agrega una nueva línea en lectura / escritura.
enzotib
fuente
1
Esto no agrega una nueva línea para mí.
Olhovsky
44
Funciona para mi; incluso imprime "Newline adjunto" (ed-1.10-1 en Arch Linux).
Stefan Majewsky
12

Una manera simple, portátil y compatible con POSIX de agregar una nueva línea final ausente a un archivo de texto sería:

[ -n "$(tail -c1 file)" ] && echo >> file

Este enfoque no necesita leer el archivo completo; simplemente puede buscar EOF y trabajar desde allí.

Este enfoque tampoco necesita crear archivos temporales a sus espaldas (por ejemplo, sed -i), por lo que los enlaces duros no se ven afectados.

echo agrega una nueva línea al archivo solo cuando el resultado de la sustitución del comando es una cadena no vacía. Tenga en cuenta que esto solo puede suceder si el archivo no está vacío y el último byte no es una nueva línea.

Si el último byte del archivo es una nueva línea, tail lo devuelve, luego la sustitución del comando lo elimina; El resultado es una cadena vacía. La prueba -n falla y echo no se ejecuta.

Si el archivo está vacío, el resultado de la sustitución del comando también es una cadena vacía, y nuevamente echo no se ejecuta. Esto es deseable, porque un archivo vacío no es un archivo de texto no válido, ni es equivalente a un archivo de texto no vacío con una línea vacía.

IO descalzo
fuente
1
Tenga en cuenta que no funciona yashsi el último carácter del archivo es un carácter de varios bytes (en las configuraciones regionales UTF-8, por ejemplo), o si la configuración regional es C y el último byte del archivo tiene el octavo bit establecido. Con otros shells (excepto zsh), no agregaría una nueva línea si el archivo terminara en un byte NUL (pero nuevamente, eso significaría que la entrada no será de texto incluso después de agregar una nueva línea).
Stéphane Chazelas
1
@ StéphaneChazelas Se ha agregado una solución para yash .
sorontar
1
¿Es posible ejecutar esto para cada archivo en una carpeta y subcarpetas?
Qwerty
12

Agregar nueva línea independientemente:

echo >> filename

Aquí hay una manera de verificar si existe una nueva línea al final antes de agregar una, utilizando Python:

f=filename; python -c "import sys; sys.exit(open(\"$f\").read().endswith('\n'))" && echo >> $f
Alejandro
fuente
1
No usaría la versión de Python en ningún tipo de bucle debido al tiempo de inicio lento de Python. Por supuesto, podría hacer el bucle en Python si lo desea.
Kevin Cox
2
El tiempo de inicio de Python es 0.03 segundos aquí. ¿Realmente consideras que eso es problemático?
Alexander
3
El tiempo de inicio sí importa si llamas a python en un bucle, por eso dije considerar hacer el bucle en python. Entonces solo incurrirá en el costo de inicio una vez. Para mí, la mitad del costo de la puesta en marcha es más de la mitad del tiempo de todo el fragmento, consideraría esa sobrecarga sustancial. (Nuevamente, irrelevante si solo hace una pequeña cantidad de archivos)
Kevin Cox
2
echo ""Parece más robusto que echo -n '\n'. O podría usarprintf '\n'
Keith Thompson
2
Esto funcionó bien para mí
Daniel Gómez Rico
8

La solución más rápida es:

[ -n "$(tail -c1 file)" ] && printf '\n' >>file 

  1. Es realmente rapido.
    En un archivo de tamaño mediano, seq 99999999 >fileesto lleva milisegundos.
    Otras soluciones llevan mucho tiempo:

    [ -n "$(tail -c1 file)" ] && printf '\n' >>file  0.013 sec
    vi -ecwq file                                    2.544 sec
    paste file 1<> file                             31.943 sec
    ed -s file <<< w                             1m  4.422 sec
    sed -i -e '$a\' file                         3m 20.931 sec
  2. Funciona en ash, bash, lksh, mksh, ksh93, attsh y zsh pero no en yash.

  3. No cambia la marca de tiempo del archivo si no es necesario agregar una nueva línea.
    Todas las demás soluciones presentadas aquí cambian la marca de tiempo del archivo.
  4. Todas las soluciones anteriores son POSIX válidas.

Si necesita una solución portátil para cortar (y todos los demás shells enumerados anteriormente), puede ser un poco más complejo:

f=file
if       [ "$(tail -c1 "$f"; echo x)" != "$(printf '\nx')" ]
then     printf '\n' >>"$f"
fi
Isaac
fuente
7

La forma más rápida de probar si el último byte de un archivo es una nueva línea es leer solo ese último byte. Eso podría hacerse con tail -c1 file. Sin embargo, la forma simplista de probar si el valor de byte es una nueva línea, dependiendo de la eliminación habitual de una nueva línea final dentro de una expansión de comando falla (por ejemplo) en yash, cuando el último carácter del archivo es un UTF- 8 valor.

La forma correcta, compatible con POSIX, de todos los shells (razonables) para encontrar si el último byte de un archivo es una nueva línea es usar xxd o hexdump:

tail -c1 file | xxd -u -p
tail -c1 file | hexdump -v -e '/1 "%02X"'

Luego, comparar la salida de arriba 0Aproporcionará una prueba robusta.
Es útil para evitar agregar una nueva línea a un archivo vacío.
Archivo que no proporcionará un último carácter de 0A, por supuesto:

f=file
a=$(tail -c1 "$f" | hexdump -v -e '/1 "%02X"')
[ -s "$f" -a "$a" != "0A" ] && echo >> "$f"

Corto y dulce. Esto lleva muy poco tiempo, ya que solo lee el último byte (buscar EOF). No importa si el archivo es grande. Luego solo agregue un byte si es necesario.

No se necesitan ni se usan archivos temporales. No hay enlaces duros afectados.

Si esta prueba se ejecuta dos veces, no agregará otra nueva línea.

sorontar
fuente
1
@crw Creo que agrega información útil.
sorontar
2
Tenga en cuenta que xxdni hexdumplas utilidades POSIX tampoco . En el cofre de herramientas POSIX, hay od -An -tx1que obtener el valor hexadecimal de un byte.
Stéphane Chazelas
@ StéphaneChazelas Publique eso como respuesta; He venido aquí buscando este comentario muchas veces :)
Kelvin
@kelvin, he actualizado mi respuesta
Stéphane Chazelas
Tenga en cuenta que POSIX no garantiza que el valor de LF sea 0x0a. Todavía hay sistemas POSIX donde no está (basados ​​en EBCDIC), aunque son extremadamente raros en estos días.
Stéphane Chazelas
4

Es mejor corregir el editor del usuario que editó el archivo por última vez. Si usted es la última persona que ha editado el archivo, ¿qué editor está utilizando, supongo que textmate ...?

AD7six
fuente
2
Vim es el editor en cuestión. Pero en general, tienes razón, no solo debería arreglar los síntomas;)
k0pernikus
66
para vim, tienes que salir de tu camino y hacer el baile de archivo binario en guardar para que vim no agregue una nueva línea al final del archivo, simplemente no hagas ese baile. O, para corregir simplemente los archivos existentes, ábralos en vim y guarde el archivo y vim 'arreglará' la nueva línea faltante para usted (puede ser fácilmente programada para múltiples archivos)
AD7six
3
Mi emacsno añada una nueva línea al final del archivo.
enzotib
2
Gracias por el comentario @ AD7six, sigo recibiendo informes fantasmas de diffs cuando cometo cosas, sobre cómo el archivo original no tiene una nueva línea al final. No importa cómo edite un archivo con vim, no puedo hacer que no ponga una nueva línea allí. Entonces es solo vim hacerlo.
Steven Lu
1
@enzotib: Tengo (setq require-final-newline 'ask)en mi.emacs
Keith Thompson
3

Si solo desea agregar rápidamente una nueva línea al procesar alguna tubería, use esto:

outputting_program | { cat ; echo ; }

También es compatible con POSIX.

Luego, por supuesto, puede redirigirlo a un archivo.

MichalH
fuente
2
El hecho de que pueda usar esto en una tubería es útil. Esto me permite contar el número de filas en un archivo CSV, excluyendo el encabezado. Y ayuda a obtener un recuento de línea preciso en los archivos de Windows que no terminan con una nueva línea o un retorno de carro. cat file.csv | tr "\r" "\n" | { cat; echo; } | sed "/^[[:space:]]*$/d" | tail -n +2 | wc -l
Kyle Tolle
3

Siempre que no haya valores nulos en la entrada:

paste - <>infile >&0

... bastaría con añadir siempre una nueva línea al final de un archivo si aún no tenía una. Y solo necesita leer el archivo de entrada una vez para hacerlo bien.

Toby Speight
fuente
Eso no funcionará así, ya que stdin y stdout comparten la misma descripción de archivo abierto (así que el cursor dentro del archivo). Necesitarías en su paste infile 1<> infilelugar.
Stéphane Chazelas
2

Aunque no responde directamente a la pregunta, aquí hay un script relacionado que escribí para detectar archivos que no terminan en nueva línea. Es muy rápido.

find . -type f | # sort |        # sort file names if you like
/usr/bin/perl -lne '
   open FH, "<", $_ or do { print " error: $_"; next };
   $pos = sysseek FH, 0, 2;                     # seek to EOF
   if (!defined $pos)     { print " error: $_"; next }
   if ($pos == 0)         { print " empty: $_"; next }
   $pos = sysseek FH, -1, 1;                    # seek to last char
   if (!defined $pos)     { print " error: $_"; next }
   $cnt = sysread FH, $c, 1;
   if (!$cnt)             { print " error: $_"; next }
   if ($c eq "\n")        { print "   EOL: $_"; next }
   else                   { print "no EOL: $_"; next }
'

El script perl lee una lista de nombres de archivos (opcionalmente ordenados) desde stdin y para cada archivo lee el último byte para determinar si el archivo termina en una nueva línea o no. Es muy rápido porque evita leer todo el contenido de cada archivo. Produce una línea para cada archivo que lee, con el prefijo "error:" si se produce algún tipo de error, "vacío:" si el archivo está vacío (¡no termina con nueva línea!), "EOL:" ("fin de línea ") si el archivo termina con nueva línea y" sin EOL: "si el archivo no termina con nueva línea.

Nota: el script no maneja los nombres de archivo que contienen nuevas líneas. Si está en un sistema GNU o BSD, puede manejar todos los nombres de archivo posibles agregando -print0 para buscar, -z para ordenar y -0 para perl, de esta manera:

find . -type f -print0 | sort -z |
/usr/bin/perl -ln0e '
   open FH, "<", $_ or do { print " error: $_"; next };
   $pos = sysseek FH, 0, 2;                     # seek to EOF
   if (!defined $pos)     { print " error: $_"; next }
   if ($pos == 0)         { print " empty: $_"; next }
   $pos = sysseek FH, -1, 1;                    # seek to last char
   if (!defined $pos)     { print " error: $_"; next }
   $cnt = sysread FH, $c, 1;
   if (!$cnt)             { print " error: $_"; next }
   if ($c eq "\n")        { print "   EOL: $_"; next }
   else                   { print "no EOL: $_"; next }
'

Por supuesto, aún tendría que encontrar una forma de codificar los nombres de archivo con nuevas líneas en la salida (dejada como ejercicio para el lector).

La salida podría filtrarse, si se desea, para agregar una nueva línea a aquellos archivos que no tienen uno, más simplemente con

 echo >> "$filename"

La falta de una nueva línea final puede causar errores en las secuencias de comandos, ya que algunas versiones de shell y otras utilidades no manejarán correctamente una nueva línea final faltante al leer dicho archivo.

En mi experiencia, la falta de una nueva línea final se debe al uso de varias utilidades de Windows para editar archivos. Nunca he visto que vim cause una nueva línea final faltante al editar un archivo, aunque informará sobre dichos archivos.

Finalmente, hay secuencias de comandos mucho más cortas (pero más lentas) que pueden recorrer sus entradas de nombre de archivo para imprimir aquellos archivos que no terminan en nueva línea, como:

/usr/bin/perl -ne 'print "$ARGV\n" if /.\z/' -- FILE1 FILE2 ...
jrw32982
fuente
1

Los vi/ vim/ exeditores agregan automáticamente <EOL>en EOF a menos que el archivo ya lo tenga.

Así que prueba cualquiera de los dos:

vi -ecwq foo.txt

que es equivalente a:

ex -cwq foo.txt

Pruebas:

$ printf foo > foo.txt && wc foo.txt
0 1 3 foo.txt
$ ex -scwq foo.txt && wc foo.txt
1 1 4 foo.txt

Para corregir varios archivos, verifique: ¿Cómo reparar 'No hay nueva línea al final del archivo' para muchos archivos? en SO

¿Por qué esto es tan importante? Para mantener nuestros archivos POSIX compatibles .

kenorb
fuente
0

Para aplicar la respuesta aceptada a todos los archivos en el directorio actual (más subdirectorios):

$ find . -type f -exec sed -i -e '$a\' {} \;

Esto funciona en Linux (Ubuntu). En OS X, probablemente tenga que usar -i ''(no probado).

Friederbluemle
fuente
44
Tenga en cuenta que find .enumera todos los archivos, incluidos los archivos en .git. Para excluir:find . -type f -not -path './.git/*' -exec sed -i -e '$a\' {} \;
friederbluemle
Ojalá hubiera leído este comentario / pensado antes de ejecutarlo. Oh bien.
kstev
0

Al menos en las versiones de GNU, simplemente grep ''oawk 1 canoniza su entrada, agregando una nueva línea final si aún no está presente. Copian el archivo en el proceso, lo que lleva tiempo si es grande (¿pero la fuente no debería ser demasiado grande para leer de todos modos?) Y actualiza el modtime a menos que haga algo como

 mv file old; grep '' <old >file; touch -r old file

(aunque puede estar bien en un archivo que está registrando porque lo modificó) y pierde enlaces duros, permisos no predeterminados y ACL, etc. a menos que sea aún más cuidadoso.

dave_thompson_085
fuente
O simplemente grep '' file 1<> file, aunque todavía leería y escribiría el archivo por completo.
Stéphane Chazelas
-1

Esto funciona en AIX ksh:

lastchar=`tail -c 1 *filename*`
if [ `echo "$lastchar" | wc -c` -gt "1" ]
then
    echo "/n" >> *filename*
fi

En mi caso, si al archivo le falta la nueva línea, el wccomando devuelve un valor de 2y escribimos una nueva línea.

Daemoncan
fuente
La retroalimentación vendrá en forma de votos positivos o negativos, o se le pedirá en los comentarios que describa más sus respuestas / preguntas, no tiene sentido pedirla en el cuerpo de la respuesta. ¡Mantenlo al punto, bienvenido a stackexchange!
k0pernikus
-1

Agregando a la respuesta de Patrick Oscity , si solo desea aplicarlo a un directorio específico, también puede usar:

find -type f | while read f; do tail -n1 $f | read -r _ || echo >> $f; done

Ejecute esto dentro del directorio al que desea agregar nuevas líneas.

cifrar
fuente
-1

echo $'' >> <FILE_NAME> agregará una línea en blanco al final del archivo.

echo $'\n\n' >> <FILE_NAME> agregará 3 líneas en blanco al final del archivo.

usuario247137
fuente
El StackExchange tiene un formato divertido, lo arreglé para usted :-)
Peter
-1

Si su archivo finaliza con terminaciones de línea de Windows\r\n y está en Linux, puede usar este sedcomando. Solo se agrega \r\na la última línea si aún no está allí:

sed -i -e '$s/\([^\r]\)$/\1\r\n/'

Explicación:

-i    replace in place
-e    script to run
$     matches last line of a file
s     substitute
\([^\r]\)$    search the last character in the line which is not a \r
\1\r\n    replace it with itself and add \r\n

Si la última línea ya contiene un, \r\nentonces la expresión regular de búsqueda no coincidirá, por lo tanto, no sucederá nada.

masgo
fuente
-1

Podrías escribir un fix-non-delimited-lineguión como:

#! /bin/zsh -
zmodload zsh/system || exit
ret=0
for file do
  if sysopen -rwu0 -- "$file"; then
    if sysseek -w end -1; then
      read -r x || print -u0
    else
      syserror -p "Can't seek in $file before the last byte: "
      ret=1
    fi
  else
    ret=1
  fi
done
exit $ret

Contrariamente a algunas de las soluciones dadas aquí,

  • debería ser eficiente ya que no bifurca ningún proceso, solo lee un byte para cada archivo y no reescribe el archivo (solo agrega una nueva línea)
  • no romperá enlaces simbólicos / enlaces duros ni afectará a los metadatos (también, ctime / mtime solo se actualizan cuando se agrega una nueva línea)
  • debería funcionar bien incluso si el último byte es un NUL o es parte de un carácter de varios bytes.
  • debería funcionar bien independientemente de qué caracteres o no caracteres puedan contener los nombres de archivo
  • Debe manejar correctamente los archivos ilegibles, no escribibles o no buscables (e informar los errores en consecuencia)
  • No debe agregar una nueva línea a los archivos vacíos (pero informa un error sobre una búsqueda no válida en ese caso)

Puedes usarlo por ejemplo como:

that-script *.txt

o:

git ls-files -z | xargs -0 that-script

POSIXY, podría hacer algo funcionalmente equivalente con

export LC_ALL=C
ret=0
for file do
  [ -s "$file" ] || continue
  {
    c=$(tail -c 1 | od -An -vtc)
    case $c in
      (*'\n'*) ;;
      (*[![:space:]]*) printf '\n' >&0 || ret=$?;;
      (*) ret=1;; # tail likely failed
    esac
  } 0<> "$file" || ret=$? # record failure to open
done
Stéphane Chazelas
fuente