it-swarm.it

Perché `while IFS = read` viene usato così spesso, invece di` IFS =; mentre leggi..`?

Sembra che la pratica normale metterebbe l'impostazione di IFS al di fuori del ciclo while per non ripetere l'impostazione per ogni iterazione ... È solo uno stile abituale di "scimmia vedi, scimmia fa", come è stato per questa scimmia fino a quando Ho letto man read, o mi manca una trappola sottile (o palesemente ovvia) qui?

85
Peter.O

La trappola è quella

IFS=; while read..

imposta IFS per l'intero ambiente Shell al di fuori del ciclo, mentre

while IFS= read

la ridefinisce solo per l'invocazione read (tranne nella Bourne Shell). Puoi verificarlo facendo un ciclo simile

while IFS= read xxx; ... done

quindi dopo tale ciclo, echo "blabalbla $IFS ooooooo" stampe

blabalbla
 ooooooo

mentre dopo

IFS=; read xxx; ... done

il IFS rimane ridefinito: ora echo "blabalbla $IFS ooooooo" stampe

blabalbla  ooooooo

Quindi, se si utilizza il secondo modulo, è necessario ricordare di ripristinare: IFS=$' \t\n'.


La seconda parte di questa domanda è stata unita qui , quindi ho rimosso la relativa risposta da qui.

86
rozcietrzewiacz

Diamo un'occhiata a un esempio, con un testo di input accuratamente realizzato:

text=' hello  world\
foo\bar'

Sono due righe, il primo che inizia con uno spazio e termina con una barra rovesciata. Innanzitutto, diamo un'occhiata a ciò che accade senza alcuna precauzione in giro read (ma usando printf '%s\n' "$text" per stampare attentamente $text senza alcun rischio di espansione). (Sotto, $ ‌ è il prompt della shell.)

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

read ha mangiato le barre rovesciate: backslash-newline fa sì che la newline venga ignorata e backslash-qualunque cosa ignora la prima barra rovesciata. Per evitare che le barre rovesciate vengano trattate in modo speciale, utilizziamo read -r.

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

Va meglio, abbiamo due linee come previsto. Le due righe contengono quasi il contenuto desiderato: il doppio spazio tra hello e world è stato mantenuto, perché si trova all'interno della variabile line. D'altra parte, lo spazio iniziale è stato consumato. Questo perché read legge tutte le parole che passi attraverso le variabili, tranne per il fatto che l'ultima variabile contiene il resto della riga, ma inizia comunque con la prima parola, ovvero gli spazi iniziali vengono scartati.

Quindi, per leggere letteralmente ogni riga, dobbiamo assicurarci che no Word split stia succedendo. Lo facciamo impostando IFS variabile su un valore vuoto.

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

Nota come impostiamo IFS specificatamente per la durata del read incorporato . Il IFS= read -r line imposta la variabile di ambiente IFS (su un valore vuoto) specificamente per l'esecuzione di read. Questa è un'istanza della sintassi generale comando semplice : una sequenza (possibilmente vuota) di assegnazioni di variabili seguita da un nome di comando e dai suoi argomenti (inoltre, è possibile generare reindirizzamenti in qualsiasi punto). Poiché read è un built-in, la variabile non finisce mai in un ambiente di processo esterno; tuttavia il valore di $IFS è ciò che stiamo assegnando lì fintanto che read è in esecuzione¹. Nota che read non è un built-in speciale , quindi l'assegnazione dura solo per la sua durata.

Quindi stiamo facendo attenzione a non modificare il valore di IFS per altre istruzioni che potrebbero fare affidamento su di esso. Questo codice funzionerà indipendentemente da ciò che il codice circostante ha impostato inizialmente IFS e non causerà alcun problema se il codice all'interno del ciclo si basa su IFS.

In contrasto con questo frammento di codice, che cerca i file in un percorso separato da due punti. L'elenco dei nomi dei file viene letto da un file, un nome file per riga.

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 il ciclo era while IFS=; read -r name; do …, poi for dir in $PATH non dividerebbe $PATH in componenti separati da due punti. Se il codice era IFS=; while read …, sarebbe ancora più ovvio che IFS non è impostato su : nel corpo del loop.

Naturalmente, sarebbe possibile ripristinare il valore di IFS dopo aver eseguito read. Ma ciò richiederebbe la conoscenza del valore precedente, che è uno sforzo extra. IFS= read è il modo semplice (e, convenientemente, anche il modo più breve).

¹ E, se read viene interrotto da un segnale intrappolato, possibilmente mentre la trap è in esecuzione - questo non è specificato da POSIX e dipende dalla Shell in pratica.

Oltre alle (già chiarite) IFS differenze di ambito tra while IFS='' read, IFS=''; while read e while IFS=''; read idiomi (per comando vs script/ambito variabile IFS scoping variabile), la lezione da portare a casa è che perdi il comando e spazi finali di una riga di input se la variabile IFS è impostata su (contiene a) spazio.

Ciò può avere conseguenze piuttosto gravi se i percorsi dei file vengono elaborati.

Pertanto, impostare la variabile IFS sulla stringa vuota è tutt'altro che una cattiva idea poiché garantisce che lo spazio bianco iniziale e finale di una linea non venga rimosso.

Vedi anche: Bash, leggi riga per riga dal file, con 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"
)
3
jon

Ispirato da Risposta di Yuzem

Se vuoi impostare IFS su un personaggio reale, questo ha funzionato per me

iconv -f cp1252 zapni.tv.php | while IFS='#' read -d'#' line
do
  echo "$line"
done
1
Steven Penny