Por que `while IFS = read` é usado com tanta frequência, em vez de` IFS =; enquanto lê ...?


81

Parece que a prática normal colocaria a configuração do IFS fora do loop while, a fim de não repeti-la para cada iteração ... Esse é apenas um estilo habitual de "macaco vê, macaco faz", como foi para esse macaco até Eu li, homem leu , ou estou perdendo alguma armadilha sutil (ou descaradamente óbvia) aqui?

Respostas:


82

A armadilha é que

IFS=; while read..

define o IFSambiente shell inteiro fora do loop, enquanto

while IFS= read

redefine apenas para a readchamada (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

considerando que depois

IFS=; read xxx; ... done

as IFS estadias redefinidas: agora echo "blabalbla $IFS ooooooo"imprime

blabalbla  ooooooo

Então, se você usar a segunda forma, você tem que lembrar para repor: IFS=$' \t\n'.


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


Ok, parece que uma "armadilha" em potencial é negligenciar a redefinição do IFS externo ... Mas eu me pergunto se também há algo mais em andamento ... Estou testando as coisas aqui, com bastante febre, e tenho observe que a configuração do IFS na lista de comandos do while se comporta de maneira diferente, dependendo de ser seguido por dois pontos. Ainda não entendo esse comportamento, e agora me pergunto se há alguma consideração especial envolvida nesse nível ... por exemplo. while IFS=X readnão dividir a X, mas while IFS=X; readfaz ...
Peter.O

(Você significou semi cólon, certo?) O segundo whilenão faz muito sentido - a condição para while fins naquele ponto e vírgula, então não há nenhum laço real ... readse torna apenas o primeiro comando dentro do loop de um elemento ... ou não ? E o doentão ..?
rozcietrzewiacz

1
Não, espere - você está certo, você pode ter vários comandos na whilecondição (antes do).
rozcietrzewiacz

Ah .. definitivamente, você pode tê-los ... como você percebeu ... mas eles parecem não gostar do ponto-e-vírgula ... (e o loop continuará repetindo ad infinitum até que o último comando retorne um não -código de saída zero) ... Agora estou me perguntando se a armadilha está em um setor completamente diferente; o de entender como a lista de comandos do while funciona, por exemplo. por que faz IFS=o trabalho, mas IFS=Xnão ... (ou talvez eu tenha uma overdose sobre isso por um tempo .. coffee break necessário :)
Peter.O

1
$ rozcietrzewiacz .. Opa ... Eu não tinha notado sua atualização, quando mudei minha atualização (como mencionado no comentário anterior) .. Parece interessante e está começando a fazer sentido ... mas mesmo por uma noite- pássaro como eu, é extremamente tarde ... (Acabei de ouvir os pássaros da manhã:) ... Dito isto, eu me reuni um pouco e li seus exemplos ... Acho que entendi, na verdade eu ' tenho certeza que você conseguiu, mas eu preciso dormir :) ... Isso é quase um Eureka! momento ... graças
Peter.O

45

Vejamos um exemplo, com algum texto de entrada cuidadosamente criado:

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 read(mas usando printf '%s\n' "$text"para imprimir com cuidado, $textsem nenhum risco de expansão). (Abaixo, $ ‌está o prompt do shell.)

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

readcomeu as barras invertidas: a barra invertida-nova faz com que a nova linha seja ignorada e a barra invertida-qualquer coisa ignora a 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]

É melhor, temos duas linhas como esperado. As duas linhas quase contêm o conteúdo desejado: o espaço duplo entre helloe worldfoi retido, porque está dentro da linevariável. Por outro lado, o espaço inicial foi consumido. Isso readocorre porque lê quantas palavras você passar pelas variáveis, exceto que a última variável contém o restante da linha - mas ainda começa com a primeira palavra, ou seja, os espaços iniciais são descartados.

Portanto, para ler cada linha literalmente, precisamos garantir que não haja divisão de palavras . Fazemos isso definindo a IFSvariável 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 configuramos IFS especificamente para a duração do readbuilt-in . Os IFS= read -r lineconjuntos do ambiente variável IFS(para um valor vazio) especificamente para a execução de read. Essa é uma instância da sintaxe geral de comando simples : uma sequência (possivelmente vazia) de atribuições de variáveis ​​seguida por um nome de comando e seus argumentos (também é possível ativar redirecionamentos a qualquer momento). Como readé um built-in, a variável nunca acaba realmente no ambiente de um processo externo; no entanto, o valor de $IFSé o que estamos atribuindo lá enquanto readestiver executando is. Observe que readnão é um built-in especial ; portanto, a atribuição dura apenas por sua duração.

Portanto, tomamos o cuidado de não alterar o valor de IFSoutras instruções que possam depender dele. Esse código funcionará independentemente do que o código circundante tenha definido IFSinicialmente e não causará nenhum problema se o código dentro do loop depender IFS.

Contraste com esse trecho 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 $PATHnão seria dividido $PATHem componentes separados por dois pontos. Se o código fosse IFS=; while read …, seria ainda mais óbvio que IFSnão está definido :no corpo do loop.

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

¹ E, se readfor interrompido por um sinal interceptado, possivelmente enquanto o interceptador estiver em execução - isso não é especificado pelo POSIX e depende, na prática, do shell.


4
Obrigado Gilles .. uma visita guiada muito agradável .. (você quis dizer 'set -f'?) .... Agora, para o leitor, para reafirmar o que já foi dito, eu gostaria de enfatizar a questão que eu olhando para o lado errado. Primeiro e acima de tudo, o fato de que o construto while IFS= read(sem ponto e vírgula depois =) não é uma forma especial de whileou de IFSou de read.. O construto é genérico: ie. anyvar=anyvalue anycommand. A falta de ;configuração posterior anyvartorna o escopo de anyvar local como anycommand.. O loop while - do / done não é 100% relacionado ao escopo local de any_var.
Peter.O

3

Além da (já clarificada) IFSDiferenças de escopo entre o while IFS='' read, IFS=''; while reade while IFS=''; readexpressões idiomáticas (per-comando vs script / wide-shell IFSescopo de variáveis), a lição para levar para casa é que você perde os líderes e espaços à direita de uma linha de entrada se a variável IFS está definido como (contém um) espaço.

Isso pode ter consequências bastante graves se os caminhos do arquivo estiverem sendo processados.

Portanto, definir a variável IFS para a sequência vazia é apenas uma má idéia, pois garante que o espaço em branco à esquerda e à direita de uma linha não seja removido.

Veja também: Bash, leia linha por linha do arquivo, com 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"
)

+1 excelente demonstração, limpeza após com o arquivo 'rm * * com * espaços *' '
amdn

0

Inspirado pela resposta de Yuzem

Se você deseja definir IFSum caractere real, funcionou para mim

iconv -f cp1252 zapni.tv.php | while IFS='#' read -d'#' line
do
  echo "$line"
done
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.