Configurando o IFS para uma única instrução

42

Eu sei que um valor IFS personalizado pode ser definido para o escopo de um único comando / interno. Existe uma maneira de definir um valor IFS personalizado para uma única instrução? Aparentemente não, pois 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
iruvar
fonte

Respostas:

39

Em algumas conchas (incluindo bash):

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

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

Com o zsh, você pode fazer:

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

$=PATHé forçar a divisão de palavras que não é feita por padrão em zsh(globbing na expansão de variáveis ​​também não é feito, então você não precisa, a set -fmenos que seja em emulação de sh).

(){...}(ou function {...}) são chamadas de funções anônimas e geralmente são usadas para definir um escopo local. com outros shells que oferecem suporte ao escopo local em funções, você pode fazer algo semelhante com:

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

Para implementar um escopo local para variáveis ​​e opções nos shells POSIX, você também pode usar as funções fornecidas em https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh . 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 $PATHdessa maneira acima, exceto zshem outros shells, o IFS é delimitador de campo, não separador de campo).

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

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

Uma nota explicativa sobre var=value cmd:

Em:

var=value cmd arg

Os executa shell /path/to/cmdem um novo processo e passes cmde argem argv[]e var=valueem envp[]. Isso não é realmente uma atribuição de variável, 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 funções internas ou funções que não são executadas . No shell Bourne, em var=value some-builtin, varacaba sendo definido posteriormente, assim como var=valuesozinho. Isso significa, por exemplo, que o comportamento de var=value echo foo(que não é útil) varia dependendo se echoestá embutido ou não.

O POSIX e / ou kshalterou o fato de que o comportamento Bourne ocorre apenas para uma categoria de componentes internos chamados componentes especiais . evalé um builtin especial, readnão é. Para embutidos não especiais, var=value builtindefine varapenas para a execução do embutido, o que o comporta de maneira semelhante a quando um comando externo está sendo executado.

O commandcomando pode ser usado para remover o especial atributo desses builtins especiais . O que o POSIX ignorou é que, para os evale .builtins, isso significaria que os shells precisariam implementar uma pilha variável (mesmo que não especifique os comandos de limitação de escopo localou typeset), porque você poderia fazer:

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

Ou até:

a=1 command eval myfunction

com myfunctionsendo uma função usando ou a criação $ae potencialmente chamando command eval.

Isso foi realmente um esquecimento, porque ksh(no qual a especificação é baseada principalmente) não a implementou (e a AT&T kshe zshainda não o fez), mas hoje em dia, exceto os dois, a maioria das conchas o implementa. O comportamento varia entre as conchas, embora em coisas como:

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

Apesar. O uso localde shells compatíveis é uma maneira mais confiável de implementar o escopo local.

Stéphane Chazelas
fonte
Estranhamente, IFS=: command eval …define IFSapenas a duração do eval, conforme exigido pelo POSIX, no hífen, pdksh e bash, mas não no ksh 93u. É incomum ver o ksh ser o único fora de conformidade.
Gilles 'SO- stop be evil'
12

Salvar e restaurar padrão extraído de "The Unix Programming Environment", de Kernighan e Pike:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}
msw
fonte
2
obrigado e +1. Sim, eu estou ciente desta opção, mas eu gostaria de saber se existe uma opção "mais limpa" se você sabe o que quero dizer
Iruvar
Você poderia colocá-lo em uma linha com ponto e vírgula, mas não acho que seja mais limpo. Pode ser bom se tudo o que você queria expressar teve apoio sintática especial, mas então nós provavelmente teria que aprender carpintaria ou sumptin em vez de codificação;)
msw
9
Isso falha ao restaurar $IFScorretamente se não estava definido anteriormente.
Stéphane Chazelas
2
Se não estiver definido, o Bash o tratará como $'\t\n'' ', conforme explicado aqui: wiki.bash-hackers.org/syntax/expansion/…
davide
2
@ David, isso seria $' \t\n'. o espaço precisa ser o primeiro a ser usado "$*". Observe que é o mesmo em todas as conchas do tipo Bourne.
Stéphane Chazelas
8

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

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"
helpermethod
fonte
6

Para este comando:

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

Existe uma solução alternativa: dar à 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 divisão, mas não será retido no ambiente atual.

Isso também evita o uso sempre arriscado de avaliação.


fonte
Nos ksh93 e mksh, e bash e zsh no modo POSIX, isso ainda é $IFSdefinido $'\n'como posteriormente, conforme exigido pelo POSIX.
Stéphane Chazelas
4

A resposta proposta de @helpermethod é certamente uma abordagem interessante. Mas também é uma armadilha, porque no BASH o escopo da variável local se estende do chamador à função chamada. Portanto, configurar o IFS em main () resultará na persistência desse valor 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 estivesse no escopo em func (), a matriz não teria sido analisada corretamente em func () B. Remova o comentário da 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 o IFS estiver fora do escopo.

Uma solução muito melhor, IMHO, é renunciar à mudança ou confiar no IFS em nível global / local. Em vez disso, crie uma nova concha e mexa com o IFS lá. Por exemplo, se você chamar func () em main () da seguinte maneira, passando a matriz como uma sequência com um separador de campo barra invertida:

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

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

ick\blick\flick

... mas dentro de func () o IFS ainda será "/" (conforme 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 uma string delimitada por novas linhas?

Cadeia de caracteres de basquete para matriz com IFS

Dicas e sugestões para programação geral de scripts de shell - Consulte "OBSERVAÇÃO do uso de subcascas ..."

markeissler
fonte
interessante mesmo ...
Iruvar
"Bash string to array with IFS" IFS=$'\n' declare -a astr=(...)perfeito obrigado!
Poder de Aquário
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 global IFSfoi modificado e por que a divisão da palavra $strem elementos da matriz foi realizada usando o novo valor de IFS.

Você pode ficar tentado a usar um subshell para limitar o efeito da IFSmodificação assim:

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 atambém está limitada ao subshell:

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ê seria tentado a salvar / restaurar o IFS usando a solução desta resposta anterior por @msw ou a tentar usar local IFSuma função interna, conforme sugerido por @helpermethod. Mas, em breve, você perceberá que está com todo tipo de problemas, especialmente se você é um autor de uma biblioteca que precisa ser robusto contra o mau comportamento de scripts de chamada:

  • E se IFSfoi inicialmente não definido?
  • E se estivermos executando com set -u(aka set -o nounset)?
  • E se IFSfosse feito somente leitura via declare -r IFS?
  • E se eu precisar do mecanismo de salvar / restaurar para trabalhar com recursão e / ou execução assíncrona (como um trapmanipulador`)?

Por favor, não salve / restaure o IFS. Em vez disso, atenha-se a modificações temporárias:

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

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

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

      IFS=":" read -r -a array_var <<< "$str"
  • Limite os efeitos da modificação da variável em um subshell.

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

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

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

A solução mais direta é tirar uma cópia do original $IFS, como em, por exemplo, a resposta do msw. No entanto, esta solução não distingue entre um conjunto IFSe um IFSconjunto igual à cadeia vazia, o que é importante para muitos aplicativos. 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
jmd_dk
fonte