¿Por qué se usa `while IFS = read` con tanta frecuencia, en lugar de` IFS =; mientras lee..`?

81

Parece que la práctica normal pondría la configuración de IFS fuera del ciclo while para no repetir la configuración para cada iteración ... ¿Es esto solo un estilo habitual de "mono ver, mono hacer", como lo ha sido para este mono hasta Leo hombre leído , ¿o me estoy perdiendo alguna trampa sutil (o descaradamente obvia) aquí?

Peter.O
fuente

Respuestas:

82

La trampa es que

IFS=; while read..

establece IFSpara todo el entorno de shell fuera del bucle, mientras que

while IFS= read

lo redefine solo para la readinvocación (excepto en el shell Bourne). Puedes comprobar que haciendo un ciclo como

while IFS= read xxx; ... done

luego, después de tal bucle, echo "blabalbla $IFS ooooooo"imprime

blabalbla
 ooooooo

mientras que después

IFS=; read xxx; ... done

las IFS estancias redefinidas: ahora echo "blabalbla $IFS ooooooo"imprime

blabalbla  ooooooo

Así que si se utiliza la segunda forma, usted tiene que recordar para restablecer: IFS=$' \t\n'.


La segunda parte de esta pregunta se ha fusionado aquí , por lo que he eliminado la respuesta relacionada de aquí.

rozcietrzewiacz
fuente
Bien, parece que una 'trampa' potencial es descuidar restablecer el IFS externo ... Pero me pregunto si también hay algo más en marcha ... He estado probando cosas aquí, con mucha febril, y he tenga en cuenta que configurar IFS en la lista de comandos while se comporta de manera diferente, dependiendo de si va seguido de dos puntos. No entiendo este comportamiento (todavía), y ahora me pregunto si hay alguna consideración especial involucrada en este nivel ... por ejemplo. while IFS=X readno se separa X, pero while IFS=X; readsí ...
Peter
(Significaste semi colon, ¿verdad?) La segunda whileno tiene mucho sentido - la condición de while extremos en ese punto y coma, así que no hay lazo real ... readse convierte en sólo el primer comando dentro del bucle de un solo elemento o no ... ? ¿Qué pasa con el doentonces ..?
rozcietrzewiacz
1
No, espera, tienes razón, puedes tener varios comandos en la whilecondición (antes do).
rozcietrzewiacz
Oh ... definitivamente, puedes tenerlos ... como te has dado cuenta ... pero parece que no les gusta el punto y coma ... (y el bucle seguirá en bucle hasta el infinito hasta que el último comando devuelva un no -código de salida cero) ... ahora me pregunto si la trampa se encuentra en un sector completamente diferente; el de entender cómo funciona la lista de comandos while , por ejemplo. ¿por qué IFS=el trabajo, pero IFS=Xno lo hace ... (o tal vez he sobredosis en esto durante un tiempo .. pausa para el café necesita :)
Peter.O
1
$ rozcietrzewiacz ... Vaya ... No había notado tu actualización, cuando moví mi actualización (como se mencionó en el comentario anterior) ... Parece interesante, y está empezando a tener sentido ... pero incluso por una noche ... pájaro como yo, es extremadamente tarde ... (Acabo de escuchar los pájaros de la mañana:) ... Dicho esto, me he recuperado un poco y leí tus ejemplos ... Creo que lo tengo, en realidad yo ' Estoy seguro de que lo tienes, pero debo dormir :) ... ¡Esto es casi un Eureka! momento ... gracias
Peter.O
45

Veamos un ejemplo, con un texto de entrada cuidadosamente elaborado:

text=' hello  world\
foo\bar'

Son dos líneas, la primera comienza con un espacio y termina con una barra diagonal inversa. Primero, echemos un vistazo a lo que sucede sin precauciones read(pero utilizando printf '%s\n' "$text"para imprimir cuidadosamente $textsin ningún riesgo de expansión). (A continuación, se $ ‌encuentra el indicador de comandos de la shell).

$ printf '%s\n' "$text" |
  while read line; do printf '%s\n' "[$line]"; done
[hello worldfoobar]

readcomió las barras diagonales inversas: la barra diagonal inversa-nueva línea hace que la nueva línea se ignore, y la barra diagonal inversa-cualquier cosa ignora esa primera barra diagonal inversa. Para evitar el tratamiento especial de las barras invertidas, utilizamos read -r.

$ printf '%s\n' "$text" |
  while read -r line; do printf '%s\n' "[$line]"; done
[hello  world\]
[foo\bar]

