Establecer IFS para una sola declaración

42

Sé que se puede establecer un valor IFS personalizado para el alcance de un solo comando / incorporado. ¿Hay alguna manera de establecer un valor IFS personalizado para una sola declaración? Aparentemente no, ya que en base a lo siguiente, el valor global de IFS se ve afectado cuando se intenta

#check environment IFS value, it is space-tab-newline
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003
#invoke built-in with custom IFS
IFS=$'\n' read -r -d '' -a arr <<< "$str"
#environment IFS value remains unchanged as seen below
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003

#now attempt to set IFS for a single statement
IFS=$'\n' a=($str)
#BUT environment IFS value is overwritten as seen below
printf "%s" "$IFS" | od -bc
0000000 012
         \n
     0000001
iruvar
fuente

Respuestas:

39

En algunas conchas (incluidas bash):

IFS=: command eval 'p=($PATH)'

(con bash, puede omitir commandsi no está en la emulación sh / POSIX). Pero tenga en cuenta que cuando usa variables sin comillas, generalmente también necesita hacerlo set -f, y no hay un alcance local para eso en la mayoría de los shells.

Con zsh, puedes hacer:

(){ local IFS=:; p=($=PATH); }

$=PATHes forzar la división de palabras que no se realiza de forma predeterminada en zsh( no se hace globbing en la expansión de variables, por lo que no es necesario a set -fmenos que sea ​​en emulación sh).

(){...}(o function {...}) se denominan funciones anónimas y generalmente se utilizan para establecer un ámbito local. con otros shells que admiten el alcance local en funciones, podría hacer algo similar con:

e() { eval "$@"; }
e 'local IFS=:; p=($PATH)'

Para implementar un ámbito local para variables y opciones en shells POSIX, también puede usar las funciones proporcionadas en https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh . Entonces puedes usarlo como:

. /path/to/locvar.sh
var=3,2,2
call eval 'locvar IFS; locopt -f; IFS=,; set -- $var; a=$1 b=$2 c=$3'

(por cierto, no es válido dividir de $PATHesa manera arriba excepto en zshcomo en otros shells, IFS es delimitador de campo, no separador de campo).

IFS=$'\n' a=($str)

Son solo dos tareas, una tras otra igual a=1 b=2.

Una nota de explicación sobre var=value cmd:

En:

var=value cmd arg

Los ejecuta shell /path/to/cmden un nuevo proceso y pases cmdy argen argv[]y var=valueen envp[]. Eso no es realmente una asignación de variables, sino más variables de entorno que pasan al comando ejecutado . En el shell Bourne o Korn, con set -k, incluso puedes escribirlo cmd var=value arg.

Ahora, eso no se aplica a las funciones o funciones incorporadas que no se ejecutan . En el shell Bourne, en var=value some-builtin, vartermina siendo establecido después, al igual que con var=valuesolo. Eso significa, por ejemplo, que el comportamiento de var=value echo foo(que no es útil) varía dependiendo de si echoestá incorporado o no.

POSIX y / o kshcambió eso en el sentido de que el comportamiento de Bourne solo ocurre para una categoría de incorporados llamados incorporados especiales . evales una construcción especial, readno lo es. Para el builtin no especial, se var=value builtinestablece varsolo para la ejecución del builtin que hace que se comporte de manera similar a cuando se ejecuta un comando externo.

El commandcomando se puede utilizar para eliminar el atributo especial de esas incorporaciones especiales . Lo POSIX se pasa por alto es que aunque para los evaly .las órdenes internas, eso significaría que las conchas tendría que implementar una pila variable (aunque no especifica los localo typesetalcance comandos de limitación), ya que podría hacer:

a=0; a=1 command eval 'a=2 command eval echo \$a; echo $a'; echo $a

O incluso:

a=1 command eval myfunction

con myfunctionser una función usando o configurando $ay potencialmente llamando command eval.

Eso fue realmente un descuido porque ksh(en el que se basa principalmente la especificación) no lo implementó (y AT&T kshy zshaún no lo hace), pero hoy en día, excepto esos dos, la mayoría de los shells lo implementan. El comportamiento varía entre los caparazones, aunque en cosas como:

