Entendiendo IFS


71

Los siguientes hilos en este sitio y StackOverflow fueron útiles para comprender cómo IFSfunciona:

Pero todavía tengo algunas preguntas cortas. Decidí preguntarles en la misma publicación, ya que creo que puede ayudar a futuros lectores mejores:

Q1. IFSse discute típicamente en el contexto de "división de campo". ¿La división de campos es lo mismo que la división de palabras ?

P2: La especificación POSIX dice :

Si el valor de IFS es nulo, no se realizará división de campo.

¿Es IFS=lo mismo establecer IFSque nulo? ¿Es esto lo que significa establecerlo también en un empty string?

P3: En la especificación POSIX, leo lo siguiente:

Si no se establece IFS, el shell se comportará como si el valor de IFS fuera <space>, <tab> and <newline>

Digamos que quiero restaurar el valor predeterminado de IFS. ¿Cómo puedo hacer eso? (más específicamente, ¿cómo me refiero <tab>y <newline>?)

P4: Finalmente, ¿cómo este código:

while IFS= read -r line
do    
    echo $line
done < /path_to_text_file

comportarse si cambiamos la primera línea a

while read -r line # Use the default IFS value

o para:

while IFS=' ' read -r line

Respuestas:


28
  1. Sí, son lo mismo.
  2. Si.
  3. En bash y conchas similares, podrías hacer algo así IFS=$' \t\n'. De lo contrario, puede insertar los códigos de control literales utilizando [space] CTRL+V [tab] CTRL+V [enter]. Sin embargo, si planea hacer esto, es mejor usar otra variable para almacenar temporalmente el IFSvalor anterior y luego restaurarlo (o anularlo temporalmente para un comando usando la var=foo commandsintaxis).
    • El primer fragmento de código colocará toda la línea leída, textualmente $line, ya que no hay separadores de campo para realizar la división de palabras. Sin embargo, tenga en cuenta que, dado que muchos shells usan cadenas cs para almacenar cadenas, la primera instancia de un NUL aún puede causar la aparición de su terminación prematura.
    • Es posible que el segundo fragmento de código no coloque una copia exacta de la entrada $line. Por ejemplo, si hay múltiples separadores de campo consecutivos, se convertirán en una sola instancia del primer elemento. Esto a menudo se reconoce como pérdida de espacio en blanco circundante.
    • El tercer fragmento de código hará lo mismo que el segundo, excepto que solo se dividirá en un espacio (no en el espacio habitual, la pestaña o la nueva línea).

3
La respuesta a la Q2 es incorrecta: un vacío IFSy un no establecido IFSson muy diferentes. La respuesta a Q4 es en parte incorrecta: los separadores internos no se tocan aquí, solo los iniciales y finales.
Gilles 'SO- deja de ser malvado'

3
@Gilles: en Q2 ninguna de las tres denominaciones dadas se refiere a un no establecido IFS, todas ellas significan IFS=.
Stéphane Gimenez

@Gilles En el segundo trimestre, nunca dije que fueran iguales. Y separadores internos se tocan, como se muestra aquí: IFS=' ' ; foo=( bar baz qux ) ; echo "${#foo[@]}". (Er, ¿qué? Debería haber múltiples delimitadores de espacio allí, SO motor sigue despojándolos).
Chris Down

2
@ StéphaneGimenez, Chris: Oh, cierto, perdón por la Q2, leí mal la pregunta. Para el cuarto trimestre, estamos hablando read; la última variable toma todo lo que queda, excepto el último separador y deja separadores internos dentro.
Gilles 'SO- deja de ser malvado'

1
Gilles es parcialmente correcto acerca de los espacios que no se eliminan mediante la lectura. Lee mi respuesta para más detalles.

22

Q1: sí. "División de campo" y "división de palabras" son dos términos para el mismo concepto.

Q2: sí. Si no IFSestá configurado (es decir, después unset IFS), es equivalente a IFSestar configurado en $' \t\n'(un espacio, una pestaña y una nueva línea). Si IFSse establece en un valor vacío (que es lo “nulo” significa aquí) (es decir, después IFS=o IFS=''o IFS=""), sin la división de campos se realiza en absoluto (y $*, lo que normalmente se utiliza el primer carácter $IFS, utiliza un carácter de espacio).

