Definindo o IFS para uma única instrução

40

Eu sei que um valor personalizado do IFS pode ser definido para o escopo de um único comando / built-in. Existe uma maneira de definir um valor personalizado do IFS para uma única instrução? Aparentemente não, já que, com base no abaixo, o valor global do IFS é afetado quando isso é tentado

#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
    
por iruvar 24.09.2013 / 18:37

7 respostas

37

Em alguns shells (incluindo bash ):

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

(com bash , você pode omitir o command se não estiver em emulação sh / POSIX). Mas cuidado ao usar variáveis sem aspas, você geralmente também precisa de set -f , e não há escopo local para isso na maioria dos shells.

Com zsh, você pode fazer:

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

$=PATH é forçar a divisão de palavras, o que não é feito por padrão em zsh (a globulação após a expansão de variáveis também não é feita, portanto, você não precisa de set -f a menos que em shulation).

(){...} (ou function {...} ) são chamados de funções anônimas e são normalmente usados para definir um escopo local. com outras shells que suportem escopo local em funções, você poderia fazer algo similar com:

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

Para implementar um escopo local para variáveis e opções em shells POSIX, você também pode usar as funções fornecidas em link . Então você pode usá-lo como:

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

(a propósito, é inválido dividir $PATH dessa forma acima, exceto em zsh como em outros shells, o IFS é um delimitador de campo, não um separador de campo).

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

São apenas duas tarefas, uma após a outra, como a=1 b=2 .

Uma nota de explicação sobre var=value cmd :

Em:

var=value cmd arg

O shell executa /path/to/cmd em um novo processo e passa cmd e arg em argv[] e var=value em envp[] . Isso não é realmente uma atribuição de variáveis, mas mais variáveis de ambiente de passagem para o comando executado . No shell Bourne ou Korn, com set -k , você pode até escrever cmd var=value arg .

Agora, isso não se aplica a builtins ou funções que não são executadas . No shell Bourne, em var=value some-builtin , var acaba sendo definido posteriormente, assim como com var=value sozinho. Isso significa, por exemplo, que o comportamento de var=value echo foo (que não é útil) varia dependendo se o echo está embutido ou não.

POSIX e / ou ksh mudaram isso porque o comportamento de Bourne só acontece para uma categoria de builtins chamados special builtins . eval é um especial embutido, read não é. Para construções não especiais, var=value builtin define var apenas para a execução do builtin, o que faz com que ele se comporte de forma semelhante a quando um comando externo está sendo executado.

O comando command pode ser usado para remover o atributo especial dos especiais incorporados . O que POSIX ignorou é que, para os eval e . builtins, isso significaria que shells teriam que implementar uma pilha variável (mesmo que não especifique os comandos% limitlocal ou typeset scope limiting), porque você poderia fazer:

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

Ou até mesmo:

a=1 command eval myfunction

com myfunction sendo uma função usando ou configurando $a e possivelmente chamando command eval .

Isso foi realmente uma negligência porque ksh (em que a especificação é baseada principalmente) não o implementou (e AT & T ksh e zsh ainda não o fazem), mas hoje em dia, exceto esses dois , a maioria das shells implementa isso. O comportamento varia entre os shells em coisas como:

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

embora. Usar local em shells compatíveis é uma maneira mais confiável de implementar o escopo local.

    
por 24.09.2013 / 19:15
11

Salvamento e restauração padrão retirados de "The Unix Programming Environment" por Kernighan e Pike:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}
    
por 24.09.2013 / 18:44
7

Coloque seu script em uma função e chame essa função passando os argumentos da linha de comando para ela. Como o IFS é definido localmente, as alterações nele não afetam o IFS global.

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"
    
por 25.09.2013 / 10:26
6

Para este comando:

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

Existe uma solução alternativa: para dar a primeira atribuição ( IFS=$'\n' ) um comando para executar (uma função):

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

Isso colocará o IFS no ambiente para chamar a divisão, mas não será retido no ambiente atual.

