Lectura del script de Shell falta la última línea
Tengo un... problema extraño con un script de shell bash del que esperaba obtener información.
Mi equipo está trabajando en un script que recorre las líneas de un archivo y verifica el contenido de cada una. Tuvimos un error en el que, cuando se ejecuta a través del proceso automatizado que secuencia diferentes scripts, no se veía la última línea.
El código utilizado para iterar sobre las líneas del archivo (el nombre almacenado en DATAFILE
era
cat "$DATAFILE" | while read line
Podríamos ejecutar el script desde la línea de comando y vería cada línea del archivo, incluida la última, sin problemas. Sin embargo, cuando lo ejecuta el proceso automatizado (que ejecuta el script que genera el DATAFILE justo antes del script en cuestión), la última línea nunca se ve.
Actualizamos el código para usar lo siguiente para iterar sobre las líneas y el problema se solucionó:
for line in `cat "$DATAFILE"`
Nota: DATAFILE nunca tiene una nueva línea escrita al final del archivo.
Mi pregunta consta de dos partes... ¿Por qué el código original no ve la última línea y por qué este cambio marcaría la diferencia?
Lo único que pensé que se me ocurrió por qué no se vería la última línea fue:
- El proceso anterior, que escribe el archivo, dependía de que el proceso finalizara para cerrar el descriptor del archivo.
- El script problemático se iniciaba y abría el archivo lo suficientemente rápido como para que, si bien el proceso anterior había "finalizado", no se había "apagado/limpiado" lo suficiente como para que el sistema cerrara el descriptor de archivo automáticamente.
Dicho esto, parece que si tiene 2 comandos en un script de shell, el primero debería estar completamente cerrado cuando el script ejecute el segundo.
Cualquier idea sobre las preguntas, especialmente la primera, sería muy apreciada.
El estándar C dice que los archivos de texto deben terminar con una nueva línea o los datos después de la última nueva línea pueden no leerse correctamente.
ISO/IEC 9899:2011 §7.21.2 Corrientes
Una secuencia de texto es una secuencia ordenada de caracteres compuestos en líneas, cada línea consta de cero o más caracteres más un carácter de nueva línea final. Si la última línea requiere un carácter de nueva línea final está definido por la implementación. Es posible que sea necesario agregar, modificar o eliminar caracteres en la entrada y salida para cumplir con diferentes convenciones para representar texto en el entorno anfitrión. Por lo tanto, no es necesario que haya una correspondencia uno a uno entre los caracteres de una secuencia y los de la representación externa. Los datos leídos de una secuencia de texto necesariamente se compararán con los datos que se escribieron anteriormente en esa secuencia solo si: los datos consisten únicamente en caracteres de impresión y los caracteres de control de tabulación horizontal y nueva línea; ningún carácter de nueva línea va precedido inmediatamente de caracteres de espacio; y el último carácter es un carácter de nueva línea. La implementación define si los caracteres de espacio que se escriben inmediatamente antes de un carácter de nueva línea aparecen cuando se leen.
No hubiera esperado que una nueva línea faltante al final del archivo causara problemas bash
(o cualquier shell de Unix), pero ese parece ser el problema reproducible ( $
es el mensaje en este resultado):
$ echo xxx\\c
xxx$ { echo abc; echo def; echo ghi; echo xxx\\c; } > y
$ cat y
abc
def
ghi
xxx$
$ while read line; do echo $line; done < y
abc
def
ghi
$ bash -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ ksh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ zsh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ for line in $(<y); do echo $line; done # Preferred notation in bash
abc
def
ghi
xxx
$ for line in $(cat y); do echo $line; done # UUOC Award pending
abc
def
ghi
xxx
$
Tampoco se limita a bash
: Korn shell ( ksh
) y zsh
también se comporta así. Vivo, aprendo; gracias por plantear el problema.
Como se demuestra en el código anterior, el cat
comando lee el archivo completo. La for line in `cat $DATAFILE`
técnica recopila todos los resultados y reemplaza secuencias arbitrarias de espacios en blanco con un solo espacio en blanco (concluyo que cada línea del archivo no contiene espacios en blanco).
Probado en Mac OS X 10.7.5.
¿Qué dice POSIX?
La especificación del comando POSIX read
dice:
La utilidad de lectura leerá una sola línea de la entrada estándar.
De forma predeterminada, a menos que
-r
se especifique la opción, <barra invertida> actuará como carácter de escape. Una <barra invertida> sin escape preservará el valor literal del siguiente carácter, con la excepción de una <nueva línea>. Si una <nueva línea> sigue a la <barra invertida>, la utilidad de lectura lo interpretará como una continuación de línea. La <barra invertida> y<newline>
se eliminará antes de dividir la entrada en campos. Todos los demás caracteres <barra invertida> sin escape se eliminarán después de dividir la entrada en campos.Si la entrada estándar es un dispositivo terminal y el shell que invoca es interactivo, read solicitará una línea de continuación cuando lea una línea de entrada que termine con una <barra invertida> <nueva línea>, a menos que
-r
se especifique la opción.La <nueva línea> final (si la hay) se eliminará de la entrada y los resultados se dividirán en campos como en el shell para los resultados de la expansión de parámetros (consulte División de campos); [...]
Tenga en cuenta que '(si corresponde)' (énfasis añadido entre comillas). Me parece que si no hay una nueva línea, igual debería leer el resultado. Por otro lado también dice:
ESTDIN
La entrada estándar será un archivo de texto.
y luego regresamos al debate sobre si un archivo que no termina con una nueva línea es un archivo de texto o no.
Sin embargo, el razonamiento en la misma página documenta:
Aunque se requiere que la entrada estándar sea un archivo de texto y, por lo tanto, siempre terminará con una <nueva línea> (a menos que sea un archivo vacío), el procesamiento de líneas de continuación cuando
-r
no se usa la opción puede resultar en que la entrada no termine con una <nueva línea>. Esto ocurre si la última línea del archivo de entrada termina con una <barra invertida> <nueva línea>. Es por esta razón que se utiliza "si existe" en "La <nueva línea> de terminación (si existe) se eliminará de la entrada" en la descripción. No se trata de una relajación del requisito de que la entrada estándar sea un archivo de texto.
Ese razonamiento debe significar que se supone que el archivo de texto debe terminar con una nueva línea.
La definición POSIX de un archivo de texto es:
3.395 Archivo de texto
Un archivo que contiene caracteres organizados en cero o más líneas. Las líneas no contienen caracteres NUL y ninguna puede exceder {LINE_MAX} bytes de longitud, incluido el carácter <nueva línea>. Aunque POSIX.1-2008 no distingue entre archivos de texto y archivos binarios (consulte el estándar ISO C), muchas utilidades solo producen resultados predecibles o significativos cuando operan con archivos de texto. Las utilidades estándar que tienen tales restricciones siempre especifican "archivos de texto" en sus secciones STDIN o INPUT FILES.
Esto no estipula 'termina con una <nueva línea>' directamente, pero se remite al estándar C y dice "Un archivo que contiene caracteres organizados en cero o más líneas " y cuando miramos la definición POSIX de una "Línea " dice:
Línea 3.206
Una secuencia de cero o más caracteres que no son <nueva línea> más un carácter <nueva línea> de terminación.
entonces, según la definición de POSIX, un archivo debe terminar en una nueva línea final porque está compuesto de líneas y cada línea debe terminar en una nueva línea final.
Una solución al problema de la 'nueva línea sin terminal'
Tenga en cuenta la respuesta de Gordon Davisson . Una prueba sencilla muestra que su observación es precisa:
$ while read line; do echo $line; done < y; echo $line
abc
def
ghi
xxx
$
Por tanto, su técnica de:
while read line || [ -n "$line" ]; do echo $line; done < y
o:
cat y | while read line || [ -n "$line" ]; do echo $line; done
funcionará para archivos sin una nueva línea al final (al menos en mi máquina).
Todavía me sorprende descubrir que los shells eliminan el último segmento (no se le puede llamar línea porque no termina con una nueva línea) de la entrada, pero podría haber suficiente justificación en POSIX para hacerlo. Y claramente es mejor asegurarse de que sus archivos de texto realmente sean archivos de texto que terminen con una nueva línea.
Según la especificación POSIX para el comando de lectura , debería devolver un estado distinto de cero si "se detectó fin de archivo o se produjo un error". Dado que se detecta EOF cuando lee la última "línea", establece $line
y luego devuelve un estado de error, y el estado de error impide que el bucle se ejecute en esa última "línea". La solución es fácil: haga que el bucle se ejecute si el comando de lectura tiene éxito O si se leyó algo $line
.
while read line || [ -n "$line" ]; do
Agregando información adicional:
- No es necesario utilizarlo
cat
con el bucle while.while ...;do something;done<file
es suficiente. - No leas líneas con
for
.
Cuando se utiliza el bucle while para leer líneas:
- Configure
IFS
correctamente (de lo contrario, puede perder la sangría). - Casi siempre deberías usar la opción -r con read.
Si se cumplen los requisitos anteriores, un bucle while adecuado se verá así:
while IFS= read -r line; do
...
done <file
Y para que funcione con archivos sin una nueva línea al final (volviendo a publicar mi solución desde aquí ):
while IFS= read -r line || [ -n "$line" ]; do
echo "$line"
done <file
O usando grep
con el bucle while:
while IFS= read -r line; do
echo "$line"
done < <(grep "" file)
Como solución alternativa, antes de leer el archivo de texto, se puede agregar una nueva línea al archivo.
echo -e "\n" >> $file_path
Esto asegurará que se lean todas las líneas que estaban previamente en el archivo. Necesitamos pasar el argumento -e a echo para permitir la interpretación de las secuencias de escape. https://superuser.com/questions/313938/shell-script-echo-new-line-to-file