P3: Si desea tener el IFScomportamiento predeterminado , puede usarlo unset IFS. Si desea establecer IFSexplícitamente este valor predeterminado, puede poner los caracteres literales espacio, tabulación, nueva línea entre comillas simples. En ksh93, bash o zsh, puede usar IFS=$' \t\n'. Portablemente, si desea evitar tener un carácter de tabulación literal en su archivo fuente, puede usar

IFS=" $(echo t | tr t \\t)
"

P4: Con el IFSconjunto en un valor vacío, se read -r lineestablece lineen toda la línea excepto su nueva línea de terminación. Con IFS=" ", se recortan los espacios al principio y al final de la línea. Con el valor predeterminado de IFS, las pestañas y los espacios se recortan.


2
Q2 está parcialmente equivocado. Si IFS está vacío, "$ *" se une sin separadores. (para $@, hay algunas variaciones entre shells en contextos no listados como IFS=; var=$@). Debe tenerse en cuenta que cuando IFS está vacío, no se realiza la división de palabras, pero $ var todavía se expande a ningún argumento en lugar de un argumento vacío cuando $ var está vacío, y el globbing todavía se aplica, por lo que aún necesita citar variables (incluso si desactivar globbing)
Stéphane Chazelas

13

Q1. División de campo.

¿La división de campos es lo mismo que la división de palabras?

Sí, ambos apuntan a la misma idea.

P2: ¿Cuándo es nulo IFS ?

¿Establecer IFS=''lo mismo como nulo, lo mismo que una cadena vacía también?

Sí, los tres significan lo mismo: no se realizará división de campo / palabra. Además, esto afecta a los campos de impresión (como con echo "$*") todos los campos se concatenarán juntos sin espacio.

P3: (parte a) Desarmar IFS.

En la especificación POSIX, leo lo siguiente :

Si IFS no está configurado, el shell se comportará como si el valor de IFS fuera <space><tab> <newline> .

Que es exactamente equivalente a:

Con un unset IFS, el shell se comportará como si IFS es el predeterminado.

Eso significa que la 'División de campo' será exactamente la misma con un valor IFS predeterminado, o sin establecer.
Eso NO significa que IFS funcionará de la misma manera en todas las condiciones. Siendo más específico, la ejecución OldIFS=$IFSestablecerá la var OldIFScomo nula , no la predeterminada. E intentar volver a establecer IFS, ya que esto IFS=OldIFSestablecerá IFS en nulo, no lo mantendrá sin configurar como antes. Cuidado !!.

P3: (parte b) Restaurar IFS.

¿Cómo podría restaurar el valor de IFS al valor predeterminado? Digamos que quiero restaurar el valor predeterminado de IFS. ¿Cómo puedo hacer eso? (más específicamente, ¿cómo me refiero a <tab> y <newline> ?)

Para zsh, ksh y bash (AFAIK), IFS podría establecerse en el valor predeterminado como:

IFS=$' \t\n'        # works with zsh, ksh, bash.

Hecho, no necesitas leer nada más.

Pero si necesita volver a configurar IFS para sh, puede volverse complejo.

Echemos un vistazo desde el más fácil de completar sin inconvenientes (excepto la complejidad).

1.- Desarmar IFS.

Podríamos simplemente unset IFS(Leer Q3 parte a, arriba).

2.- Intercambiar caracteres.

Como solución alternativa, intercambiar el valor de tabulación y nueva línea simplifica la configuración del valor de IFS, y luego funciona de manera equivalente.

Establezca IFS en <space><newline> <tab> :

sh -c 'IFS=$(echo " \n\t"); printf "%s" "$IFS"|xxd'      # Works.

3.- ¿Un simple? solución:

Si hay secuencias de comandos secundarias que necesitan que IFS esté configurado correctamente, siempre puede escribir manualmente:

IFS = '   
'

Donde la secuencia escrita manualmente fue:, IFS='spacetabnewline'secuencia que realmente se ha escrito correctamente arriba (si necesita confirmar, edite esta respuesta). Pero una copia / pegar de su navegador se romperá porque el navegador exprimirá / ocultará el espacio en blanco. Hace que sea difícil compartir el código como se escribió anteriormente.

