Compreendendo o IFS

68

Os poucos tópicos a seguir neste site e o StackOverflow foram úteis para entender como o IFS funciona:

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

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

Q2: A especificação POSIX diz :

If the value of IFS is null, no field splitting shall be performed.

Está definindo IFS= o mesmo que definindo IFS como nulo? É isso que significa configurá-lo para um empty string também?

Q3: Na especificação POSIX, eu leio o seguinte:

If IFS is not set, the shell shall behave as if the value of IFS is <space>, <tab> and <newline>

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

Q4: Por fim, como seria esse código:

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

Comporte-se se mudarmos a primeira linha para

while read -r line # Use the default IFS value

ou para:

while IFS=' ' read -r line
    
por Amelio Vazquez-Reina 14.12.2011 / 00:43

5 respostas

26
  1. Sim, eles são iguais.
  2. Sim.
  3. No bash e em shells similares, você poderia fazer algo como IFS=$' \t\n' . Caso contrário, você poderia inserir os códigos de controle literais usando [space] CTRL+V [tab] CTRL+V [enter] . Se você estiver planejando fazer isso, no entanto, é melhor usar outra variável para armazenar temporariamente o valor IFS antigo e restaurá-lo depois (ou substituí-lo temporariamente por um comando usando a sintaxe var=foo command ).
    • O primeiro snippet de código colocará a linha inteira como lida, textualmente, em $line , pois não há separadores de campo para a divisão de palavras. No entanto, tenha em mente que, como muitos shells usam cstrings para armazenar strings, a primeira instância de um NUL ainda pode fazer com que a aparência dele seja encerrada prematuramente.
    • O segundo snippet de código não pode colocar uma cópia exata da entrada em $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.
    • O terceiro snippet de código fará o mesmo que o segundo, exceto que ele só será dividido em um espaço (não no espaço, na guia ou na nova linha).
por 14.12.2011 / 01:25
21

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

Q2: sim. Se IFS não estiver definido (ou seja, após unset IFS ), será equivalente IFS ser definido como $' \t\n' (um espaço, uma tabulação e uma nova linha). Se IFS for definido como um valor vazio (é o que significa "nulo" aqui) (ou seja, após IFS= ou IFS='' ou IFS="" ), nenhuma divisão de campo é executada (e $* , que normalmente usa o primeiro caractere de $IFS , usa um caractere de espaço).

Q3: se você quiser ter o comportamento padrão IFS , use unset IFS . Se você quiser definir IFS explicitamente para esse valor padrão, poderá colocar o espaço de caracteres literais, tab, newline entre aspas simples. No ksh93, bash ou zsh, você pode usar IFS=$' \t\n' . Portavelmente, se você quiser evitar ter um caractere de tabulação literal no seu arquivo de origem, você pode usar

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

Q4: com IFS definido como um valor vazio, read -r line define line para toda a linha, exceto sua nova linha de término. Com IFS=" " , espaços no início e no final da linha são aparados. Com o valor padrão de IFS , as guias e os espaços são reduzidos.

    
por 14.12.2011 / 13:01
12

Q1. Divisão de campo.

Is field splitting the same as word splitting ?

Sim, ambos apontam para a mesma ideia.

Q2: quando o IFS nulo ?

Is setting IFS='' the same as null, the same as an empty string too?

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

Q3: (parte a) Desactivar o IFS.

In the POSIX specification, I read the following:

If IFS is not set, the shell shall behave as if the value of IFS is <space><tab><newline>.

Qual é exatamente o equivalente a:

With an unset IFS, the shell shall behave as if IFS is default.

Isso significa que a "Divisão de campo" será exatamente igual a um valor IFS padrão ou não definida.
Isso NÃO significa que o IFS funcionará da mesma maneira em todas as condições. Sendo mais específico, a execução de OldIFS=$IFS definirá o var OldIFS para nulo , não o padrão. E tentando definir o IFS de volta, pois isso, IFS=OldIFS irá definir o IFS como nulo, não mantê-lo como antes. Cuidado!.

Q3: (parte b) Restaurar o IFS.

How could I restore the value of IFS to default. Say I want to restore the default value of IFS. How do I do that? (more specifically, how do I refer to <tab> and <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, pode se tornar complexo.

Vamos dar uma olhada do mais fácil para concluir sem inconvenientes (exceto complexidade).

1.- Desativar o IFS.

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

2.- Troque os caracteres.

Como solução alternativa, a troca do valor de tab e newline torna mais simples definir o valor do IFS e, em seguida, funciona de maneira equivalente.

Defina o IFS como < espaço > < newline > < tab > :

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

3.- Um simples? solução:

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

IFS='   
'

Onde a seqüência digitada manualmente era: IFS= ' espaço guia nova linha ' , seqüência que foi digitada corretamente acima (se você precisar confirmar, edite esta resposta). Mas uma cópia / colagem do seu navegador será interrompida porque o navegador irá apertar / ocultar o espaço em branco. Torna difícil compartilhar o código como descrito acima.

4.- Solução completa.

Para escrever código que pode ser copiado com segurança, geralmente envolve saídas imprimíveis não ambíguas.

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

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

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

Precisamos usar os truques 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 do bash (por exemplo) e, em seguida, chamar sh (as versões dele que aceitam o IFS a serem configuradas através do ambiente), como este:

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

Resumindo, sh faz com que o IFS reconfigure uma aventura bastante estranha.

Q4: no código real:

Finally, how would this code:

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

behave if we we change the first line to

while read -r line # Use the default IFS value

or to:

while IFS=' ' read -r line

Primeiro: eu não sei se o echo $line (com o var NÃO citado) está lá 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 a ambos. :)

