Noções básicas sobre "IFS = read -r line"

60

Obviamente, entendo que se pode agregar valor à variável separadora de campo interno. Por exemplo:

$ IFS=blah
$ echo "$IFS"
blah
$ 

Também entendo que read -r lineos dados serão salvos na stdinvariável denominada line:

$ read -r line <<< blah
$ echo "$line"
blah
$ 

No entanto, como um comando pode atribuir valor variável? E ele primeiro armazena dados de stdinpara variável linee depois atribui valor linea IFS?

Martin
fonte
3
Relacionados: unix.stackexchange.com/q/169716/38906
cuonglm

Respostas:

104

Algumas pessoas têm essa noção errônea que readé o comando para ler uma linha. Não é.

readpalavras de uma linha (possivelmente continuada por barra invertida), na qual as palavras são $IFSdelimitadas e a barra invertida pode ser usada para escapar dos delimitadores (ou continuar linhas).

A sintaxe genérica é:

read word1 word2... remaining_words

readlê stdin um byte de cada vez até encontrar um caractere de nova linha unescaped (ou fim-de-entrada), divide que de acordo com regras complexas e armazena o resultado dessa divisão em $word1, $word2... $remaining_words.

Por exemplo, em uma entrada como:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

e com o valor padrão de $IFS, read a b catribuiria:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

Agora, se passado apenas um argumento, isso não se torna read line. Ainda está read remaining_words. O processamento de barra invertida ainda está concluído, os caracteres de espaço em branco do IFS ainda são removidos do início e do fim.

A -ropção remove o processamento da barra invertida. Portanto, o mesmo comando acima com -ratribuiria

  • $afoo
  • $bbar\
  • $cbaz bl\ah blah\

Agora, para a parte de divisão, é importante perceber que existem duas classes de caracteres para $IFS: os caracteres de espaço em branco do IFS (ou seja, espaço e tab (e nova linha, embora aqui isso não importe, a menos que você use -d), o que também acontece estar no valor padrão de $IFS) e os outros. O tratamento para essas duas classes de personagens é diferente.

Com IFS=:( :não sendo um espaço em branco IFS), uma entrada como :foo::bar::seria dividido em "", "foo", "", bare ""(e um extra ""com algumas implementações embora isso não importa, exceto read -a). Enquanto se substituirmos isso :por espaço, a divisão será feita em somente fooe bar. Os principais e os finais são ignorados e as sequências são tratadas como uma. Existem regras adicionais quando caracteres em branco e não em branco são combinados $IFS. Algumas implementações podem adicionar / remover o tratamento especial dobrando os caracteres no IFS ( IFS=::ou IFS=' ').

Portanto, aqui, se não queremos que os caracteres de espaço em branco à esquerda e à esquerda sejam removidos, precisamos remover esses caracteres de espaço em branco do IFS do IFS.

Mesmo com caracteres IFS que não sejam espaços em branco, se a linha de entrada contiver um (e apenas um) desses caracteres e for o último caractere na linha (como IFS=: read -r wordem uma entrada como foo:) com shells POSIX (não, zshnem em algumas pdkshversões), essa entrada é considerado como uma foopalavra, porque nessas conchas, os caracteres $IFSsão considerados terminadores ; portanto word, conterão foo, não foo:.

Portanto, a maneira canônica de ler uma linha de entrada com o readbuiltin é:

IFS= read -r line

(observe que, na maioria das readimplementações, isso funciona apenas para linhas de texto, pois o caractere NUL não é suportado, exceto em zsh).

O uso da var=value cmdsintaxe garante que IFSsomente seja definido de forma diferente pela duração desse cmdcomando.

Nota do histórico

O readbuiltin foi introduzido pelo shell Bourne e já devia ler palavras , não linhas. Existem algumas diferenças importantes com os shells POSIX modernos.

O shell Bourne readnão suportava uma -ropção (que foi introduzida pelo shell Korn), então não há como desativar o processamento de barra invertida além de pré-processar a entrada com algo parecido sed 's/\\/&&/g'.

O shell Bourne não tinha a noção de duas classes de caracteres (que novamente foram introduzidas pelo ksh). No shell Bourne, todos os caracteres passam pelo mesmo tratamento que os caracteres de espaço em branco do IFS no ksh, ou seja, IFS=: read a b cem uma entrada que foo::barseria atribuída bara $b, e não na sequência vazia.

No shell Bourne, com:

var=value cmd

Se cmdfor um built-in (como readé), varpermanece definido como valueapós a cmdconclusão. Isso é particularmente crítico $IFSporque, no shell Bourne, $IFSé usado para dividir tudo, não apenas as expansões. Além disso, se você remover o caractere de espaço do $IFSshell Bourne, "$@"não funcionará mais.

No shell Bourne, o redirecionamento de um comando composto faz com que ele seja executado em um subshell (nas versões anteriores, até coisas como read var < fileou exec 3< file; read var <&3não funcionavam), portanto, era raro no shell Bourne usar readpara qualquer coisa, exceto a entrada do usuário no terminal (onde esse tratamento de continuação de linha fazia sentido)

Alguns Unices (como HP / UX, também há um util-linux) ainda têm um linecomando para ler uma linha de entrada (que costumava ser um comando UNIX padrão até a Especificação Única do UNIX versão 2 ).