Eso es mejor, tenemos dos líneas como se esperaba. Las dos líneas casi contienen el contenido deseado: el doble espacio entre helloy worldse ha retenido, porque está dentro de la linevariable. Por otro lado, el espacio inicial fue devorado. Esto se debe a que readlee tantas palabras como le pasa variables, excepto que la última variable contiene el resto de la línea, pero aún comienza con la primera palabra, es decir, los espacios iniciales se descartan.

Por lo tanto, para leer cada línea literalmente, debemos asegurarnos de que no haya división de palabras . Hacemos esto estableciendo la IFSvariable en un valor vacío.

$ printf '%s\n' "$text" |
  while IFS= read -r line; do printf '%s\n' "[$line]"; done
[ hello  world\]
[foo\bar]

Tenga en cuenta cómo establecemos IFS específicamente para la duración de la readincorporada . Los IFS= read -r lineconjuntos de la variable de entorno IFS(a un valor vacío) específicamente para la ejecución de read. Esta es una instancia de la sintaxis de comando simple general : una secuencia (posiblemente vacía) de asignaciones de variables seguida de un nombre de comando y sus argumentos (también, puede lanzar redireccionamientos en cualquier punto). Como reades una función integrada, la variable nunca termina en el entorno de un proceso externo; no obstante, el valor de $IFSes lo que estamos asignando allí mientras se readesté ejecutando¹. Tenga en cuenta que readno es una función integrada especial , por lo que la asignación solo dura su duración.

Por lo tanto, nos encargamos de no cambiar el valor de IFSotras instrucciones que puedan depender de él. Este código funcionará sin importar en qué se haya configurado IFSinicialmente el código circundante , y no causará ningún problema si el código dentro del bucle se basa IFS.

Contraste con este fragmento de código, que busca archivos en una ruta separada por dos puntos. La lista de nombres de archivos se lee desde un archivo, un nombre de archivo por línea.

IFS=":"; set -f
while IFS= read -r name; do
  for dir in $PATH; do
    ## At this point, "$IFS" is still ":"
    if [ -e "$dir/$name" ]; then echo "$dir/$name"; fi
  done
done <filenames.txt

Si el bucle fuera while IFS=; read -r name; do …, entonces for dir in $PATHno se dividiría $PATHen componentes separados por dos puntos. Si el código fuera IFS=; while read …, sería aún más obvio que IFSno está configurado :en el cuerpo del bucle.

Por supuesto, sería posible restaurar el valor de IFSdespués de ejecutar read. Pero eso requeriría conocer el valor anterior, que es un esfuerzo adicional. IFS= reades el camino simple (y, convenientemente, también el camino más corto).

¹ Y, si readse interrumpe por una señal atrapada, posiblemente mientras la trampa se está ejecutando, POSIX no lo especifica y depende del shell en la práctica.

Gilles
fuente
44
Gracias Gilles ... una visita guiada muy agradable ... (¿quiso decir 'set -f'?) ... Ahora, para el lector, para reafirmar lo que ya se ha dicho, me gustaría enfatizar el tema que tenía yo mirándolo de la manera incorrecta. Primero y principal es el hecho de que la construcción while IFS= read(sin un punto y coma después =) no es una forma especial de whileo de IFSo de read... La construcción es genérica: es decir. anyvar=anyvalue anycommand. La falta de ;después del ajuste anyvarhace que el alcance de anyvar locales a anycommand.. El tiempo - hacer bucle / hecho es 100% no relacionada con el ámbito local any_var.
Peter
3

Aparte de la (ya clarificada) IFSlas diferencias de alcance entre el while IFS='' read, IFS=''; while ready while IFS=''; readexpresiones idiomáticas (por mando vs script / en toda la cáscara IFSámbito de las variables), la lección para llevar a casa es que se pierde los líderes y espacios al final de una línea de entrada si la variable IFS se establece en (contiene un) espacio.

Esto puede tener consecuencias bastante graves si se procesan las rutas de archivo.

Por lo tanto, establecer la variable IFS en la cadena vacía no es una mala idea, ya que garantiza que el espacio en blanco inicial y posterior de una línea no se elimine.

Ver también: Bash, leer línea por línea desde el archivo, con IFS

(
shopt -s nullglob
touch '  file with spaces   '
IFS=$' \t\n' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
IFS='' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
)
jon
fuente
+1 excelente demostración, limpieza después con 'rm * file * with * spaces *'
amdn
0

Inspirado por la respuesta de Yuzem

Si quieres establecer IFSun personaje real, esto funcionó para mí

iconv -f cp1252 zapni.tv.php | while IFS='#' read -d'#' line
do
  echo "$line"
done
Steven Penny
fuente