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?
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?
Respuestas:
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 -z
enumera 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 NUL
entradas 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.
find -name \*.java | while read f; do tail -n1 $f | read -r _ || echo >> $f; done
git ls-files
que aún le ahorrará editar archivos que no se rastrean en el control de versiones.
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 -d
opción read
no está disponible en POSIX sh.
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.
sed -i -e '$a\' file
Y alternativamente para OS X sed
:
sed -i '' -e '$a\' file
Esto se agrega \n
al 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
man sed
: $ Match the last line.
Pero tal vez solo funciona por accidente. Tu solución también funciona.
$
coincide con la última línea, ¿por qué no agrega otra nueva línea a una cadena que ya contiene una nueva línea?
$
. 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í.
/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
Echar un vistazo:
$ echo -n foo > foo
$ cat foo
foo$
$ echo "" >> foo
$ cat foo
foo
entonces echo "" >> noeol-file
deberí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)
""
no es necesario (al menos para bash) y tail -1 | wc -l
puede usarse para encontrar el archivo sin una nueva línea al final
""
es necesario para bash, pero he visto echo
implementaciones que no imprimen nada cuando se invoca sin argumentos (aunque ninguno de los que puedo encontrar ahora hace esto). echo "" >> noeol-file
Es probablemente un poco más robusto. printf "\n" >> noeol-file
es aún más.
csh
's echo
es 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 rc
o es
por ejemplo.
tcsh
, a diferencia csh
, imprime una nueva línea cuando se invoca sin argumentos, independientemente de la configuración de $echo_style
.
Otra solución usando ed
. Esta solución solo afecta a la última línea y solo si \n
falta:
ed -s file <<< w
Básicamente funciona abriendo el archivo para editarlo a través de un script, el script es el w
comando ú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.
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.
yash
si 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).
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
echo ""
Parece más robusto que echo -n '\n'
. O podría usarprintf '\n'
La solución más rápida es:
[ -n "$(tail -c1 file)" ] && printf '\n' >>file
Es realmente rapido.
En un archivo de tamaño mediano, seq 99999999 >file
esto 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
Funciona en ash, bash, lksh, mksh, ksh93, attsh y zsh pero no en yash.
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
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 0A
proporcionará 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.
xxd
ni hexdump
las utilidades POSIX tampoco . En el cofre de herramientas POSIX, hay od -An -tx1
que obtener el valor hexadecimal de un byte.
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 ...?
emacs
no añada una nueva línea al final del archivo.
(setq require-final-newline 'ask)
en mi.emacs
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.
cat file.csv | tr "\r" "\n" | { cat; echo; } | sed "/^[[:space:]]*$/d" | tail -n +2 | wc -l
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.
paste infile 1<> infile
lugar.
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 ...
Los vi
/ vim
/ ex
editores 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 .
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).
find .
enumera todos los archivos, incluidos los archivos en .git
. Para excluir:find . -type f -not -path './.git/*' -exec sed -i -e '$a\' {} \;
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.
grep '' file 1<> file
, aunque todavía leería y escribiría el archivo por completo.
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 wc
comando devuelve un valor de 2
y escribimos una nueva línea.
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.
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.
Si su archivo finaliza con terminaciones de línea de Windows\r\n
y está en Linux, puede usar este sed
comando. Solo se agrega \r\n
a 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\n
entonces la expresión regular de búsqueda no coincidirá, por lo tanto, no sucederá nada.
Podrías escribir un fix-non-delimited-line
guió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í,
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