¿Cómo encontrar archivos que no tienen una línea vacía al final?

9

Tengo archivos en subdirectorios del directorio actual que pueden tener o no nuevas líneas al final; ¿Cómo puedo encontrar archivos que no tienen una nueva línea al final?

He intentado esto:

find . -name '*.styl' | while read file; do
    awk 'END{print}' $file | grep -E '^$' > /dev/null || echo $file;
done

Pero no funciona. awk 'END{print}' $fileimprime la línea antes de una nueva línea vacía, igual que tail -n 1 $file.

jcubic
fuente
@don_crissti Necesito archivos que no tengan una línea vacía final.
jcubic
2
¿Puedo preguntarle por qué necesita encontrar esos archivos? Supongo que tiene que ver con el hecho de que los archivos de texto en Unix deben terminar con una nueva línea (vi "casi silenciosamente" agregará uno cuando guarde, por ejemplo), y varios comandos (orientados al texto) ignorarán última línea si no está terminada por una nueva línea (wc, iirc ... pero hay otras). Y esto puede ayudar
Olivier Dulac
awk 'END{print}' $file : esto ignora totalmente el contenido de $ file, y después de terminar de analizar todos los archivos contenidos en "$ file" agrega una nueva línea. Como es lo único que imprime el comando awk, podría reemplazarse por: printf '\n'(sin ningún mentino de $ file) y hacer lo mismo. Creo que esto NO es lo que buscabas (es decir, ¿imprimir la última línea del archivo?)
Olivier Dulac
@don_crissti: si el último carácter de un archivo no es una nueva línea, entonces ese archivo no es estrictamente posixly un archivo TEXT de Unix. ver: unix.stackexchange.com/a/263919/27616 . tenga en cuenta que muchos comandos de texto (wc, por ejemplo) simplemente ignoran esa última "línea" si no está terminada por una nueva línea
Olivier Dulac
1
@OlivierDulac: gawk imprime cy también FreeBSD, pero no me había dado cuenta de que está documentado como dependiente de la implementación: gnu.org/software/gawk/manual/… . Por lo que no suceda, pero no siempre.
dave_thompson_085

Respuestas:

14

Para aclarar, el carácter LF (también conocido \ncomo nueva línea) es el delimitador de línea , no es el separador de línea. Una línea no está terminada a menos que esté terminada por un carácter de nueva línea. Un archivo que solo contiene a\nbno es un archivo de texto válido porque contiene caracteres después de la última línea. Lo mismo para un archivo que solo contiene a. Un archivo que contiene a\ncontiene una línea no vacía.

Entonces, un archivo que termina con al menos una línea vacía termina con dos caracteres de nueva línea o contiene un solo carácter de nueva línea.

Si:

 tail -c 2 file | od -An -vtc

Salidas \no \n \n, entonces el archivo contiene al menos una línea vacía final. Si no genera nada, entonces ese es un archivo vacío, si genera <anything-but-\0> \n, entonces termina en una línea no vacía. Cualquier otra cosa, no es un archivo de texto.

Ahora, para usar eso para encontrar archivos que terminan en una línea vacía, OK, eso es eficiente (especialmente para archivos grandes) ya que solo lee los dos últimos bytes de los archivos, pero primero la salida no es fácilmente analizable programáticamente, especialmente teniendo en cuenta que es no es consistente de una implementación oda la siguiente, y tendríamos que ejecutar uno taily uno odpor archivo.

find . -type f -size +0 -exec gawk '
  ENDFILE{if ($0 == "") print FILENAME}' {} +

(para buscar archivos que terminen en una línea vacía) ejecutaría la menor cantidad de comandos posible, pero significaría leer el contenido completo de todos los archivos.

Idealmente, necesitaría un shell que pueda leer el final de un archivo por sí mismo.

Con zsh:

zmodload zsh/system
for f (**/*(D.L+0)) {
  {
    sysseek -w end -2
    sysread
    [[ $REPLY = $'\n' || $REPLY = $'\n\n' ]] && print -r -- $f
  } < $f
}
Stéphane Chazelas
fuente
una manera de utilizar el método de esta respuesta para saber si algún archivo (s) son archivos de texto: are_textfiles () { nontext=0; rem="return 0 if all args are files with terminating newline, or n [=number of non-textfiles]" ; for f in "$@" ; do [ -f "$f" ] && { tail -c 1 "$f" | od -An -vtc | grep "\\n" ;} >/dev/null 2>&1 || ((nontext++)) ; done ; return $nontext ; }. Usar como:if ( are_textfiles this that otherthing ) ; then echo all are text files ; else echo "are_textfiles returned : $?" ; fi
Olivier Dulac
6

Con gnu sedy un shell como zsh(o bashcon shopt -s globstar):

sed -ns '${/./F}' ./**/*.styl

esto comprueba si la última línea de cada archivo no está vacía, si es así, imprime el nombre del archivo.
Si desea lo contrario (imprima los nombres de los archivos si la última línea está vacía) simplemente reemplace /./con/^$/

don_crissti
fuente
1
Nunca visto -sen acción antes. ¡Gracias GNU!
Glenn Jackman
Nota: La opción F existe desde la versión 4.2.2 de sed (22 de diciembre de 2012)
Isaac
3

Un archivo de texto correctamente terminado con una última línea vacía termina en dos \n.

Entonces, esperamos que tail -c2sea ​​igual a $'\n\n'.

Lamentablemente, las expansiones de comando eliminan las nuevas líneas finales. Necesitaremos un poco de ajustes.

f=filename
nl='
'
t=$(tail -c2 $f; printf x)  # capture the last two characters.
r="${nl}${nl}$"                 # regex for: "ends in two newlines".
[[ ${t%x} =~ $r ]] &&  echo "file $f ends in an empty line"

Incluso podríamos expandirnos un poco para verificar qué archivos no tienen una nueva línea final:

nl='
'
nl=$'\n'
find . -type f -name '*.styl' | while read f; do
    t=$(tail -c2 $f; printf x); r1="${nl}$"; r2="${nl}${r1}"
    [[ ${t%x} =~ $r1 ]] || echo "file $f is missing a trailing newline"
    [[ ${t%x} =~ $r2 ]] && echo "$f"
done

Tenga en cuenta que la nueva línea podría cambiarse a algo así como $'\r\nsi fuera necesario.
En ese caso, también cambie tail -c2a tail -c4.

Isaac
fuente
0
for file in *; do
    # Check if the file is readable to avoid clutter
    if cat "./$file" 2&>1 /dev/null; then
        # Compare the last character with a single newline character.
        if [ -n "$(tail -c 1 -- "./$file")" ]; then
            echo "$file"
        fi
        # Also report empty files.
        if [ $(wc -c  < "./$file") -eq 0 ]; then
            echo "$file"
        fi
    fi
done
Oskar Skog
fuente
1
esto no funciona con archivos vacíos pero puedo vivir con eso.
jcubic
Puede haber algunos errores más porque la comparación de cadenas no parece funcionar como esperaba. Agregué un cheque por archivos vacíos.
Oskar Skog
Ah, ignora los caracteres de nueva línea.
Oskar Skog
Considere el más fácil de leer cat $file 2>&1 /dev/null, o si se trata sólo de Bash, cat $file &> /dev/null.
gato
1
Además, considere citar en $filetodos los lugares donde se usa, y por favor, use en $(commands ...)lugar de `backticks`...
cat