Warum wird "while IFS = read" so oft verwendet, anstatt "IFS =; während gelesen..`?

81

Es scheint, dass die normale Praxis die Einstellung von IFS außerhalb der while-Schleife setzen würde, um sie nicht bei jeder Iteration zu wiederholen Ich lese, man liest , oder vermisse ich hier eine subtile (oder offensichtlichste) Falle?

Peter.O
quelle

Antworten:

82

Die Falle ist das

IFS=; while read..

Legt die IFSfür die gesamte Shell-Umgebung außerhalb der Schleife fest, wohingegen

while IFS= read

Definiert es nur für den readAufruf neu (außer in der Bourne-Shell). Sie können das überprüfen, indem Sie eine Schleife wie

while IFS= read xxx; ... done

Dann wird nach einer solchen Schleife echo "blabalbla $IFS ooooooo"gedruckt

blabalbla
 ooooooo

während nach

IFS=; read xxx; ... done

die IFS bleibt neu definiert: jetzt echo "blabalbla $IFS ooooooo"druckt

blabalbla  ooooooo

Also , wenn Sie das zweite Formular zu verwenden, müssen Sie zurücksetzen erinnern: IFS=$' \t\n'.


Der zweite Teil dieser Frage wurde hier zusammengeführt , daher habe ich die zugehörige Antwort hier entfernt.

rozcietrzewiacz
quelle
Okay, es scheint, dass eine mögliche "Falle" darin besteht, das Zurücksetzen des äußeren IFS zu vernachlässigen ... Aber ich frage mich, ob noch etwas anderes im Gange ist ... Ich habe hier Dinge getestet, ziemlich fieberhaft, und ich habe Beachten Sie, dass sich das Setzen von IFS in der while-Befehlsliste unterschiedlich verhält, je nachdem, ob ein Doppelpunkt folgt oder nicht. Ich verstehe dieses Verhalten (noch) nicht und frage mich jetzt, ob es auf dieser Ebene besondere Überlegungen gibt ... z. while IFS=X readteilt sich nicht um X, while IFS=X; readtut es aber ...
Peter.O
(Sie bedeutete halb Kolon, nicht wahr?) Die zweite whilenicht viel Sinn macht - die Bedingung für die while Enden an diesem Semikolon, so gibt es keine eigentliche Schleife ... readwird nur der erste Befehl in der einelementige Schleife ... Oder auch nicht ? Was ist mit dem dodann ..?
Rozcietrzewiacz
1
Nein, warte - du hast recht, du kannst mehrere Befehle in der whileBedingung haben (vorher do).
Rozcietrzewiacz
Oh ... definitiv, Sie können sie haben ... wie Sie bemerkt haben ... aber sie scheinen das Semikolon nicht zu mögen ... (und die Schleife wird ad-infinitum weiterlaufen, bis der letzte Befehl ein Non zurückgibt -Zero-Exit-Code) ... Ich frage mich jetzt, ob die Falle vollständig in einem anderen Sektor liegt. das Verständnis, wie whiles Befehlsliste funktioniert, z. warum funktioniert IFS=, aber IFS=Xnicht ... (oder vielleicht habe ich eine Weile darüber nachgedacht .. Kaffeepause benötigt :)
Peter.O
1
$ rozcietrzewiacz .. Oops ... Ich hatte das Update nicht bemerkt, wenn ich meine Update bewege (wie in i.Vj. Kommentar erwähnt) .. Es sieht interessant aus , und es ist Ausgang Sinn machen ... aber auch nur für eine Nacht Vogel wie ich, es ist extrem spät ... (ich habe gerade die Morgenvögel gehört:) ... Das heißt, ich habe mich ein bisschen erholt und deine Beispiele gelesen ... Ich bin mir sicher, dass du es hast, aber ich muss schlafen :) ... Das ist fast ein Eureka! Moment ... danke
Peter.O
45

Schauen wir uns ein Beispiel mit einem sorgfältig ausgearbeiteten Eingabetext an:

text=' hello  world\
foo\bar'

Das sind zwei Zeilen, wobei die erste mit einem Leerzeichen beginnt und mit einem Backslash endet. Schauen wir uns zunächst an, was passiert, ohne dass Vorsichtsmaßnahmen getroffen werden müssen read(aber printf '%s\n' "$text"um vorsichtig zu drucken, $textohne dass die Gefahr einer Erweiterung besteht). (Unten sehen Sie $ ‌die Shell-Eingabeaufforderung.)

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

readaßen die Backslashes auf: backslash-newline bewirkt, dass die Newline ignoriert wird, und backslash-anything ignoriert diesen ersten Backslash. Um zu vermeiden, dass Backslashes speziell behandelt werden, verwenden wir read -r.

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

