El script de shell leyó la última línea que faltaba

Tengo un … extraño problema con un script de shell bash que esperaba obtener una idea.

Mi equipo está trabajando en un script que recorre líneas en un archivo y busca contenido en cada una. Tuvimos un error en el que, cuando se ejecutaba a través del proceso automático que secuencia secuencias de comandos diferentes, la última línea no se veía.

El código usado para iterar sobre las líneas en el 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 en el archivo, incluida la última, muy bien. Sin embargo, cuando se ejecuta por 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 no tiene ninguna nueva línea escrita al final del archivo.

Mi pregunta es en dos partes … ¿Por qué la última línea no sería vista por el código original, y por qué esto cambiaría la diferencia?

Solo pensé que podría pensar en por qué no se veía la última línea:

  • El proceso anterior, que escribe el archivo, dependía del proceso para finalizar y cerrar el descriptor del archivo.
  • La secuencia de comandos del problema se estaba iniciando y abriendo el archivo con la suficiente rapidez para que, mientras el proceso anterior había “finalizado”, no se “cerró / limpió” lo suficiente para que el sistema cerrara automáticamente el descriptor del archivo.

Dicho esto, parece que si tiene 2 comandos en un script de shell, el primero debería estar completamente apagado para 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 línea nueva o los datos después de que la última línea nueva no se lea correctamente.

ISO / IEC 9899: 2011 §7.21.2 Flujos

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 de terminación. Si la última línea requiere un carácter de nueva línea de terminación es definida por la implementación. Los caracteres pueden tener que ser agregados, alterados o eliminados en la entrada y salida para cumplir con las diferentes convenciones para representar texto en el entorno de host. Por lo tanto, no es necesario que haya una correspondencia de uno a uno entre los caracteres de una secuencia y los de la representación externa. Los datos leídos desde una secuencia de texto se compararán necesariamente igual a los datos que se escribieron anteriormente en esa secuencia solo si: los datos consisten únicamente en caracteres de impresión y la pestaña horizontal de caracteres de control y nueva línea; ningún carácter de línea nueva está precedido inmediatamente por caracteres de espacio; y el último personaje es un personaje de nueva línea. Si los caracteres de espacio que se escriben inmediatamente antes de que aparezca un carácter de nueva línea al leerlos están definidos por la implementación.

