Porque é que 'while IFS = read' é usado com muita frequência, em vez de 'IFS =; enquanto lê .. '?

76

Parece que a prática normal colocaria a configuração do IFS fora do loop while para não repetir a configuração para cada iteração ... Isso é apenas um estilo habitual de "monkey see, monkey do", como tem sido para esse macaco até eu ler homem ler , ou estou sentindo falta de alguma armadilha sutil (ou flagrantemente óbvia) aqui?

    
por Peter.O 17.08.2011 / 07:25

4 respostas

78

A armadilha é que

IFS=; while read..

define o IFS para todo o ambiente de shell fora do loop, enquanto

while IFS= read

redefine apenas para a read invocação (exceto no shell Bourne). Você pode verificar isso fazendo um loop como

while IFS= read xxx; ... done

depois desse loop, echo "blabalbla $IFS ooooooo" imprime

blabalbla
 ooooooo

ao passo que depois

IFS=; read xxx; ... done

o IFS permanece redefinido: agora echo "blabalbla $IFS ooooooo" imprime

blabalbla  ooooooo

Portanto, se você usar o segundo formulário, precisará redefinir: IFS=$' \t\n' .

A segunda parte desta questão foi mesclada aqui , removi a resposta relacionada daqui.

    
por 17.08.2011 / 09:28
45

Vejamos um exemplo com um texto de entrada bem elaborado:

text=' hello  world\
foo\bar'

São duas linhas, a primeira começando com um espaço e terminando com uma barra invertida. Primeiro, vamos ver o que acontece sem nenhuma precaução em torno de read (mas usando printf '%s\n' "$text" para imprimir cuidadosamente $text sem qualquer risco de expansão). (Abaixo, $ ‌ é o prompt do shell.)

$ printf '%s\n' "$text" |
  while read line; do printf '%s\n' "[$line]"; done
[hello worldfoobar]

read das barras invertidas: a barra invertida-nova linha faz com que a nova linha seja ignorada e a barra invertida-nada ignora essa primeira barra invertida. Para evitar que as barras invertidas sejam tratadas especialmente, usamos read -r .

$ printf '%s\n' "$text" |
  while read -r line; do printf '%s\n' "[$line]"; done
[hello  world\]
[foo\bar]

Isso é melhor, temos duas linhas conforme o esperado. As duas linhas quase contêm o conteúdo desejado: o espaço duplo entre hello e world foi mantido, porque está dentro da variável line . Por outro lado, o espaço inicial foi comido. Isso porque read lê quantas palavras você passar variáveis, exceto que a última variável contém o resto da linha - mas ainda começa com a primeira palavra, ou seja, os espaços iniciais são descartados.

Então, para ler cada linha literalmente, precisamos ter certeza de que nenhuma divisão de palavras está acontecendo. Fazemos isso definindo a IFS variable como um valor vazio.

$ printf '%s\n' "$text" |
  while IFS= read -r line; do printf '%s\n' "[$line]"; done
[ hello  world\]
[foo\bar]

Observe como definimos IFS especificamente para a duração do read interno . O IFS= read -r line define a variável de ambiente IFS (para um valor vazio) especificamente para a execução de read . Esta é uma instância da sintaxe geral do comando simples : uma sequência (possivelmente vazia) de atribuições de variáveis seguidas por um nome de comando e seus argumentos (também, você pode lançar redirecionamentos em qualquer ponto). Como read é um built-in, a variável nunca acaba no ambiente de um processo externo; No entanto, o valor de $IFS é o que estamos atribuindo lá, desde que read esteja sendo executado¹. Tenha em atenção que read não é incorporado especial , pelo que a tarefa é válida apenas pela sua duração.

Assim, estamos tomando cuidado para não alterar o valor de IFS para outras instruções que possam depender dele. Esse código funcionará independentemente do código que tenha sido definido em IFS , e não causará nenhum problema se o código dentro do loop depender de IFS .

Compare com este snippet de código, que procura arquivos em um caminho separado por dois pontos. A lista de nomes de arquivos é lida de um arquivo, um nome de arquivo por linha.

IFS=":"; set -f
while IFS= read -r name; do
  for dir in $PATH; do
    ## At this point, "$IFS" is still ":"
    if [ -e "$dir/name" ]; then echo "$dir/$name"; fi
  done
done <filenames.txt

Se o loop fosse while IFS=; read -r name; do … , for dir in $PATH não dividiria $PATH nos componentes separados por dois pontos. Se o código fosse IFS=; while read … , seria ainda mais óbvio que IFS não esteja definido como : no corpo do loop.

Naturalmente, seria possível restaurar o valor de IFS após a execução de read . Mas isso exigiria conhecer o valor anterior, que é um esforço extra. IFS= read é o caminho simples (e, convenientemente, também o caminho mais curto).

¹ E, se read for interrompido por um sinal aprisionado, possivelmente enquanto o trap estiver sendo executado - isso não é especificado por POSIX e depende do shell na prática.

    
por 18.08.2011 / 03:23
3

Além das diferenças de escopo IFS (já esclarecidas) entre os idiomas while IFS='' read , IFS=''; while read e while IFS=''; read (por-comando vs script / todo o shell IFS variable scoping), o take-home A lição é que você perde os espaços à direita e de uma linha de entrada se a variável IFS estiver configurada para (conter um) espaço.

Isso pode ter consequências muito graves se os caminhos de arquivo estiverem sendo processados.

Portanto, configurar a variável IFS para a cadeia vazia é tudo menos uma má ideia, pois garante que os espaços em branco iniciais e finais de uma linha não sejam removidos.

Veja também: Bash, leia linha por linha do arquivo com o IFS

(
shopt -s nullglob
touch '  file with spaces   '
IFS=$' \t\n' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
IFS='' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
)
    
por 19.08.2011 / 13:08
0

Inspirado por resposta da Yuzem

Se você quiser definir IFS para um personagem real, isso funcionou para mim

iconv -f cp1252 zapni.tv.php | while IFS='#' read -d'#' line
do
  echo "$line"
done
    
por 16.06.2012 / 05:42