Isso também evita o uso sempre arriscado de eval.

    
por 21.02.2016 / 05:35
4

A resposta proposta do @helpermethod é certamente uma abordagem interessante. Mas também é uma armadilha porque no escopo da variável local BASH se estende do chamador para a função chamada. Portanto, definir IFS em main () resultará nesse valor persistente para funções chamadas de main (). Aqui está um exemplo:

#!/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 "$@"

E a saída ...

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

Se o IFS declarado em main () ainda não estava no escopo em func (), então o array não teria sido apropriadamente analisado em func () B. Descomente a primeira linha em func () e você obtém esta saída:

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

Qual é o que você deve obter se a IFS estiver fora do escopo.

Uma solução muito melhor, IMHO, é abrir mão de mudar ou depender do IFS no nível global / local. Em vez disso, crie uma nova shell e mexa com o IFS lá. Por exemplo, se você fosse chamar func () em main () como segue, passando a matriz como uma string com um separador de campo de barra invertida:

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

... essa alteração no IFS não será refletida no func (). A matriz será passada como uma string:

ick\blick\flick

... mas dentro de func () o IFS ainda será "/" (como definido em main ()) a menos que seja alterado localmente em func ().

Mais informações sobre o isolamento de alterações no IFS podem ser visualizadas nos seguintes links:

Como faço para converter uma variável do array bash em um string delimitada por novas linhas?

String de texto para array com IFS

Sugestões e dicas para geral de scripts Programação de script shell - Veja "OBSERVE o uso de sub-shells ..."

    
por 05.11.2014 / 22:06
1

Este trecho da pergunta:

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

é interpretado como duas atribuições de variáveis globais separadas avaliadas da esquerda para a direita e é equivalente a:

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

ou

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

Isso explica por que o IFS global foi modificado e por que a divisão de palavras de $str nos elementos da matriz foi executada usando o novo valor de IFS .

Você pode se sentir tentado a usar um subshell para limitar o efeito da modificação de IFS da seguinte forma:

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]}'"

mas você notará rapidamente que a modificação de a também está limitada à sub-lista:

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

Em seguida, você será tentado a salvar / restaurar o IFS usando a solução de esta resposta anterior de @msw ou para tentar usar um local IFS dentro de uma função conforme sugerido por @helpermethod. Mas logo, você percebe que está em todos os tipos de problemas, especialmente se você for um autor de uma biblioteca que precisa ser robusto para se comportar incorretamente ao invocar scripts:

  • E se IFS estivesse inicialmente desativado?
  • E se estivermos executando com set -u (a.k.a set -o nounset )?
  • E se IFS fosse feito somente leitura via declare -r IFS ?
  • E se eu precisar que o mecanismo de salvamento / restauração funcione com recursão e / ou execução assíncrona (como trap handler ')?

Por favor, não salve / restaure o IFS. Em vez disso, mantenha as modificações temporárias:

  • Para limitar a modificação da variável a um único comando, chamada interna ou de função, use IFS="value" command .

    • Para ler em várias variáveis, dividindo em um caractere específico ( : usado abaixo como exemplo), use:

      IFS=":" read -r var1 var2 <<< "$str"
      
    • Para ler em um array use (faça isso em vez de array_var=( $str ) ):

      IFS=":" read -r -a array_var <<< "$str"
      
  • Limite os efeitos de modificar a variável para um subnível.

    • Para produzir os elementos de uma matriz separados por vírgula:

      (IFS=","; echo "${array[*]}")
      
    • Para capturar isso em uma string:

      csv="$(IFS=","; echo "${array[*]}")"
      
por 27.07.2018 / 18:54
0

A solução mais direta é pegar uma cópia do original $IFS , como em, e. a resposta do msw. No entanto, essa solução não distingue entre unset IFS e IFS set iguais à string vazia, o que é importante para muitas aplicações. Aqui está uma solução mais geral que captura essa distinção:

# 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
    
por 29.07.2018 / 21:55

Tags