Entendendo o IFS

71

Os seguintes tópicos neste site e o StackOverflow foram úteis para entender como IFSfunciona:

Mas ainda tenho algumas perguntas curtas. Decidi perguntar a eles no mesmo post, pois acho que pode ajudar melhores futuros leitores:

Q1 IFSé normalmente discutido no contexto de "divisão de campo". A divisão de campos é igual à divisão de palavras ?

P2: A especificação POSIX diz :

Se o valor do IFS for nulo, nenhuma divisão de campo deve ser realizada.

A configuração é IFS=igual IFSa nula? É isso que significa definir também um empty string?

Q3: na especificação POSIX, li o seguinte:

Se o IFS não estiver definido, o shell deve se comportar como se o valor do IFS fosse <space>, <tab> and <newline>

Digamos que eu queira restaurar o valor padrão de IFS. Como faço isso? (mais especificamente, como me refiro <tab>e <newline>?)

Q4: Finalmente, como esse código:

while IFS= read -r line
do    
    echo $line
done < /path_to_text_file

se comportar se mudarmos a primeira linha para

while read -r line # Use the default IFS value

ou para:

while IFS=' ' read -r line
Amelio Vazquez-Reina
fonte

Respostas:

28
  1. Sim, eles são iguais.
  2. Sim.
  3. No bash e em conchas semelhantes, você poderia fazer algo parecido IFS=$' \t\n'. Caso contrário, você pode inserir os códigos de controle literais usando [space] CTRL+V [tab] CTRL+V [enter]. No entanto, se você estiver planejando fazer isso, é melhor usar outra variável para armazenar temporariamente o IFSvalor antigo e depois restaurá-lo (ou substituí-lo temporariamente por um comando usando a var=foo commandsintaxe).
    • O primeiro trecho de código colocará toda a linha lida, literalmente, em $line, pois não há separadores de campo para os quais a divisão de palavras seja executada. No entanto, lembre-se de que, como muitos shells usam cstrings para armazenar strings, a primeira instância de um NUL ainda pode causar a aparência de seu término prematuramente.
    • O segundo trecho de código pode não colocar uma cópia exata da entrada $line. Por exemplo, se houver vários separadores de campo consecutivos, eles serão transformados em uma única instância do primeiro elemento. Isso geralmente é reconhecido como perda de espaço em branco ao redor.
    • O terceiro trecho de código fará o mesmo que o segundo, exceto que ele será dividido apenas em um espaço (não no espaço, guia ou nova linha usual).
Chris Down
fonte
3
A resposta para o Q2 está errada: um vazio IFSe um não configurado IFSsão muito diferentes. A resposta para o quarto trimestre está parcialmente errada: os separadores internos não são tocados aqui, apenas os iniciais e os finais.
Gilles 'SO- stop be evil'
3
@ Gilles: No segundo trimestre, nenhuma das três denominações mencionadas se refere a uma definição IFS, todas elas significam IFS=.
Stéphane Gimenez
@ Gilles No segundo trimestre, eu nunca disse que eles eram iguais. E separadores internos são tocados, como mostrado aqui: IFS=' ' ; foo=( bar baz qux ) ; echo "${#foo[@]}". (Er, o quê? Deve haver vários delimitadores de espaço, o mecanismo SO continua removendo-os).
Chris Baixo
2
@ StéphaneGimenez, Chris: Ah, certo, desculpe pelo Q2, eu interpretei mal a pergunta. Para o quarto trimestre, estamos falando sobre read; a última variável pega tudo o que resta, exceto o último separador e deixa os separadores internos para dentro.
Gilles 'SO- stop be evil'
11
Gilles está parcialmente correto sobre os espaços que não estão sendo removidos pela leitura. Leia minha resposta para obter detalhes.
22

Q1: sim. "Divisão de campo" e "divisão de palavras" são dois termos para o mesmo conceito.

P2: Sim. Se não estiver definido IFS(ou seja, depois unset IFS), é equivalente a IFSser definido como $' \t\n'(um espaço, uma guia e uma nova linha). Se IFSestiver definido como um valor vazio (é o que “nulo” significa aqui) (ou seja, depois IFS=ou IFS=''ou IFS=""), nenhuma divisão de campo é executada (e $*, que normalmente usa o primeiro caractere $IFS, usa um caractere de espaço).