Das ist besser, wir haben wie erwartet zwei Zeilen. Die beiden Zeilen enthalten fast den gewünschten Inhalt: Der doppelte Abstand zwischen hellound worldwurde beibehalten, da er sich innerhalb der lineVariablen befindet. Auf der anderen Seite wurde der anfängliche Platz aufgefressen. Das liegt daran, dass readso viele Wörter gelesen werden, wie Sie Variablen übergeben, mit der Ausnahme, dass die letzte Variable den Rest der Zeile enthält - aber sie beginnt immer noch mit dem ersten Wort, dh die anfänglichen Leerzeichen werden verworfen.

Um also jede Zeile buchstäblich lesen zu können, müssen wir sicherstellen, dass keine Wortspaltung stattfindet . Dazu setzen wir die IFSVariable auf einen leeren Wert.

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

Beachten Sie, wie wir IFS speziell für die Dauer des readeingebauten einstellen . Das IFS= read -r linesetzt die Umgebungsvariable IFS(auf einen leeren Wert) speziell für die Ausführung von read. Dies ist eine Instanz der allgemeinen einfachen Befehlssyntax : Eine (möglicherweise leere) Folge von Variablenzuweisungen, gefolgt von einem Befehlsnamen und seinen Argumenten (Sie können auch an jeder Stelle Umleitungen einfügen). Da reades sich um eine integrierte Variable handelt, gelangt die Variable nie in die Umgebung eines externen Prozesses. nichtsdestotrotz ist der Wert von $IFSdas, was wir dort zuweisen, solange reades ausgeführt wird¹. Beachten Sie, dass dies readkeine spezielle integrierte Funktion ist , sodass die Zuweisung nur für ihre Dauer gültig ist.

Daher achten wir darauf, den Wert von nicht IFSfür andere Anweisungen zu ändern, die möglicherweise darauf angewiesen sind. Dieser Code funktioniert unabhängig von der IFSursprünglichen Einstellung des umgebenden Codes und verursacht keine Probleme, wenn der Code in der Schleife darauf angewiesen ist IFS.

Vergleichen Sie diesen Codeausschnitt, der Dateien in einem durch Doppelpunkte getrennten Pfad nachschlägt. Die Liste der Dateinamen wird aus einer Datei gelesen, ein Dateiname pro Zeile.

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

Wenn die Schleife wäre while IFS=; read -r name; do …, for dir in $PATHwürde sie nicht $PATHin durch Doppelpunkte getrennte Komponenten aufgeteilt. Wenn der Code IFS=; while read …wäre, wäre es noch offensichtlicher, dass im Schleifenkörper IFSnicht festgelegt :ist.

Natürlich wäre es möglich, den Wert von IFSnach der Ausführung wiederherzustellen read. Dafür müsste man jedoch den vorherigen Wert kennen, was ein zusätzlicher Aufwand ist. IFS= readist der einfache Weg (und bequemerweise auch der kürzeste Weg).

¹ Und wenn readdies durch ein überfülltes Signal unterbrochen wird, möglicherweise während die Überfüllung ausgeführt wird - dies wird von POSIX nicht angegeben und hängt von der Shell in der Praxis ab.

Gilles
quelle
4
Vielen Dank, Gilles. Eine sehr schöne Führung. (Meinten Sie 'set -f'?). Um das Gesagte noch einmal zu wiederholen, möchte ich das Problem hervorheben, das es gab Ich sehe es falsch. In erster Linie ist die Tatsache, dass das Konstrukt while IFS= read(ohne ein Semikolon danach =) keine spezielle Form von whileoder von IFSoder von ist read. Das Konstrukt ist generisch: dh. anyvar=anyvalue anycommand. Das Fehlen von ;after setting anyvarmacht den Gültigkeitsbereich von anyvar local zu anycommand. Die while-do / done-Schleife ist zu 100% unabhängig vom lokalen Gültigkeitsbereich von any_var.
Peter.O
3

Abgesehen von der (bereits geklärt) IFSScoping Unterschiede zwischen dem while IFS='' read, IFS=''; while readund while IFS=''; readIdiomen (pro-Befehl vs / script Shell weiter IFSVariable Scoping), die Take-Home - Lehre ist , dass Sie die führenden verlieren und Leerzeichen am Ende einer Eingabezeile , wenn das IFS - Variable ist auf (enthält ein) Leerzeichen gesetzt.

Dies kann schwerwiegende Folgen haben, wenn Dateipfade verarbeitet werden.

Daher ist das Setzen der IFS-Variablen auf die leere Zeichenfolge alles andere als eine schlechte Idee, da hierdurch sichergestellt wird, dass das führende und nachfolgende Leerzeichen einer Zeile nicht entfernt wird.

Siehe auch: Bash, Zeile für Zeile aus Datei lesen, mit 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"
)
jon
quelle
+ 1 ausgezeichnete Demonstration, Bereinigung nach mit "RM * Datei * mit * Leerzeichen *"
amdn
0

Inspiriert von Yuzems Antwort

Wenn Sie IFSeinen tatsächlichen Charakter festlegen möchten , funktionierte dies für mich

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