¿Es un enfoque sensato "respaldar" la variable $ IFS?

19

Siempre dudo mucho en jugar $IFSporque es un golpe global.

Pero a menudo hace que cargar cadenas en una matriz bash sea agradable y conciso, y para las secuencias de comandos bash, la concisión es difícil de encontrar.

Así que me imagino que podría ser mejor que nada si trato de "guardar" el contenido inicial de $IFSotra variable y luego restaurarlo inmediatamente después de que termine de usar $IFSpara algo.

¿Es esto práctico? ¿O es esencialmente inútil y debería IFSvolver directamente a lo que sea necesario para sus usos posteriores?

Steven Lu
fuente
¿Por qué no sería práctico?
Bratchley
Porque un IFS inquietante haría bien el trabajo.
llua
1
Para aquellos que dicen que un IFS inestable funcionará bien, tenga en cuenta que es situacional: stackoverflow.com/questions/39545837/… . En mi experiencia, es mejor establecer IFS manualmente en el valor predeterminado para su intérprete de shell, es decir, $' \t\n'si está usando bash. unset $IFSsimplemente no siempre lo restaura a lo que esperarías que sea el predeterminado.
Darrel Holt

Respuestas:

9

Puede guardar y asignar a IFS según sea necesario. No hay nada de malo en hacerlo. No es raro guardar su valor para la restauración posterior a una modificación rápida y temporal, como su ejemplo de asignación de matriz.

Como @llua menciona en su comentario a su pregunta, simplemente deshabilitar IFS restaurará el comportamiento predeterminado, equivalente a asignar una nueva línea de tabulación de espacio.

Vale la pena considerar cómo puede ser más problemático no establecer / deshabilitar explícitamente IFS de lo que es hacerlo.

Desde la edición POSIX 2013, 2.5.3 Variables de Shell :

Las implementaciones pueden ignorar el valor de IFS en el entorno, o la ausencia de IFS del entorno, en el momento en que se invoca el shell, en cuyo caso el shell establecerá IFS en <space> <tab> <newline> cuando se invoca .

Un shell invocado compatible con POSIX puede o no heredar IFS de su entorno. De esto sigue:

  • Un script portátil no puede heredar de manera confiable IFS a través del entorno.
  • Una secuencia de comandos que tiene la intención de usar solo el comportamiento de división predeterminado (o unión, en el caso de "$*"), pero que puede ejecutarse bajo un shell que inicializa IFS desde el entorno, debe establecer / deshabilitar explícitamente IFS para defenderse de la intrusión ambiental.

