Comprender "IFS = leer -r línea"

60

Obviamente entiendo que uno puede agregar valor a la variable de separador de campo interno. Por ejemplo:

$ IFS=blah
$ echo "$IFS"
blah
$ 

También entiendo que read -r lineguardará datos de stdinuna variable llamada line:

$ read -r line <<< blah
$ echo "$line"
blah
$ 

Sin embargo, ¿cómo puede un comando asignar un valor variable? ¿Y primero almacena datos de stdina variable liney luego le da valor linea IFS?

Martín
fuente
3
Relacionado: unix.stackexchange.com/q/169716/38906
cuonglm

Respuestas:

104

Algunas personas tienen esa noción errónea de que reades el comando de leer una línea. No es.

readlee palabras de una línea (posiblemente barra invertida), donde las palabras están $IFSdelimitadas y la barra invertida puede usarse para escapar de los delimitadores (o líneas continuas).

La sintaxis genérica es:

read word1 word2... remaining_words

readlee la entrada estándar de un byte a la vez hasta que encuentra un carácter de nueva línea sin escape (o al final de la entrada), se divide que de acuerdo con las reglas complejas y almacena el resultado de esa división en $word1, $word2... $remaining_words.

Por ejemplo en una entrada como:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

y con el valor predeterminado de $IFS, read a b casignaría:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

Ahora, si se pasa solo un argumento, ese no se convierte read line. Aun esta read remaining_words. El procesamiento de barra invertida todavía se realiza, los caracteres de espacio en blanco IFS aún se eliminan desde el principio y el final.

La -ropción elimina el procesamiento de la barra diagonal inversa. Entonces, el mismo comando anterior con en -rsu lugar asignaría

  • $afoo
  • $bbar\
  • $cbaz bl\ah blah\

Ahora, para la parte de división, es importante darse cuenta de que hay dos clases de caracteres para $IFS: los caracteres de espacio en blanco IFS (a saber, espacio y tabulación (y nueva línea, aunque aquí eso no importa a menos que use -d), que también sucede estar en el valor predeterminado de $IFS) y los demás. El tratamiento para esas dos clases de personajes es diferente.

Con IFS=:( :no siendo un carácter de espacio en blanco IFS), al igual que una entrada :foo::bar::se divide en "", "foo", "", bary ""(y un extra ""con algunas implementaciones, aunque eso no importa a excepción de read -a). Mientras que si reemplazamos eso :con espacio, la división se realiza solo en fooy bar. Eso es líder y los posteriores se ignoran, y sus secuencias se tratan como una sola. Hay reglas adicionales cuando se combinan los espacios en blanco y los no espacios en blanco $IFS. Algunas implementaciones pueden agregar / eliminar el tratamiento especial duplicando los caracteres en IFS ( IFS=::o IFS=' ').

Entonces, aquí, si no queremos que se eliminen los caracteres de espacio en blanco sin escape iniciales y finales, debemos eliminar esos caracteres de espacio en blanco IFS de IFS.

Incluso con caracteres IFS que no sean espacios en blanco, si la línea de entrada contiene uno (y solo uno) de esos caracteres y es el último carácter de la línea (como IFS=: read -r worden una entrada como foo:) con shells POSIX (no zshni algunas pdkshversiones), esa entrada se considera como una foopalabra porque en esos shells, los caracteres $IFSse consideran terminadores , por wordlo que contendrán foo, no foo:.

Entonces, la forma canónica de leer una línea de entrada con el readincorporado es:

IFS= read -r line

(tenga en cuenta que para la mayoría de las readimplementaciones, eso solo funciona para líneas de texto ya que el carácter NUL no es compatible, excepto en zsh).

El uso de la var=value cmdsintaxis asegura que IFSsolo se configure de manera diferente durante la duración de ese cmdcomando.

Nota de la historia

La readconstrucción fue introducida por el shell Bourne y ya era para leer palabras , no líneas. Hay algunas diferencias importantes con los modernos proyectiles POSIX.

El shell Bourne readno admitía una -ropción (que fue introducida por el shell Korn), por lo que no hay forma de deshabilitar el procesamiento de barra diagonal inversa que no sea el preprocesamiento de la entrada con algo como sed 's/\\/&&/g'eso.

El shell Bourne no tenía esa noción de dos clases de caracteres (que nuevamente fue presentada por ksh). En el shell Bourne todos los caracteres se someten al mismo tratamiento que los espacios en blanco IFS pueden hacer en ksh, es decir IFS=: read a b cen una entrada como foo::barasignaría bara $b, no la cadena vacía.

En el shell Bourne, con:

var=value cmd

Si cmdestá integrado (como reades), varpermanece configurado valuedespués de que cmdhaya terminado. Eso es particularmente crítico $IFSporque, en el shell Bourne, $IFSse usa para dividir todo, no solo las expansiones. Además, si elimina el carácter de espacio $IFSen el shell Bourne, "$@"ya no funciona.

En el shell Bourne, la redirección de un comando compuesto hace que se ejecute en un subshell (en las versiones más antiguas, incluso cosas como read var < fileo exec 3< file; read var <&3no funcionaban), por lo que era raro en el shell Bourne usar readcualquier cosa que no fuera la entrada del usuario en el terminal (donde el manejo de continuación de línea tenía sentido)