T3: se você deseja ter o IFScomportamento padrão , você pode usar unset IFS. Se você desejar definir IFSexplicitamente esse valor padrão, poderá colocar os caracteres literais espaço, guia, nova linha entre aspas simples. No ksh93, bash ou zsh, você pode usar IFS=$' \t\n'. Portably, se você quiser evitar ter um caractere de tabulação literal no arquivo de origem, poderá usar

IFS=" $(echo t | tr t \\t)
"

Q4: com IFSdefinido como um valor vazio, read -r linedefine linecomo a linha inteira, exceto sua nova linha final. Com IFS=" ", os espaços no início e no final da linha são cortados. Com o valor padrão de IFS, tabulações e espaços são cortados.

Gilles 'SO- parar de ser mau'
fonte
2
Q2 está parcialmente errado. Se o IFS estiver vazio, "$ *" será associado sem separadores. (pois $@, existem algumas variações entre shells em contextos não relacionados à lista IFS=; var=$@). Deve-se observar que, quando o IFS está vazio, nenhuma divisão de palavras é realizada, mas $ var ainda se expande para nenhum argumento, em vez de um argumento vazio quando $ var está vazio, e globbing ainda se aplica, portanto, você ainda precisa citar variáveis ​​(mesmo se desativar globbing)
Stéphane Chazelas
13

Q1 Divisão de campo.

A divisão de campos é igual à divisão de palavras?

Sim, ambos apontam para a mesma idéia.

P2: quando o IFS é nulo ?

A configuração é IFS=''igual a null, o mesmo que uma string vazia também?

Sim, todos os três significam o mesmo: Nenhuma divisão de campo / palavra deve ser realizada. Além disso, isso afeta os campos de impressão (como em echo "$*") todos os campos serão concatenados, sem espaço.

Q3: (parte a) Desativar IFS.

Na especificação POSIX, li o seguinte :

Se o IFS não estiver definido, o shell deverá se comportar como se o valor do IFS fosse <espaço> <guia> <nova linha> .

O que é exatamente equivalente a:

Com um unset IFS, o shell deve se comportar como se o IFS fosse o padrão.

Isso significa que a 'Divisão do campo' será exatamente a mesma com um valor padrão do IFS ou não definido.
Isso NÃO significa que o IFS funcionará da mesma maneira em todas as condições. Sendo mais específico, a execução OldIFS=$IFSdefinirá o var OldIFScomo nulo , não o padrão. E tentar definir o IFS de volta, como esse, IFS=OldIFSdefinirá o IFS para nulo, não o manterá desativado como antes. Cuidado !!.

Q3: (parte b) Restaurar o IFS.

Como eu poderia restaurar o valor do IFS para o padrão. Digamos que eu queira restaurar o valor padrão do IFS. Como faço isso? (mais especificamente, como me refiro a <tab> e <newline> ?)

Para zsh, ksh e bash (AFAIK), o IFS pode ser definido como o valor padrão como:

IFS=$' \t\n'        # works with zsh, ksh, bash.

Feito, você não precisa ler mais nada.

Mas se você precisar redefinir o IFS para sh, ele poderá se tornar complexo.

Vamos dar uma olhada do mais fácil ao completo, sem inconvenientes (exceto a complexidade).

1.- Desative o IFS.

Poderíamos apenas unset IFS(Leia Q3 parte a, acima.).

2.- Trocar caracteres.

Como solução alternativa, a troca do valor de tab e newline facilita a configuração do valor do IFS e, em seguida, funciona de maneira equivalente.

Defina IFS para <espaço> <nova linha> <guia> :

sh -c 'IFS=$(echo " \n\t"); printf "%s" "$IFS"|xxd'      # Works.

3.- Um simples? solução:

Se houver scripts filhos que precisam do IFS configurado corretamente, você sempre poderá escrever manualmente:

IFS = '   
"

Onde a sequência digitada manualmente foi:, IFS='spacetabnewline'sequência que foi realmente digitada corretamente acima (se você precisar confirmar, edite esta resposta). Mas uma cópia / pasta do seu navegador será interrompida porque o navegador comprime / oculta o espaço em branco. Torna difícil compartilhar o código conforme escrito acima.