Nota: es importante comprender que para esta discusión la palabra "invocado" tiene un significado particular. Un shell se invoca solo cuando se llama explícitamente con su nombre (incluido un #!/path/to/shellshebang). Un subshell, como podría ser creado por $(...)o cmd1 || cmd2 &, no es un shell invocado, y su IFS (junto con la mayoría de su entorno de ejecución) es idéntico al de su padre. Un shell invocado establece el valor de $su pid, mientras que los subshells lo heredan.


Esto no es simplemente una disquisición pedante; Existe una divergencia real en esta área. Aquí hay un breve script que prueba el escenario utilizando varios shells diferentes. Exporta un IFS modificado (establecido en :) a un shell invocado que luego imprime su IFS predeterminado.

$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
    printf '\n%s\n' "$sh"
    $sh -c 'printf %s "$IFS"' | hexdump -C
done

IFS generalmente no está marcado para exportación, pero, si lo fuera, tenga en cuenta cómo bash, ksh93 y mksh ignoran su entorno IFS=:, mientras que dash y busybox lo respetan.

$ sh export-IFS.sh

bash
00000000  20 09 0a                                          | ..|
00000003

ksh93
00000000  20 09 0a                                          | ..|
00000003

mksh
00000000  20 09 0a                                          | ..|
00000003

dash
00000000  3a                                                |:|
00000001

busybox:sh
00000000  3a                                                |:|
00000001

Alguna información de la versión:

bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1

Aunque bash, ksh93 y mksh no inicializan IFS desde el entorno, reexportan sus IFS modificados.

Si por alguna razón necesita pasar IFS de forma portátil a través del entorno, no puede hacerlo utilizando el propio IFS; necesitará asignar el valor a una variable diferente y marcar esa variable para exportar. Luego, los niños deberán asignar explícitamente ese valor a su IFS.

IO descalzo
fuente
Ya veo, así que si puedo parafrasear, podría decirse que es más portátil especificar explícitamente el IFSvalor en la mayoría de las situaciones en las que se va a utilizar, por lo que a menudo no es terriblemente productivo incluso intentar "preservar" su valor original.
Steven Lu
1
El problema primordial es que si su script usa IFS, debe establecer / deshabilitar explícitamente IFS para garantizar que su valor sea el que desea. Por lo general, el comportamiento de su script depende de IFS si hay expansiones de parámetros sin comillas, sustituciones de comandos sin comillas, expansiones aritméticas sin comillas, reads o referencias con comillas dobles $*. Esa lista está justo en la parte superior de mi cabeza, por lo que puede no ser completa (especialmente cuando se consideran las extensiones POSIX de los proyectiles modernos).
IO descalzo
10

En general, es una buena práctica restablecer las condiciones por defecto.

Sin embargo, en este caso, no tanto.

¿Por qué?:

Además, almacenar el valor IFS tiene un problema.
Si el IFS original no estaba configurado, el código IFS="$OldIFS"establecerá IFS en "", no lo desarmará.

Para mantener realmente el valor de IFS (incluso si no está configurado), use esto:

${IFS+"false"} && unset oldifs || oldifs="$IFS"    # correctly store IFS.

IFS="error"                 ### change and use IFS as needed.

${oldifs+"false"} && unset IFS || IFS="$oldifs"    # restore IFS.
Comunidad
fuente
IFS realmente no puede ser desarmado. Si lo desarma, el shell lo revierte al valor predeterminado. Por lo tanto, realmente no necesita verificar eso al guardarlo.
filbranden
Tenga en cuenta que en bash, unset IFSno puede deshabilitar IFS si se ha declarado local en un contexto primario (contexto de función) y no en el contexto actual.
Stéphane Chazelas
5

Tienes razón al dudar acerca de golpear a un global. No temas, es posible escribir un código de trabajo limpio sin modificar nunca el global real IFS, o hacer un baile de recuperación / restauración engorroso y propenso a errores.

Usted puede:

  • establecer IFS para una sola invocación:

    IFS=value command_or_function

    o

  • establecer IFS dentro de una subshell:

    (IFS=value; statement)
    $(IFS=value; statement)

Ejemplos

  • Para obtener una cadena delimitada por comas de una matriz:

    str="$(IFS=, ; echo "${array[*]-}")"

    Nota: El -solo es para proteger una matriz vacía set -ual proporcionar un valor predeterminado cuando se desarma (ese valor es la cadena vacía en este caso) .

    La IFSmodificación solo es aplicable dentro de la subshell generada por$() sustitución del comando . Esto se debe a que los subshells tienen copias de las variables del shell de invocación y, por lo tanto, pueden leer sus valores, pero cualquier modificación realizada por el subshell solo afecta la copia del subshell y no la variable del padre.

    También podría estar pensando: ¿por qué no omitir la subshell y simplemente hacer esto?

    IFS=, str="${array[*]-}"  # Don't do this!

    Aquí no hay invocación de comandos, y esta línea se interpreta como dos asignaciones de variables posteriores independientes, como si fuera:

    IFS=,                     # Oops, global IFS was modified
    str="${array[*]-}"

    Finalmente, expliquemos por qué esta variante no funcionará:

    # Notice missing ';' before echo
    str="$(IFS=, echo "${array[*]-}")" # Don't do this! 

    De echohecho, se llamará al comando con su IFSvariable establecida en ,, pero echono le importa ni lo usa IFS. La magia de expandirse"${array[*]}" a una cadena la realiza el (sub) shell antes de echoque incluso se invoque.

  • Para leer en un archivo completo (que no contiene NULLbytes) en una sola variable llamada VAR:

    IFS= read -r -d '' VAR < "${filepath}"

    Nota: IFS=es lo mismo que IFS=""y IFS='', todo lo cual establece IFS en la cadena vacía, que es muy diferente de unset IFS: si IFSno está configurado, el comportamiento de todas las funcionalidades de bash que usa internamente IFSes exactamente el mismo que siIFS tuviera el valor predeterminado de $' \t\n'.

    Ajuste IFS en la cadena vacía garantiza que se conservan los espacios en blanco iniciales y finales.

    La lectura -d ''o -d ""le dice que solo detenga su invocación actual en unNULL byte, en lugar de la nueva línea habitual.

  • Para dividir a lo $PATHlargo de sus :delimitadores:

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    Este ejemplo es puramente ilustrativo. En el caso general en el que se divide a lo largo de un delimitador, es posible que los campos individuales contengan (una versión escapada de) ese delimitador. Piense en tratar de leer una fila de.csv archivo cuyas columnas pueden contener comas (escapadas o citadas de alguna manera). El fragmento anterior no funcionará según lo previsto para tales casos.

    Dicho esto, es poco probable que encuentres tales :caminos de contención dentro $PATH. Si bien los nombres de ruta UNIX / Linux pueden contener un :, parece que bash no podría manejar tales rutas de todos modos si intenta agregarlos a su $PATHy almacenar archivos ejecutables en ellos, ya que no hay código para analizar dos puntos escapados / entre comillas : código fuente de bash 4.4 .

    Finalmente, tenga en cuenta que el fragmento agrega una nueva línea final al último elemento de la matriz resultante (como lo llamó @ StéphaneChazelas en los comentarios ahora eliminados), y que si la entrada es la cadena vacía, la salida será un elemento único matriz, donde el elemento consistirá en una nueva línea ( $'\n').

Motivación

El old_IFS="${IFS}"; command; IFS="${old_IFS}"enfoque básico que toca lo global IFSfuncionará como se espera para los scripts más simples. Sin embargo, tan pronto como agregue cualquier complejidad, puede separarse fácilmente y causar problemas sutiles:

  • Si commandes una función bash que también modifica el global IFS(ya sea directamente o, oculto a la vista, dentro de otra función que llama), y mientras lo hace por error usa la misma old_IFSvariable global para guardar / restaurar, obtiene un error.
  • Como se señaló en este comentario de @Gilles , si el estado original de IFSestaba desarmado, el ingenuo guardar y restaurar no funcionará, e incluso resultará en fallas absolutas si la opción de shell comúnmente (mal utilizada) set -u(aka set -o nounset) Está en vigor.
  • Es posible que algún código de shell se ejecute de forma asíncrona al flujo de ejecución principal, como con los manejadores de señales (ver help trap). Si ese código también modifica el global IFSo supone que tiene un valor particular, puede obtener errores sutiles.

Podría idear una secuencia de salvar / restaurar más robusta (como la propuesta en esta otra respuesta para evitar algunos o todos estos problemas. Sin embargo, tendría que repetir esa pieza de código repetitivo ruidoso donde sea que necesite temporalmente una costumbre) IFS. reduce la legibilidad y la facilidad de mantenimiento del código.

Consideraciones adicionales para scripts de biblioteca

IFSEsto es especialmente preocupante para los autores de bibliotecas de funciones de shell que necesitan asegurarse de que su código funcione de manera sólida independientemente del estado global ( IFS, opciones de shell, ...) impuesto por sus invocadores, y también sin alterar ese estado en absoluto (los invocadores pueden confiar en él para permanecer siempre estático).

Al escribir el código de la biblioteca, no puede confiar en IFStener ningún valor en particular (ni siquiera el predeterminado) o incluso establecerlo. En su lugar, debe establecer explícitamente IFScualquier fragmento cuyo comportamiento dependa IFS.

Si IFSse establece explícitamente en el valor necesario (incluso si ese es el predeterminado) en cada línea de código donde el valor importa utilizando cualquiera de los dos mecanismos descritos en esta respuesta es apropiado para localizar el efecto, entonces el código es ambos independiente del estado global y evita golpearlo por completo. Este enfoque tiene el beneficio adicional de hacerlo muy explícito para una persona que lee el script que IFSimporta precisamente para este comando / expansión a un costo textual mínimo (en comparación con incluso el guardado / restauración más básico).

¿Qué código se ve afectado de IFStodos modos?

Afortunadamente, no hay tantos escenarios en los que IFSimporta (suponiendo que siempre cites tus expansiones ):

  • "$*"y "${array[*]}"expansiones
  • invocaciones de la readorientación integrada de múltiples variables ( read VAR1 VAR2 VAR3) o una variable de matriz (read -a ARRAY_VAR_NAME )
  • invocaciones de readsegmentación de una sola variable cuando se trata de espacios en blanco iniciales o finales o no espacios en blanco que aparecen IFS.
  • división de palabras (como las expansiones sin comillas, que es posible que desee evitar como la peste )
  • algunos otros escenarios menos comunes (Ver: IFS @ Greg's Wiki )
sls
fuente
No puedo decir que entiendo el Para dividir $ PATH a lo largo de: delimitadores asumiendo que ninguno de los componentes contiene una oración : ellos mismos . ¿Cómo podrían contener los componentes :cuando :es el delimitador?
Stéphane Chazelas
@ StéphaneChazelas Bueno, :es un carácter válido para usar en un nombre de archivo en la mayoría de los sistemas de archivos UNIX / Linux, por lo que es completamente posible tener un directorio con un nombre que lo contenga :. Tal vez algunas conchas tienen una disposición para escapar :en el PATH utilizando algo como \:, y luego se verían columnas apareciendo que no son delimitadores reales (Parece fiesta no permite tales escape. La función de bajo nivel utilizado cuando la iteración en $PATHsólo búsquedas de :en una cadena C: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
sls
Revisé la respuesta para hacer que el $PATHejemplo de división sea :más claro.
sls
1
Bienvenido a SO! Gracias por una respuesta tan profunda :)
Steven Lu
1

¿Es esto práctico? ¿O es esencialmente inútil y debería configurar directamente IFS de nuevo a lo que sea necesario para sus usos posteriores?

¿Por qué arriesgarse a que un error tipográfico establezca IFS $' \t\n'cuando todo lo que tiene que hacer es

OIFS=$IFS
do_your_thing
IFS=$OIFS

Alternativamente, puede llamar a una subshell si no necesita ninguna variable establecida / modificada dentro de:

( IFS=:; do_your_thing; )
arielCo
fuente
Esto es peligroso porque no funciona si IFSinicialmente no se configuró.
Gilles 'SO- deja de ser malvado'