No tendría una nueva línea inesperada al final del archivo para causar problemas en bash (o en cualquier shell de Unix), pero ese parece ser el problema de forma reproducible ( $ es el mensaje en esta salida):

 $ 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 $( 

Tampoco se limita a bash - Korn shell ( ksh ) y zsh comportan así también. Vivo, aprendo; gracias por plantear el problema

Como se demuestra en el código anterior, el comando cat lee todo el archivo. La for line in `cat $DATAFILE` técnica for line in `cat $DATAFILE` recostack toda la salida y reemplaza las secuencias arbitrarias de espacios en blanco con un solo espacio en blanco (concluyo que cada línea en el archivo no contiene espacios en blanco).

Probado en Mac OS X 10.7.5.


¿Qué dice POSIX?

La especificación del comando de read POSIX dice:

La utilidad de lectura debe leer una sola línea de la entrada estándar.

Por defecto, a menos que se especifique la opción -r , actuará como un carácter de escape. Una no guardada preservará el valor literal del siguiente carácter, con la excepción de una . Si una sigue a la , la utilidad de lectura interpretará esto como una continuación de línea. La y la se eliminarán antes de dividir la entrada en los campos. Todos los demás caracteres no escamados deberán eliminarse después de dividir la entrada en campos.

Si la entrada estándar es un dispositivo terminal y el intérprete de invocación es interactivo, read pedirá una línea de continuación cuando lea una línea de entrada que termina con una , a menos que se especifique la opción -r .

La de terminación (si existe) 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 los parámetros (consulte División de campos); [...]

Tenga en cuenta que '(si hay alguno)' (énfasis añadido en la cita)! Me parece que si no hay una nueva línea, todavía debe leer el resultado. Por otro lado, también dice:

STDIN

La entrada estándar debe ser un archivo de texto.

y luego vuelves 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 (a menos que sea un archivo vacío), el procesamiento de las líneas de continuación cuando no se utiliza la opción -r puede dar como resultado terminando con una . Esto ocurre si la última línea del archivo de entrada finaliza con una . Es por esta razón que "si alguno" se utiliza en "La terminación (si hay) se eliminará de la entrada" en la descripción. No es una relajación del requisito de que la entrada estándar sea un archivo de texto.

Ese razonamiento debe significar 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 . 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 se trabaja en 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 que 'termina con una ' directamente, sino que difiere al estándar C.


Una solución al problema de la "nueva línea no terminal"

Tenga en cuenta la respuesta de Gordon Davisson . Una simple prueba muestra que su observación es precisa:

 $ while read line; do echo $line; done < y; echo $line abc def ghi xxx $ 

Por lo 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 estoy sorprendido de encontrar que las capas caen el último segmento (no se puede llamar una línea porque no termina con una nueva línea) de la entrada, pero puede 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 en una nueva línea.

Según la especificación POSIX para el comando de lectura , debería devolver un estado distinto de cero si “Fin de archivo se detectó o se produjo un error”. Como EOF se detecta cuando lee la última “línea”, establece $ line y luego devuelve un estado de error, y el estado de error evita que el ciclo se ejecute en la última “línea”. La solución es fácil: haga que el ciclo se ejecute si el comando de lectura tiene éxito O si se leyó algo en $ line.

 while read line || [ -n "$line" ]; do 

Agregar información adicional:

  1. No es necesario usar cat con while loop. while ...;do something;done es suficiente.
  2. No lea líneas con for .

Al usar while loop para leer líneas:

  1. Establezca el IFS correctamente (de lo contrario, puede perder una muesca).
  2. Casi siempre debes usar la opción -r con lectura.

con el cumplimiento de los requisitos anteriores, un ciclo while correcto se verá así:

 while IFS= read -r line; do ... done  

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  

O usando grep con while loop:

 while IFS= read -r line; do echo "$line" done < <(grep "" file) 

Sospecho que no tener nueva línea en la última línea de tu archivo podría estar causando este problema. Para las pruebas, puede hacer una ligera modificación en su script y leer DATAFILE de la siguiente manera:

 while read line do echo $line # do processing here done < "$DATAFILE" 

Y mira si esto hace alguna diferencia.

Use sed para hacer coincidir la última línea de un archivo, que luego agregará una nueva línea si no existe y le pedirá que haga una sustitución en línea del archivo:

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

El código es de este enlace stackexchange

Nota: He añadido comillas simples vacías a -i '' porque, al menos en OS X, -i estaba usando -e como una extensión de archivo para el archivo de copia de seguridad. Me hubiera gustado comentar sobre la publicación original pero carecía de 50 puntos. Tal vez esto me gane algunos en este hilo, gracias.

Probé esto en línea de comando

 # create dummy file. last line doesn't end with newline printf "%i\n%i\nNo-newline-here" >testing 

Pruebe con su primera forma (tubería a while-loop)

 cat testing | while read line; do echo $line; done 

Esto pasa por alto la última línea, lo cual tiene sentido ya que la read solo recibe una entrada que termina con una nueva línea.


Prueba con tu segunda forma (sustitución de comando)

 for line in `cat testbed1` ; do echo $line; done 

Esto obtiene la última línea también


read solo recibe entrada si termina en nueva línea, es por eso que te pierdes la última línea.

Por otro lado, en la segunda forma

 `cat testing` 

se expande a la forma de

 line1\nline2\n...lineM 

que está separado por el shell en múltiples campos utilizando IFS, para que pueda obtener

 line1 line2 line3 ... lineM 

Es por eso que todavía obtienes la última línea.

p / s: Lo que no entiendo es cómo obtienes la primera forma trabajando …

Como solución, antes de leer desde el archivo de texto, se puede agregar una nueva línea al archivo.

 echo "\n" >> $file_path 

Esto asegurará que se leerán todas las líneas que estaban previamente en el archivo.

Tuve un problema similar. Estaba haciendo un gato de un archivo, conectándolo a un tipo y luego conectando el resultado a ‘while read var1 var2 var3’. es decir: cat $ FILE | ordenar -k3 | mientras se lee Count IP Name do El trabajo debajo del “do” fue una statement if que identificó el cambio de datos en el campo $ Name y en base a cambio o no cambio sums de $ Count o impreso la línea sumda al informe. También me encontré con el problema donde no pude obtener la última línea para imprimir en el informe. Fui con el simple expediente de redireccionar cat / sort a un nuevo archivo, haciendo eco de una nueva línea para ese nuevo archivo y LUEGO ejecuté mi “mientras leía el nombre de IP de Count” en el nuevo archivo con resultados exitosos. es decir: cat $ FILE | ordenar -k3> NEWFILE echo “\ n” >> NEWFILE cat NEWFILE | mientras se lee Count IP Name do A veces lo simple, lo poco elegante es la mejor manera de hacerlo.