4.- Solução completa.

Escrever código que pode ser copiado com segurança geralmente envolve escapes imprimíveis e inequívocos.

Precisamos de algum código que "produz" o valor esperado. Mas, mesmo que conceitualmente correto, esse código NÃO definirá um final \n:

sh -c 'IFS=$(echo " \t\n"); printf "%s" "$IFS"|xxd'      # wrong.

Isso acontece porque, na maioria dos shells, todas as novas linhas finais $(...)ou `...`substituições de comandos são removidas na expansão.

Precisamos usar um truque para sh:

sh -c 'IFS="$(printf " \t\nx")"; IFS="${IFS%x}"; printf "$IFS"|xxd'  # Correct.

Uma maneira alternativa pode ser definir o IFS como um valor de ambiente a partir do bash (por exemplo) e, em seguida, chamar sh (as versões dele que aceitam o IFS a serem definidas pelo ambiente), da seguinte forma:

env IFS=$' \t\n' sh -c 'printf "%s" "$IFS"|xxd'

Em resumo, sh torna a redefinição do IFS como padrão uma aventura bastante estranha.

Q4: no código real:

Por fim, como esse código:

while IFS= read -r line
do
    echo $line
done < /path_to_text_file

se comportar se mudarmos a primeira linha para

while read -r line # Use the default IFS value

ou para:

while IFS=' ' read -r line

Primeiro: eu não sei se o echo $line(com o var NÃO citado) existe no porpouse, ou não. Introduz um segundo nível de 'divisão de campo' que a leitura não possui. Então eu vou responder as duas. :)

Com este código (para que você possa confirmar). Você precisará do xxd útil :

#!/bin/ksh
# Correctly set IFS as described above.
defIFS="$(printf " \t\nx")"; defIFS="${defIFS%x}";
IFS="$defIFS"
printf "IFS value: "
printf "%s" "$IFS"| xxd -p

a='   bar   baz   quz   '; l="${#a}"
printf "var value          : %${l}s-" "$a" ; printf "%s\n" "$a" | xxd -p

printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x--          : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf 'Values      quoted :\n' ""  # With values quoted:
printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null    quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS default quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf '%s\n' "Values unquoted :"   # Now with values unquoted:
printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x-- unquoted : "
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null  unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS defau unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

Eu recebo:

$ ./stackexchange-Understanding-IFS.sh
IFS value: 20090a
var value          :    bar   baz   quz   -20202062617220202062617a20202071757a2020200a
IFS --x--          :    bar   baz   quz   -20202062617220202062617a20202071757a202020
Values      quoted :
IFS null    quoted :    bar   baz   quz   -20202062617220202062617a20202071757a202020
IFS default quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS unset   quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS space   quoted :       bar   baz   quz-62617220202062617a20202071757a
Values unquoted :
IFS --x-- unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS null  unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS defau unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS unset unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS space unquoted : bar, baz, quz, 6261722c62617a2c71757a2c

O primeiro valor é apenas o valor correto de IFS='spacetabnewline'

A próxima linha é todos os valores hexadecimais que a var $apossui e uma nova linha '0a' no final, conforme será atribuída a cada comando de leitura.

A próxima linha, para a qual o IFS é nulo, não executa nenhuma 'divisão de campo', mas a nova linha é removida (conforme o esperado).

As próximas três linhas, como o IFS contém um espaço, remova os espaços iniciais e defina a linha var para o saldo restante.

As últimas quatro linhas mostram o que uma variável não citada fará. Os valores serão divididos nos (vários) espaços e serão impressos como:bar,baz,qux,


fonte
4

unset IFS limpa o IFS, mesmo que se presuma que o IFS seja "\ t \ n":

$ echo "'$IFS'"
'   
'
$ IFS=""
$ echo "'$IFS'"
''
$ unset IFS
$ echo "'$IFS'"
''
$ IFS=$' \t\n'
$ echo "'$IFS'"
'   
'
$

Testado nas versões 4.2.45 e 3.2.25 do bash com o mesmo comportamento.

derekm
fonte
A questão ea documentação relacionada não falam sobre unsetde IFS, como explicado nos comentários da resposta aceito aqui.
ILMostro_7 10/11/18