É uma abordagem sensata para "fazer backup" da variável $ IFS?

19

Eu sempre estou realmente hesitante em mexer com $IFSisso, porque está derrotando um global.

Mas muitas vezes torna o carregamento de seqüências de caracteres em uma matriz do bash agradável e conciso, e para scripts do bash, a concisão é difícil de encontrar.

Então, acho que seria melhor do que nada se eu tentar "salvar" o conteúdo inicial de $IFSoutra variável e depois restaurá-lo imediatamente depois que terminar de usar $IFSalgo.

Isso é prático? Ou é essencialmente inútil e eu deveria IFSvoltar diretamente ao que precisa para seus usos subseqüentes?

Steven Lu
fonte
Por que não seria prático?
Bratchley
Porque desarmar o IFS faria o trabalho bem.
llua 22/02
11
Para aqueles que dizem que a desativação do IFS funcionará bem, lembre-se de que é situacional: stackoverflow.com/questions/39545837/… . Na minha experiência, é melhor definir o IFS manualmente como o padrão para o seu interpretador de shell, a saber, $' \t\n'se você estiver usando o bash. unset $IFSsimplesmente nem sempre restaura o que você espera ser o padrão.
Darrel Holt

Respostas:

9

Você pode salvar e atribuir ao IFS conforme necessário. Não há nada errado em fazer isso. Não é incomum salvar seu valor para restauração após uma modificação temporária e rápida, como o exemplo de atribuição de matriz.

Como @llua menciona em seu comentário à sua pergunta, simplesmente desabilitar o IFS restaurará o comportamento padrão, equivalente a atribuir uma nova linha de aba de espaço.

Vale a pena considerar como pode ser mais problemático não definir / desabilitar explicitamente o IFS do que fazê-lo.

Na edição POSIX 2013, 2.5.3 Shell Variables :

As implementações podem ignorar o valor do IFS no ambiente ou a ausência do IFS no ambiente no momento em que o shell é chamado; nesse caso, o shell deve definir o IFS para <espaço> <tab> <novaline> quando é chamado .

Um shell invocado compatível com POSIX pode ou não herdar o IFS de seu ambiente. A partir disso, segue-se:

  • Um script portátil não pode herdar o IFS de maneira confiável por meio do ambiente.
  • Um script que pretende usar apenas o comportamento de divisão padrão (ou ingressar, no caso de "$*"), mas que pode ser executado em um shell que inicializa o IFS a partir do ambiente, deve explicitamente definir / desabilitar o IFS para se defender contra invasões ambientais.