É basicamente o mesmo, head -n 1exceto que ele lê um byte de cada vez para garantir que não leia mais de uma linha. Nesses sistemas, você pode fazer:

line=`line`

Obviamente, isso significa gerar um novo processo, executar um comando e ler sua saída através de um pipe, muito menos eficiente que o ksh IFS= read -r line, mas ainda muito mais intuitivo.

Stéphane Chazelas
fonte
3
+1 Obrigado por algumas informações úteis sobre os diferentes tratamentos no espaço / tab vs "outros" no IFS no bash ... Eu sabia que eles foram tratados de maneira diferente, mas essa explicação simplifica muito tudo. (E o discernimento entre bash (e outras conchas POSIX) e os regulares shdiferenças é útil também para escrever scripts portáteis!)
Olivier Dulac
Pelo menos para bash-4.4.19, while read -r; do echo "'$REPLY'"; donefunciona como while IFS= read -r line; do echo "'$line'"; done.
X-yuri 28/05
O seguinte: "... essa noção errônea que ler é o comando para ler uma linha ..." me leva a pensar que se usar reada leitura de uma linha é errôneo, deve haver algo mais. O que poderia ser essa noção não errônea? Ou essa primeira afirmação é tecnicamente correta, mas, na verdade, a noção não errônea é: "read é o comando para ler palavras de uma linha. Por ser tão poderosa, você pode usá-lo para ler linhas de um arquivo fazendo o seguinte: IFS= read -r line"
Mike S
8

A teoria

Existem dois conceitos em jogo aqui:

  • IFSé o separador de campos de entrada, o que significa que a sequência de caracteres lida será dividida com base nos caracteres em IFS. Em uma linha de comando, IFSnormalmente existem caracteres de espaço em branco, é por isso que a linha de comando é dividida em espaços.
  • Fazer algo como VAR=value commandsignifica "modificar o ambiente de comando para que VARtenha o valor value". Basicamente, o comando commandverá VARcomo tendo o valor value, mas qualquer comando executado depois disso continuará VARcom o valor anterior. Em outras palavras, essa variável será modificada apenas para essa instrução.

Nesse caso

Portanto, ao fazer IFS= read -r line, o que você está fazendo é definir IFSuma string vazia (nenhum caractere será usado para dividir; portanto, nenhuma divisão ocorrerá), para que readleia a linha inteira e a veja como uma palavra que será atribuída à linevariável. As alterações IFSafetam apenas essa instrução, para que os seguintes comandos não sejam afetados pela alteração.

Como uma nota rodapé

Embora o comando esteja correto e funcione conforme o planejado, a configuração IFSnesse caso não é 1, pode não ser necessária. Conforme escrito na bashpágina de manual na readseção incorporada:

Uma linha é lida a partir da entrada [...] padrão e a primeira palavra é atribuída ao primeiro nome, a segunda palavra ao segundo nome e assim por diante, com as palavras restantes e seus separadores intermediários atribuídos ao sobrenome . Se houver menos palavras lidas no fluxo de entrada do que nomes, os nomes restantes receberão valores vazios. Os caracteres IFSsão usados ​​para dividir a linha em palavras. [...]

Como você tem apenas a linevariável, todas as palavras serão atribuídas a ela de qualquer maneira, portanto, se você não precisar de nenhum dos caracteres de espaço em branco precedente e à direita 1, basta escrever read -r linee concluir com ela.

[1] Apenas como um exemplo de como um valor unsetou padrão $IFSfará com readque o espaço em branco do IFS seja inicial / final , você pode tentar:

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

Execute-o e você verá que os caracteres anteriores e finais não sobreviverão se IFSnão estiverem definidos. Além disso, algumas coisas estranhas poderiam acontecer se $IFSfosse modificado em algum lugar anteriormente no script.

user43791
fonte
5

Você deve ler essa declaração em duas partes, a primeira apaga o valor da variável IFS, ou seja, é equivalente à mais legível IFS="", a segunda está lendo a linevariável a partir de stdin read -r line,.

O que é específico nessa sintaxe é que a afetação do IFS é transitória e válida apenas para o readcomando.

A menos que esteja faltando alguma coisa, nesse caso específico, a limpeza IFSnão tem efeito, pois, como IFSestiver definido, a linha inteira será lida na linevariável. Haveria uma mudança de comportamento apenas no caso de mais de uma variável ter sido passada como parâmetro para a readinstrução.

Editar:

O -rexiste para permitir que a entrada que termina com \a não ser processada especialmente, isto é, para a barra invertida para ser incluído na linevariável e não como um carácter de continuação para permitir a entrada multi-linha.

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

A limpeza do IFS tem o efeito colateral de impedir a leitura para aparar possíveis caracteres iniciais ou finais de espaço ou tabulação, por exemplo:

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

Obrigado a rici por apontar essa diferença.

jlliagre
fonte
O que você está perdendo é que, se o IFS não for alterado, read -r linecortará os espaços em branco à esquerda e à direita antes de atribuir a entrada à linevariável.
rici 12/06
@rici Eu suspeitava de algo assim, mas apenas verifiquei caracteres IFS entre palavras, não caracteres iniciais / finais. Obrigado por apontar esse fato!
Jlliagre
limpar o IFS também impedirá a atribuição de múltiplas variáveis ​​(efeito colateral). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"mostrará-aa bb--
kyodev 13/04/19