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 line
guardará datos de stdin
a la 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 stdin
? a variable line
y luego dar el valor de line
a IFS
?
Respuesta aceptada:
En shells POSIX, read
, sin ninguna opción no lee una línea , lee palabras de una línea (posiblemente continuación de una barra invertida), donde las palabras son $IFS
se puede usar delimitado y barra invertida para escapar de los delimitadores (o líneas continuas).
La sintaxis genérica es:
read word1 word2... remaining_words
read
lee stdin un byte a la vez¹ hasta que encuentra un carácter de nueva línea sin escape (o fin de entrada), lo divide de acuerdo con 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 blah blah
whatever whatever
y con el valor predeterminado de $IFS
, read a b c
asignaría:
$a
⇐foo
$b
⇐bar baz
$c
⇐blah blahwhatever whatever
Ahora, si se pasa solo un argumento, eso no se convierte en read line
. Todavía es read remaining_words
. El procesamiento de la barra invertida aún se realiza, los caracteres de espacio en blanco² de IFS aún se eliminan del principio y el final.
El -r
La opción elimina el procesamiento de la barra invertida. Así que el mismo comando anterior con -r
en su lugar asignaría
$a
⇐foo
$b
⇐bar
$c
⇐baz blah 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 de IFS² (incluidos el espacio y la tabulación (y la nueva línea, aunque aquí eso no importa a menos que use -d), que también están en el valor predeterminado de $IFS
) y los otros. El tratamiento para esas dos clases de personajes es diferente.
Con IFS=:
(:
no siendo un carácter de espacio en blanco IFS), una entrada como :foo::bar::
se dividiría en ""
, "foo"
, ""
, bar
y ""
(y un ""
adicional con algunas implementaciones, aunque eso no importa excepto para read -a
). Mientras que si reemplazamos ese :
con espacio, la división se realiza solo en foo
y bar
. Es decir, los principales y los posteriores se ignoran, y las secuencias de ellos se tratan como una sola. Hay reglas adicionales cuando los espacios en blanco y los caracteres que no son espacios en blanco se combinan en $IFS
. Algunas implementaciones pueden agregar/eliminar el tratamiento especial duplicando los caracteres en IFS (IFS=::
o IFS=' '
).
Así que aquí, si no queremos que se eliminen los espacios en blanco iniciales y finales sin escape, debemos eliminar esos espacios 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 word
en una entrada como foo:
) con shells POSIX (no zsh
ni algunos pdksh
versiones), esa entrada se considera como una foo
word porque en esos shells, los caracteres $IFS
se consideran terminadores , entonces word
contendrá foo
, no foo:
.
Entonces, la forma canónica de leer una línea de entrada con read
integrado es:
IFS= read -r line
(tenga en cuenta que para la mayoría de read
implementaciones, que solo funcionan para líneas de texto ya que el carácter NUL no es compatible excepto en zsh
).
Usando var=value cmd
la sintaxis se asegura de que IFS
solo se establece de manera diferente durante la duración de ese cmd
comando.
Nota de historia
El read
builtin fue introducido por el shell Bourne y ya estaba para leer words , no líneas. Hay algunas diferencias importantes con los shells POSIX modernos.
read
del shell de Bourne no admitía un -r
opción (que fue introducida por el shell Korn), por lo que no hay forma de deshabilitar el procesamiento de barra invertida que no sea preprocesar la entrada con algo como sed 's/\/&&/g'
allí.
El shell Bourne no tenía esa noción de dos clases de caracteres (que nuevamente fue introducido por ksh). En el shell de Bourne, todos los caracteres reciben el mismo tratamiento que los espacios en blanco de IFS en ksh, es decir, IFS=: read a b c
en una entrada como foo::bar
asignaría bar
a $b
, no la cadena vacía.
En el caparazón de Bourne, con:
var=value cmd
Si cmd
es un integrado (como read
es), var
permanece establecido en value
después de cmd
ha terminado Eso es particularmente crítico con $IFS
porque en el shell de Bourne, $IFS
se usa para dividir todo, no solo las expansiones. Además, si elimina el carácter de espacio de $IFS
en el shell de Bourne, "[email protected]"
ya no funciona.
En el shell de Bourne, la redirección de un comando compuesto hace que se ejecute en un subshell (en las primeras versiones, incluso cosas como read var < file
o exec 3< file; read var <&3
no funcionó), por lo que era raro en el shell de Bourne usar read
para cualquier cosa que no sea la entrada del usuario en la terminal (donde el manejo de continuación de línea tenía sentido)
Algunos Unices (como HP/UX, también hay uno en util-linux
) todavía tiene una line
Comando para leer una línea de entrada (que solía ser un comando UNIX estándar hasta la versión 2 de la especificación UNIX única).
Eso es básicamente lo mismo que head -n 1
excepto 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 IFS= read -r line
de ksh. , pero mucho más intuitivo.