NB É importante entender que, para esta discussão, a palavra "invocado" tem um significado particular. Um shell é chamado apenas quando é chamado explicitamente usando seu nome (incluindo um #!/path/to/shellshebang). Um subshell - como pode ser criado por $(...)ou cmd1 || cmd2 &- não é um shell invocado e seu IFS (junto com a maior parte de seu ambiente de execução) é idêntico ao do pai. Um shell invocado define o valor de $para seu pid, enquanto os subshells o herdam.


Isso não é meramente uma disquisição pedante; existe divergência real nessa área. Aqui está um breve script que testa o cenário usando vários shells diferentes. Ele exporta um IFS modificado (definido como :) para um shell chamado que, em seguida, imprime seu IFS padrão.

$ 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

O IFS geralmente não é marcado para exportação, mas, se assim for, observe como o bash, ksh93 e mksh ignoram o ambiente IFS=:, enquanto o dash e o busybox o honram.

$ 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

Algumas informações da versão:

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

Embora bash, ksh93 e mksh não inicializem o IFS do ambiente, eles reexportam o IFS modificado.

Se, por qualquer motivo, você precisar passar o IFS de maneira portável pelo ambiente, não poderá fazê-lo usando o próprio IFS; você precisará atribuir o valor a uma variável diferente e marcar essa variável para exportação. As crianças precisarão atribuir explicitamente esse valor ao seu IFS.

IO descalço
fonte
Entendo, portanto, se posso parafrasear, é indiscutivelmente mais portátil especificar explicitamente o IFSvalor na maioria das situações em que ele deve ser usado e, portanto, muitas vezes não é terrivelmente produtivo tentar "preservar" seu valor original.
Steven Lu
11
O principal problema é que, se seu script usa o IFS, ele deve definir / desabilitar explicitamente o IFS para garantir que seu valor seja o que você deseja que seja. Normalmente, o comportamento do seu script depende do IFS se houver expansões de parâmetros readsem aspas , substituições de comandos sem aspas , expansões aritméticas sem aspas , s ou referências com aspas duplas $*. Essa lista está no topo da minha cabeça, por isso pode não ser abrangente (especialmente quando se considera as extensões POSIX dos shells modernos).
Barefoot IO
10

Em geral, é uma boa prática retornar as condições ao padrão.

No entanto, neste caso, nem tanto.

Por quê?:

Além disso, o armazenamento do valor do IFS tem um problema.
Se o IFS original não tiver sido IFS="$OldIFS"definido , o código definirá o IFS para "", não o desmarcado.

Para realmente manter o valor do IFS (mesmo que não esteja definido), use o seguinte:

${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.
Comunidade
fonte
O IFS não pode realmente ser desabilitado. Se você desmarcá-lo, o shell o reverterá para o valor padrão. Então você realmente não precisa verificar isso ao salvá-lo.
filbranden
Cuidado, pois bash, unset IFSfalha ao desconfigurar o IFS se ele tiver sido declarado local em um contexto pai (contexto de função) e não no contexto atual.
Stéphane Chazelas
5

Você tem razão em hesitar em derrotar um global. Não tema, é possível escrever um código de trabalho limpo, sem nunca modificar o global real IFS, ou executar uma dança de salvar / restaurar complicada e propensa a erros.

Você pode:

  • configure o IFS para uma única chamada:

    IFS=value command_or_function

    ou

  • defina o IFS dentro de um subshell:

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

Exemplos

  • Para obter uma string delimitada por vírgula de uma matriz:

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

    Nota: Isso -serve apenas para proteger uma matriz vazia set -u, fornecendo um valor padrão quando desativado (esse valor é a sequência vazia neste caso) .

    A IFSmodificação é aplicável apenas dentro do subshell gerado pela $() substituição do comando . Isso ocorre porque os subshells têm cópias das variáveis ​​do shell de chamada e, portanto, podem ler seus valores, mas quaisquer modificações realizadas pelo subshell afetam apenas a cópia do subshell e não a variável pai.

    Você também pode estar pensando: por que não pular o subshell e apenas fazer o seguinte:

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

    Não há chamada de comando aqui e, em vez disso, esta linha é interpretada como duas atribuições de variáveis ​​subsequentes independentes, como se fossem:

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

    Por fim, vamos explicar por que essa variante não funcionará:

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

    O echocomando será realmente chamado com sua IFSvariável configurada como ,, mas echonão se importa ou usa IFS. A mágica de expandir "${array[*]}"para uma string é feita pelo próprio (sub) shell antes echomesmo de ser invocada.

  • Para ler em um arquivo inteiro (que não contém NULLbytes) em uma única variável chamada VAR:

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

    Nota: IFS=é o mesmo que IFS=""e IFS='', todos os quais configuram o IFS para a cadeia vazia, o que é muito diferente de unset IFS: se IFSnão estiver definido, o comportamento de todas as funcionalidades do bash usadas internamente IFSé exatamente o mesmo que se IFStivesse o valor padrão de $' \t\n'.

    Definir IFSa sequência vazia garante que os espaços em branco à esquerda e à direita sejam preservados.

    O -d ''or -d ""tell read apenas interrompe sua chamada atual em um NULLbyte, em vez da nova linha usual.

  • Para dividir $PATHseus :delimitadores:

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

    Este exemplo é puramente ilustrativo. No caso geral em que você está dividindo ao longo de um delimitador, é possível que os campos individuais contenham (uma versão de escape) esse delimitador. Pense em tentar ler uma linha de um .csvarquivo cujas colunas possam conter vírgulas (escapadas ou citadas de alguma forma). O snippet acima não funcionará como planejado para esses casos.

    Dito isto, é improvável que você encontre esses :caminhos de contenção $PATH. Embora os nomes de caminho do UNIX / Linux tenham permissão para conter a :, parece que o bash não seria capaz de lidar com esses caminhos de qualquer maneira, se você tentar adicioná-los ao seu $PATHe armazenar arquivos executáveis ​​neles, pois não há código para analisar dois pontos escapados / citados : código fonte do bash 4.4 .

    Por fim, observe que o trecho anexa uma nova linha à direita no último elemento da matriz resultante (como chamado por @ StéphaneChazelas nos comentários agora excluídos) e que, se a entrada for a string vazia, a saída será um elemento único array, onde o elemento consistirá em uma nova linha ( $'\n').

Motivação

A old_IFS="${IFS}"; command; IFS="${old_IFS}"abordagem básica que toca o global IFSfuncionará conforme o esperado para os scripts mais simples. No entanto, assim que você adiciona alguma complexidade, ela pode se separar facilmente e causar problemas sutis:

  • Se commandé uma função bash que também modifica a global IFS(diretamente ou, oculta da vista, dentro de outra função que ela chama), e, ao fazer isso por engano, usa a mesma old_IFSvariável global para salvar / restaurar, você recebe um erro.
  • Como apontado neste comentário do @Gilles , se o estado original de IFSfoi desabilitado, o ingênuo salvar e restaurar não funcionará e até resultará em falhas definitivas se a opção de shell comumente usada set -u( mis) set -o nounsetestá em vigor.
  • É possível que algum código shell seja executado de forma assíncrona no fluxo de execução principal, como com manipuladores de sinal (consulte help trap). Se esse código também modifica o global IFSou assume que ele possui um valor específico, você pode obter erros sutis.

Você pode criar uma seqüência mais robusta de salvar / restaurar (como a proposta nesta outra resposta para evitar alguns ou todos esses problemas. No entanto, você precisará repetir esse pedaço de código clichê barulhento sempre que precisar temporariamente de um costume IFS. reduz a legibilidade e a manutenção do código.

Considerações adicionais para scripts do tipo biblioteca

IFSé especialmente uma preocupação para autores de bibliotecas de funções de shell que precisam garantir que seu código funcione de maneira robusta, independentemente do estado global ( IFS, opções de shell, ...) imposto por seus invocadores e também sem perturbar esse estado (os invocadores podem confiar para permanecer sempre estático).

Ao escrever o código da biblioteca, você não pode confiar em IFSter qualquer valor específico (nem mesmo o padrão) ou mesmo estar definido. Em vez disso, você precisa definir explicitamente IFSpara qualquer snippet cujo comportamento depende IFS.

Se IFSfor explicitamente definido como o valor necessário (mesmo que esse seja o padrão) em cada linha de código em que o valor importa usando qualquer um dos dois mecanismos descritos nesta resposta que seja apropriado para localizar o efeito, o código será ambos independente do estado global e evita estragar tudo. Essa abordagem tem o benefício adicional de torná-lo muito explícito para uma pessoa que está lendo o script que IFSimporta exatamente para esse comando / expansão a um custo textual mínimo (comparado até mesmo ao salvar / restaurar mais básico).

Qual código é afetado de IFSqualquer maneira?

Felizmente, não existem muitos cenários onde isso IFSimporta (supondo que você sempre cite suas expansões ):

  • "$*"e "${array[*]}"expansões
  • invocações do readdirecionamento interno para várias variáveis ​​( read VAR1 VAR2 VAR3) ou uma variável de matriz ( read -a ARRAY_VAR_NAME)
  • invocações de readsegmentar uma única variável quando se trata de caracteres em branco à esquerda / à esquerda ou em branco que aparecem IFS.
  • divisão de palavras (como para expansões não citadas, que você pode evitar como uma praga )
  • alguns outros cenários menos comuns (Veja: IFS @ Greg's Wiki )
sls
fonte
Não posso dizer que entendi os delimitadores To split $ PATH ao longo dos seus: assumindo que nenhum dos componentes contém uma sentença : eles mesmos . Como os componentes podem conter :quando :é o delimitador?
Stéphane Chazelas
@ StéphaneChazelas Bem, :é um caractere válido para usar em um nome de arquivo na maioria dos sistemas de arquivos UNIX / Linux, portanto, é perfeitamente possível ter um diretório com um nome que contenha :. Talvez algumas conchas têm uma disposição para escapar :no caminho usando algo parecido \:, e então você iria ver colunas aparecendo que não são delimitadores de reais (Parece que o bash não permite tais escapar. A função de baixo nível usado quando iteração sobre $PATHapenas pesquisas para :em uma sequência C: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
sls 31/07
Revisei a resposta para, esperançosamente, tornar o $PATHexemplo de divisão :mais claro.
SLS
11
Bem-vindo ao SO! Obrigado por uma resposta tão aprofundada :) #
9195 Lu Lu Steven
1

Isso é prático? Ou é essencialmente inútil e devo apenas definir o IFS diretamente para o que for necessário para seus usos subseqüentes?

Por que arriscar um erro de digitação no IFS $' \t\n'quando tudo o que você precisa fazer é

OIFS=$IFS
do_your_thing
IFS=$OIFS

Como alternativa, você pode chamar um subshell se não precisar de nenhuma variável definida / modificada dentro de:

( IFS=:; do_your_thing; )
arielCo
fonte
Isso é perigoso porque não funciona se IFSfoi inicialmente desativado.
Gilles 'SO- stop be evil'