Com este código (assim você pode 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= ' espaço guia nova linha '

Próxima linha é todos os valores hexadecimais que o var $a tem, e uma nova linha '0a' no final, como será dado 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 (como esperado).

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

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

    
por 07.08.2015 / 01:13
4

unset IFS limpa o IFS, mesmo se, a partir de então, o IFS for considerado "\ 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.

    
por 17.09.2013 / 21:48
0

Q1 Divisão

Q1. Is field splitting the same as word splitting ?

Provavelmente, mas com uma advertência.
Uma expansão de parâmetros , conforme chamada em POSIX, ksh, bash ou zsh (e outros) é sujeito a "Field splitting" como chamado em POSIX (aka: "Field spliting" em ksh, "Word Splitting" no bash, e às vezes no campo e às vezes na palavra zsh).

Eu definiria como:

O processo de dividir um parâmetro que é feito usando os caracteres IFS.

Onde "parâmetro" significa "um valor variável (conteúdo)" e o uso de caracteres IFS pode ser diferente em zsh. Há um sinalizador s:string: que faz "divisão de campo" em string em zsh.

ressalva
No entanto, existe um processo de divisão chamado "Token Recognition" conforme definido pelo Posix que divide linhas de comando em palavras (tokens) usando principalmente espaços em branco (tabulações e espaços) e algumas outras regras. Os tokens são subsequentemente (imediatamente) chamados de "palavras" e são mostrados na descrição do alias ( por exemplo):

After a token has been delimited, … , a resulting word …

Como explicado na página de manual do ksh :

Command Syntax The shell begins parsing its input by breaking it into words. Words, which are sequences of characters, are delimited by unquoted white space characters (space, tab and newline) or meta-characters (<, >, |, ;, &, ( and )).

Também explicitamente definido em bash man desta forma:

word A sequence of characters considered as a single unit by the shell. Also known as a token.

Ou this :

word A sequence of characters treated as a unit by the shell. Words may not include unquoted metacharacters.

Essa é uma "divisão de palavras" em termos leigos.

Q2 IFS nulo

Q2: Is setting IFS= the same as setting IFS to null? Is this what is meant by setting it to an empty string too?

Uma variável não definida não existe. Existe uma variável definida, mas pode estar vazia. Se esse valor de vazio for chamado de "nulo" (em oposição a "NUL" ou "0x00" ou "\ 0"), então sim, todos os três são equivalentes.

A variável está definida, mas vazia. var=var=''var="" .

Q3 não configurado IFS

Q3: In the POSIX specification, I read the following:

If IFS is not set, the shell shall behave as if the value of IFS is , and

Sim, o shell deve se comportar No sentido de que os efeitos que o IFS deve ter ainda devem ser os mesmos se um unset IFS foi executado, principalmente para comandos "divisão de palavras" e read .

Isso não é exatamente igual a acreditar que uma variável não definida atua da mesma forma que uma variável set. Em específico, se você tiver:

$ unset a
$ b=$a

A variável a não está definida, ela ainda não existe, no entanto, b é definido como null , conforme descrito na pergunta anterior. E isso também será verdade:

$ echo "\"${a-a is UN-set}\"  \"${b-b is UN-set}\""
"a is UN-set"  ""

Isso é importante no caso em que isso é feito:

$ unset IFS
$ oldIFS=$IFS

A variável oldFS agora está definida (mas o IFS não está definido), tentando restaurar o IFS fazendo:

$ IFS=$oldIFS

Terminará com um conjunto do IFS como nulo, não será desativado . Seus efeitos serão diferentes.

A única solução é garantir que oldIFS também esteja definido (ou não definido) como IFS:

$ [ "$(set | grep '^IFS=')" ] && oldIFS=$IFS || unset oldIFS;

Se IFS for não não definido, defina oldIFS como seu valor, caso contrário, desmarque-o.
Restaurar pelo mesmo procedimento (swap vars):

$ [ "$(set | grep '^oldIFS=')" ] && IFS=$oldIFS || unset IFS;

Q3 redefinir o IFS

Q3 Say I want to restore the default value of IFS. How do I do that? (more specifically, how do I refer to and ?)

O único problema real é a nova linha no final. A maneira antiga e simples de obtê-lo é:

nl='
'

Sim, uma nova linha real. Para um IFS completo de:

IFS=" $(printf \t)$nl"
eval "$(printf "s=' \t\n'")"
IFS=$' \t\n'

Q4 IFS em leitura

Q4: Finalmente, como seria esse código:

while IFS= read -r line ...

Irá ler uma linha (até um caractere de nova linha) e atribuí-la (sem a nova linha final) ao var line . Nenhuma divisão de palavras nem remoção de espaço em branco (espaço em branco à esquerda ou à esquerda) será executada.

while read -r line # Use the default IFS value

Com o IFS padrão ( \ \t\n ), o primeiro efeito é que todo o espaço em branco inicial e final da linha será recortado. Em seguida, cada (grupo de delimitadores consecutivos) será usado para dividir a linha para cada variável. Ou seja: duas variáveis precisam de um delimitador um (não inicial ou final). Cada variável adicional requer um delimitador adicional (ou grupo de delimitadores).

while IFS=' ' read -r line

Os espaços iniciais e finais (trechos de) serão removidos, cada espaço (de execução) será usado para dividir a linha em tantos lugares quanto as variáveis exigirem.

    
por 07.10.2018 / 15:32

Tags