Algunos Unices (como HP / UX, también hay uno util-linux) todavía tienen un linecomando para leer una línea de entrada (que solía ser un comando estándar de UNIX hasta la versión 2 de la especificación UNIX única ).

Eso es básicamente lo mismo, head -n 1excepto que lee un byte a la vez para asegurarse de que no lea más de una línea. En esos sistemas, puede hacer:

line=`line`

Por supuesto, eso significa generar un nuevo proceso, ejecutar un comando y leer su salida a través de una tubería, por lo que es mucho menos eficiente que el de ksh IFS= read -r line, pero aún mucho más intuitivo.

Stéphane Chazelas
fuente
3
+1 Gracias por algunas ideas útiles de los diferentes tratamientos en espacio / tabulación frente a "otros" en IFS en bash ... Sabía que fueron tratados de manera diferente, pero esta explicación lo simplifica mucho. (¡Y la comprensión entre bash (y otros shells posix) y las shdiferencias regulares también es útil para escribir scripts portátiles!)
Olivier Dulac
Al menos para bash-4.4.19, while read -r; do echo "'$REPLY'"; donefunciona como while IFS= read -r line; do echo "'$line'"; done.
x-yuri
Esto: "... esa noción errónea de que leer es el comando para leer una línea ..." me lleva a pensar que si usar readpara leer una línea es erróneo, debe haber algo más. ¿Cuál podría ser esa noción no errónea? ¿O es esa primera afirmación técnicamente correcta, pero en realidad la noción no errónea es: "leer es el comando para leer palabras de una línea. Debido a que es tan poderoso, puede usarlo para leer líneas de un archivo haciendo: IFS= read -r line"
Mike S
8

La teoría

Hay dos conceptos que están en juego aquí:

  • IFSes el separador de campo de entrada, lo que significa que la lectura de la cadena se dividirá en función de los caracteres en IFS. En una línea de comando, IFSnormalmente hay cualquier espacio en blanco, es por eso que la línea de comando se divide en espacios.
  • Hacer algo como VAR=value commandsignifica "modificar el entorno de comando para que VARtenga el valor value". Básicamente, el comando commandverá VARque tiene el valor value, pero cualquier comando ejecutado después de eso seguirá VARteniendo su valor anterior. En otras palabras, esa variable se modificará solo para esa declaración.

En este caso

Entonces, al hacer IFS= read -r line, lo que está haciendo es establecer IFSuna cadena vacía (no se usará ningún carácter para dividir, por lo tanto, no se producirá división) para que readlea la línea completa y la vea como una palabra que se asignará a la linevariable. Los cambios IFSsolo afectan a esa declaración, de modo que los siguientes comandos no se verán afectados por el cambio.

Como nota al margen

Mientras que el comando es correcto y funciona según lo previsto, estableciendo IFSen este caso no es la fuerza 1 no sea necesario. Como está escrito en la bashpágina del manual en la readsección integrada:

Se lee una línea de la entrada estándar [...] y la primera palabra se asigna al primer nombre, la segunda palabra al segundo nombre, y así sucesivamente, con las palabras sobrantes y sus separadores intermedios asignados al apellido . Si se leen menos palabras de la secuencia de entrada que nombres, a los nombres restantes se les asignan valores vacíos. Los caracteres en IFSse utilizan para dividir la línea en palabras. [...]

Como solo tiene la linevariable, todas las palabras se le asignarán de todos modos, por lo que si no necesita ninguno de los caracteres de espacio en blanco anteriores y finales 1 , simplemente podría escribir read -r liney terminar con eso.

[1] Solo como un ejemplo de cómo un valor unsetpredeterminado $IFShará readque se considere el espacio en blanco IFS inicial / final , puede intentar:

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

Ejecútelo y verá que los caracteres anteriores y finales no sobrevivirán si IFSno está desarmado. Además, algunas cosas extrañas podrían suceder si $IFSse modificara en algún lugar anterior en el script.

user43791
fuente
5

Debe leer esta declaración en dos partes, la primera de ellas despeja el valor de la variable IFS, es decir, es equivalente a la más legible IFS="", el segundo es la lectura de la linevariable de entrada estándar, read -r line.

Lo que es específico en esta sintaxis es que la afectación de IFS es transciente y solo válida para el readcomando.

A menos que me falte algo, en ese caso en particular, la limpieza IFSno tiene ningún efecto, ya que lo que sea que IFSesté configurado, toda la línea se leerá en la linevariable. Hubiera habido un cambio en el comportamiento solo en el caso de que se haya pasado más de una variable como parámetro a la readinstrucción.

Editar:

El -restá allí para permitir la entrada termina con \no para ser procesado especialmente, es decir, para la barra invertida para ser incluido en la linevariable y no como un carácter de continuación para permitir la entrada de múltiples líneas.

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

El borrado de IFS tiene el efecto secundario de evitar que la lectura recorte potenciales espacios iniciales y finales o caracteres de tabulación, por ejemplo:

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

Gracias a rici por señalar esa diferencia.

jlliagre
fuente
Lo que falta es que si IFS no se cambia, read -r linerecortará los espacios en blanco iniciales y finales antes de asignar la entrada a la linevariable.
rici
@rici Sospechaba algo así, pero solo verificaba los caracteres IFS entre las palabras, no las iniciales / finales. ¡Gracias por señalar ese hecho!
jlliagre
borrar IFS también evitará la asignación de múltiples variables (efecto secundario). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"mostrará-aa bb--
kyodev