4.- Solución completa.

Escribir código que se pueda copiar de forma segura generalmente implica escapes imprimibles inequívocos.

Necesitamos un código que "produzca" el valor esperado. Pero, incluso si es conceptualmente correcto, este código NO establecerá un final \n:

sh -c 'IFS=$(echo " \t\n"); printf "%s" "$IFS"|xxd'      # wrong.

Eso sucede porque, en la mayoría de los shells, todas las nuevas líneas finales $(...)o `...`sustituciones de comandos se eliminan en la expansión.

Necesitamos usar un truco para sh:

sh -c 'IFS="$(printf " \t\nx")"; IFS="${IFS%x}"; printf "$IFS"|xxd'  # Correct.

Una forma alternativa puede ser establecer IFS como un valor de entorno de bash (por ejemplo) y luego llamar a sh (las versiones del mismo que aceptan que IFS se establezca a través del entorno), de esta manera:

env IFS=$' \t\n' sh -c 'printf "%s" "$IFS"|xxd'

En resumen, sh hace que restablecer IFS a valores predeterminados sea una aventura bastante extraña.

P4: en el código real:

Finalmente, cómo este código:

while IFS= read -r line
do
    echo $line
done < /path_to_text_file

comportarse si cambiamos la primera línea a

while read -r line # Use the default IFS value

o para:

while IFS=' ' read -r line

Primero: no sé si el echo $line(con la var NO citada) está ahí o no. Introduce un segundo nivel de 'división de campo' que la lectura no tiene. Entonces responderé a ambas. :)

Con este código (para que puedas confirmar). Necesitará el útil xxd :

#!/bin/ksh
# Correctly set IFS as described above.
defIFS="$(printf " \t\nx")"; defIFS="${defIFS%x}";
IFS="$defIFS"
printf "IFS value: "
printf "%s" "$IFS"| xxd -p

a='   bar   baz   quz   '; l="${#a}"
printf "var value          : %${l}s-" "$a" ; printf "%s\n" "$a" | xxd -p

printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x--          : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf 'Values      quoted :\n' ""  # With values quoted:
printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null    quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS default quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf '%s\n' "Values unquoted :"   # Now with values unquoted:
printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x-- unquoted : "
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null  unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS defau unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

Yo obtengo:

$ ./stackexchange-Understanding-IFS.sh
IFS value: 20090a
var value          :    bar   baz   quz   -20202062617220202062617a20202071757a2020200a
IFS --x--          :    bar   baz   quz   -20202062617220202062617a20202071757a202020
Values      quoted :
IFS null    quoted :    bar   baz   quz   -20202062617220202062617a20202071757a202020
IFS default quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS unset   quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS space   quoted :       bar   baz   quz-62617220202062617a20202071757a
Values unquoted :
IFS --x-- unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS null  unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS defau unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS unset unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS space unquoted : bar, baz, quz, 6261722c62617a2c71757a2c

El primer valor es solo el valor correcto de IFS='spacetabnewline'

La siguiente línea es todos los valores hexadecimales que tiene la var $a, y una nueva línea '0a' al final, ya que se le dará a cada comando de lectura.

La siguiente línea, para la cual IFS es nulo, no realiza ninguna 'división de campo', pero la nueva línea se elimina (como se esperaba).

Las siguientes tres líneas, como IFS contiene un espacio, eliminan los espacios iniciales y configuran la línea var con el saldo restante.

Las últimas cuatro líneas muestran lo que hará una variable sin comillas. Los valores se dividirán en los (varios) espacios y se imprimirán como:bar,baz,qux,


4

unset IFS borra IFS, incluso si se supone que IFS es "\ t \ n":

$ echo "'$IFS'"
'   
'
$ IFS=""
$ echo "'$IFS'"
''
$ unset IFS
$ echo "'$IFS'"
''
$ IFS=$' \t\n'
$ echo "'$IFS'"
'   
'
$

Probado en las versiones bash 4.2.45 y 3.2.25 con el mismo comportamiento.


La pregunta y la documentación vinculada no hablar unsetde IFS, como se explica en los comentarios de la respuesta aceptada aquí.
ILMostro_7
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.