a=0; a=1 command eval a=2; echo "$a"

aunque. El uso localde shells que lo soportan es una forma más confiable de implementar el alcance local.

Stéphane Chazelas
fuente
Extrañamente, se IFS=: command eval …establece IFSsolo para la duración de eval, según lo dispuesto por POSIX, en guión, pdksh y bash, pero no en ksh 93u. Es inusual ver a ksh como un extraño que no cumple.
Gilles 'SO- deja de ser malvado'
12

Guardar y restaurar estándar tomado de "El entorno de programación Unix" por Kernighan y Pike:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}
msw
fuente
2
gracias y +1. Sí, conozco esta opción, pero me gustaría saber si hay una opción "más limpia" si sabes a lo que me refiero
iruvar
Podrías atascarlo en una línea con punto y coma, pero no creo que sea más limpio. Podría ser bueno si todo lo que quería expresar tenido un apoyo especial sintáctica, pero entonces probablemente tendríamos que aprender carpintería o sumptin en lugar de codificar;)
msw
99
Eso no se restaura $IFScorrectamente si no se configuró previamente.
Stéphane Chazelas
2
Si no está configurado, Bash lo trata $'\t\n'' 'como se explica aquí: wiki.bash-hackers.org/syntax/expansion/…
davide
2
@davide, eso sería $' \t\n'. el espacio tiene que ser el primero ya que se usa para eso "$*". Tenga en cuenta que es lo mismo en todos los shells tipo Bourne.
Stéphane Chazelas
8

Ponga su script en una función e invoque esa función pasándole los argumentos de la línea de comandos. Como IFS se define localmente, los cambios no afectan al IFS global.

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"
método de ayuda
fuente
6

Para este comando:

IFS=$'\n' a=($str)

Hay una solución alternativa: dar a la primera asignación ( IFS=$'\n') un comando para ejecutar (una función):

$ split(){ a=( $str ); }
$ IFS=$'\n' split

Eso pondrá a IFS en el entorno para dividir las llamadas, pero no se mantendrá en el entorno actual.

Esto también evita el uso siempre riesgoso de eval.


fuente
En ksh93 y mksh, y bash y zsh cuando está en modo POSIX, eso todavía se deja $IFSconfigurado $'\n'como lo requiere POSIX.
Stéphane Chazelas
4

La respuesta propuesta de @helpermethod es ciertamente un enfoque interesante. Pero también es una trampa porque en BASH el alcance de la variable local se extiende desde el llamador hasta la función llamada. Por lo tanto, al establecer IFS en main (), el valor persistirá en las funciones llamadas desde main (). Aquí hay un ejemplo:

#!/usr/bin/env bash
#
func() {
  # local IFS='\'

  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local f_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#f_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${f_args[$i]}  "
  done
  echo
}

main() {
  local IFS='/'

  # the rest goes here
  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local m_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#m_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${m_args[$i]}  "
  done
  echo

  func "${m_args[*]}"
}

main "$@"

Y la salida ...

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick  [1]: blick  [2]: flick

Si IFS declarado en main () no estuviera todavía en alcance en func (), entonces la matriz no se habría analizado correctamente en func () B. Elimine el comentario de la primera línea en func () y obtendrá esta salida:

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick/blick/flick

Que es lo que debería obtener si IFS hubiera salido del alcance.

Una solución mucho mejor en mi humilde opinión, es renunciar a cambiar o confiar en IFS a nivel global / local. En cambio, genera un nuevo caparazón y juega con IFS allí. Por ejemplo, si tuviera que llamar a func () en main () de la siguiente manera, pasando la matriz como una cadena con un separador de campo de barra invertida:

func $(IFS='\'; echo "${m_args[*]}")

... ese cambio a IFS no se reflejará en func (). La matriz se pasará como una cadena:

ick\blick\flick

... pero dentro de func () el IFS seguirá siendo "/" (como se establece en main ()) a menos que se cambie localmente en func ().

Se puede ver más información sobre cómo aislar cambios en IFS en los siguientes enlaces:

¿Cómo convierto una variable de matriz bash en una cadena delimitada con líneas nuevas?

Bash string a array con IFS

Consejos y sugerencias para la programación general de scripts de shell: consulte "NOTA el uso de sub-shells ..."

markeissler
fuente
interesante de hecho ...
iruvar 05 de
"Bash string to array with IFS" ¡ IFS=$'\n' declare -a astr=(...)perfecto gracias!
Acuario Power
1

Este fragmento de la pregunta:

IFS=$'\n' a=($str)

se interpreta como dos asignaciones de variables globales separadas evaluadas de izquierda a derecha, y es equivalente a:

IFS=$'\n'; a=($str)

o

IFS=$'\n'
a=($str)

Esto explica por qué IFSse modificó el global y por qué la división de palabras $stren elementos de matriz se realizó utilizando el nuevo valor de IFS.

Es posible que tenga la tentación de usar una subshell para limitar el efecto de la IFSmodificación de esta manera:

str="value 0:value 1"
a=( old values )
( # Following code runs in a subshell
 IFS=":"
 a=($str)
 printf 'Subshell IFS: %q\n' "${IFS}"
 echo "Subshell: a[0]='${a[0]}' a[1]='${a[1]}'"
)
printf 'Parent IFS: %q\n' "${IFS}"
echo "Parent: a[0]='${a[0]}' a[1]='${a[1]}'"

pero notará rápidamente que la modificación de atambién está limitada a la subshell:

Subshell IFS: :
Subshell: a[0]='value 0' a[1]='value 1'
Parent IFS: $' \t\n'
Parent: a[0]='old' a[1]='values'

A continuación, estaría tentado a guardar / restaurar IFS utilizando la solución de esta respuesta anterior de @msw o intentar usar local IFSuna función interna como lo sugiere @helpermethod. Pero muy pronto, te das cuenta de que estás en todo tipo de problemas, especialmente si eres un autor de la biblioteca que necesita ser robusto contra comportamientos invocadores que se comportan mal:

  • ¿Qué pasa si IFSinicialmente no se configuró?
  • ¿Qué pasa si estamos corriendo con set -u(aka set -o nounset)?
  • ¿Qué pasa si IFSse hizo solo lectura a través de declare -r IFS?
  • ¿Qué trapsucede si necesito el mecanismo de guardar / restaurar para trabajar con recursividad y / o ejecución asincrónica (como un controlador ')?

Por favor no guarde / restaure IFS. En cambio, quédese con las modificaciones temporales:

  • Para limitar la modificación de la variable a un solo comando, invocación incorporada o de función, use IFS="value" command.

    • Para leer en múltiples variables dividiendo en un carácter específico ( :usado a continuación como ejemplo), use:

      IFS=":" read -r var1 var2 <<< "$str"
    • Para leer en una matriz, use (haga esto en lugar de array_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • Limite los efectos de modificar la variable a una subshell.

    • Para generar elementos de una matriz separados por comas:

      (IFS=","; echo "${array[*]}")
    • Para capturar eso en una cadena:

      csv="$(IFS=","; echo "${array[*]}")"
sls
fuente
0

La solución más directa es tomar una copia del original $IFS, como en, por ejemplo, la respuesta de msw. Sin embargo, esta solución no distingue entre un desarmado IFSy un IFSconjunto igual a la cadena vacía, lo cual es importante para muchas aplicaciones. Aquí hay una solución más general que captura esta distinción:

# Functions taking care of IFS
set_IFS(){
    if [ -z "${IFS+x}" ]; then
        IFS_ori="__unset__"
    else
        IFS_ori="$IFS"
    fi
    IFS="$1"
}
reset_IFS(){
    if [ "${IFS_ori}" == "__unset__" ]; then
        unset IFS
    else
        IFS="${IFS_ori}"
    fi
}

# Example of use
set_IFS "something_new"
some_program_or_builtin
reset_IFS
jmd_dk
fuente