Grundlegendes zu "IFS = read -r line"

60

Ich verstehe natürlich, dass man der internen Feldtrennungsvariablen einen Wert hinzufügen kann. Zum Beispiel:

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

Ich verstehe auch, dass read -r lineDaten von stdinin Variable mit dem Namen speichern line:

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

Wie kann ein Befehl jedoch einen variablen Wert zuweisen? Und speichert es zuerst Daten von stdinbis Variable lineund gibt dann Wert von linebis IFS?

Martin
quelle

Antworten:

104

Einige Leute haben die falsche Vorstellung, dass reades der Befehl ist, eine Zeile zu lesen. Es ist nicht.

readLiest Wörter aus einer (möglicherweise mit Backslash fortgesetzten) Zeile, in der Wörter durch $IFSTrennzeichen getrennt sind und Backslash verwendet werden kann, um die Trennzeichen zu umgehen (oder Zeilen fortzusetzen).

Die generische Syntax lautet:

read word1 word2... remaining_words

readstdin liest ein Byte zu einer Zeit , bis er eine unescaped Zeilenende- Zeichen (oder End-of-Eingang) findet, aufteilt , dass das Ergebnis dieser Aufteilung in zu komplexen Regeln und speichert nach $word1, $word2... $remaining_words.

Zum Beispiel bei einer Eingabe wie:

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

und mit dem Standardwert $IFS, read a b cwürde zuweisen:

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

Nun, wenn nur ein Argument übergeben wird, wird das nicht read line. Es ist immer noch so read remaining_words. Die Verarbeitung von umgekehrten Schrägstrichen wird weiterhin durchgeführt. IFS-Leerzeichen werden weiterhin am Anfang und am Ende entfernt.

Die -rOption entfernt die Backslash-Verarbeitung. Also würde derselbe Befehl wie oben mit -rvergeben

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

Für den aufteilenden Teil ist es wichtig zu $IFSwissen, dass es zwei Klassen von Zeichen gibt : die IFS-Whitespace-Zeichen (nämlich Leerzeichen und Tabulatoren (und Zeilenumbrüche, obwohl dies hier keine Rolle spielt, wenn Sie -d verwenden), die ebenfalls vorkommen im Standardwert von $IFS) und den anderen sein. Die Behandlung dieser beiden Charakterklassen ist unterschiedlich.

Mit IFS=:( :wobei keine IFS Leerzeichen), wie ein Eingang :foo::bar::in aufgeteilt werden würde "", "foo", "", barund ""(und eine extra ""mit einigen Implementierungen obwohl das nicht mit Ausnahme keine Rolle read -a). Wenn wir dies durch :Leerzeichen ersetzen, erfolgt die Aufteilung nur in foound bar. Das heißt, führende und nachfolgende werden ignoriert, und Sequenzen von ihnen werden wie eine behandelt. Es gibt zusätzliche Regeln, wenn Leerzeichen und Nicht-Leerzeichen kombiniert werden $IFS. Einige Implementierungen können die Sonderbehandlung durch Verdoppeln der Zeichen in IFS ( IFS=::oder IFS=' ') hinzufügen / entfernen .

Wenn wir also nicht möchten, dass die führenden und nachfolgenden Leerzeichen ohne Leerzeichen entfernt werden, müssen wir diese IFS-Leerzeichen aus IFS entfernen.

Selbst bei IFS-Zeichen ohne Leerzeichen wird diese Eingabe durchgeführt, wenn die Eingabezeile eines (und nur eines) dieser Zeichen enthält und es sich um das letzte Zeichen in der Zeile handelt (wie IFS=: read -r wordbei einer Eingabe wie foo:), die POSIX-Shells enthält (nicht zshoder in einigen pdkshVersionen) gilt als eine betrachtet fooin diesen Schalen , weil Wort, die Zeichen $IFSwerden als als Terminatoren , so wordenthalten foo, nicht foo:.

Der kanonische Weg, eine Eingabezeile mit dem readeingebauten Code zu lesen, ist:

IFS= read -r line

(Beachten Sie, dass dies bei den meisten readImplementierungen nur für Textzeilen funktioniert, da das NUL-Zeichen nur in unterstützt wird. zsh)

Durch var=value cmddie Verwendung der Syntax wird sichergestellt, dass IFSnur für die Dauer dieses cmdBefehls ein anderer Wert festgelegt wird.

Geschichtsnotiz

Das readBuiltin wurde von der Bourne-Shell eingeführt und sollte schon Worte , keine Zeilen lesen . Es gibt einige wichtige Unterschiede zu modernen POSIX-Shells.

Die Bourne-Shell readunterstützt keine -rOption (die von der Korn-Shell eingeführt wurde), daher gibt es keine Möglichkeit, die Backslash-Verarbeitung zu deaktivieren, außer die Eingabe mit so etwas wie dieser sed 's/\\/&&/g'vorzuverarbeiten.

Die Bourne-Shell hatte nicht die Vorstellung von zwei Klassen von Zeichen (die wiederum von ksh eingeführt wurde). In der Bourne - Shell alle Zeichen der gleichen Behandlung unterzogen werden, wie IFS Leerzeichen in KSH tun, ist , dass IFS=: read a b cauf einem Eingangs wie foo::barzuweisen würde , barum $bnicht den leeren String.

In der Bourne-Shell mit:

var=value cmd

Wenn cmdes ein eingebautes ist (wie es readist), varbleibt es auf eingestellt, valuenachdem cmdes fertig ist. Das ist besonders kritisch, $IFSda in der Bourne-Shell $IFSalles aufgeteilt wird, nicht nur die Erweiterungen. Wenn Sie das Leerzeichen $IFSin der Bourne-Shell entfernen , "$@"funktioniert dies ebenfalls nicht mehr.

In der Bourne-Shell führt das Umleiten eines zusammengesetzten Befehls dazu, dass dieser in einer Subshell ausgeführt wird (in den frühesten Versionen funktionierten sogar Dinge wie read var < fileoder exec 3< file; read var <&3funktionierten nicht). In der Bourne-Shell war es daher selten, readetwas anderes als Benutzereingaben auf dem Terminal zu verwenden (wo diese Zeilenfortsetzungsbehandlung Sinn machte)

Einige Unices (wie HP / UX, es gibt auch einen in util-linux) haben noch einen lineBefehl zum Lesen einer Eingabezeile (der bis zur Single UNIX Specification Version 2 ein Standard-UNIX-Befehl war ).

Das ist im Grunde dasselbe, mit der head -n 1Ausnahme, dass jeweils ein Byte gelesen wird, um sicherzustellen, dass nicht mehr als eine Zeile gelesen wird. Auf diesen Systemen können Sie Folgendes ausführen:

line=`line`

Das bedeutet natürlich, einen neuen Prozess zu erzeugen, einen Befehl auszuführen und seine Ausgabe über eine Pipe zu lesen IFS= read -r line, was viel weniger effizient ist als die von ksh , aber dennoch viel intuitiver.

Stéphane Chazelas
quelle
3
+1 Vielen Dank für einige nützliche Einblicke in die verschiedenen Behandlungen auf Leerzeichen / Tabulator im Vergleich zu "anderen" in IFS in Bash ... Ich wusste, dass sie unterschiedlich behandelt wurden, aber diese Erklärung vereinfacht alles sehr. (Und die Einsicht zwischen Bash (und anderen Posix-Shells) und den regelmäßigen shUnterschieden ist auch nützlich, um tragbare Skripte zu schreiben!)
Olivier Dulac
Zumindest für bash-4.4.19, while read -r; do echo "'$REPLY'"; donearbeitet als while IFS= read -r line; do echo "'$line'"; done.
X-Yuri
Dies: "... diese falsche Vorstellung, dass Lesen der Befehl zum Lesen einer Zeile ist ..." führt mich zu der Annahme, dass reades etwas anderes geben muss , wenn die Verwendung zum Lesen einer Zeile fehlerhaft ist. Was könnte diese nicht-falsche Vorstellung sein? Oder ist diese erste Aussage technisch korrekt, aber in Wahrheit lautet der nicht-irrtümliche Begriff: "read ist der Befehl zum Lesen von Wörtern aus einer Zeile. Weil er so mächtig ist, können Sie damit Zeilen aus einer Datei lesen, indem Sie Folgendes tun: IFS= read -r line"
Mike S
8

Die Theorie

Es gibt zwei Konzepte, die hier im Spiel sind:

  • IFSist das Eingabefeld-Trennzeichen, dh die gelesene Zeichenfolge wird anhand der Zeichen in geteilt IFS. In einer Befehlszeile werden IFSnormalerweise Leerzeichen verwendet. Aus diesem Grund wird die Befehlszeile in Leerzeichen aufgeteilt.
  • So etwas wie VAR=value command"Ändern Sie die Befehlsumgebung so, dass VARsie den Wert value" hat. Grundsätzlich wird der Befehl commandwird sehen , VARwie mit dem Wert value, aber jeder Befehl ausgeführt wird , dass nach wie vor sehen , VARwie mit seinem vorherigen Wert. Mit anderen Worten, diese Variable wird nur für diese Anweisung geändert.

In diesem Fall

Wenn IFS= read -r lineSie also IFSeine leere Zeichenfolge festlegen (zum Teilen wird kein Zeichen verwendet, daher erfolgt keine Aufteilung), sodass readdie gesamte Zeile gelesen und als ein Wort angezeigt wird, das der lineVariablen zugewiesen wird. Die Änderungen wirken sich IFSnur auf diese Anweisung aus, sodass nachfolgende Befehle von der Änderung nicht betroffen sind.

Als Anmerkung

Während der Befehl korrekt ist , und wird wie vorgesehen, Einstellung IFSin diesem Fall ist nicht Macht 1 nicht notwendig. Wie in der bashManpage im readeingebauten Abschnitt geschrieben:

Eine Zeile wird aus der Standardeingabe gelesen [...] und das erste Wort wird dem Vornamen, das zweite Wort dem zweiten Namen usw. zugewiesen , wobei verbleibende Wörter und deren dazwischenliegende Trennzeichen dem Nachnamen zugewiesen werden . Wenn weniger Wörter als Namen aus dem Eingabestream gelesen werden, werden den verbleibenden Namen leere Werte zugewiesen. Die Zeichen in IFSwerden verwendet, um die Zeile in Wörter aufzuteilen. [...]

Da Sie nur die lineVariable haben, wird ihr ohnehin jedes Wort zugewiesen. Wenn Sie also keines der vorhergehenden und nachfolgenden Leerzeichen 1 benötigen, können Sie einfach schreiben read -r lineund damit fertig sein.

[1] Nur als Beispiel dafür , wie ein unsetoder $IFSStandardwert verursacht readVorder- / Hinter betrachten IFS Leerzeichen , könnten Sie versuchen:

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

Wenn Sie IFSes ausführen, werden Sie feststellen, dass die vorhergehenden und nachfolgenden Zeichen nicht überleben, wenn sie nicht gesetzt sind. Außerdem können einige seltsame Dinge passieren, wenn $IFSsie irgendwo früher im Skript geändert werden.

user43791
quelle
5

Sie sollten diese Aussage in zwei Teilen lesen, der erste , der den Wert des IFS - Variable löscht, dh auf den lesbaren äquivalent ist IFS="", wird die zweiten die Lesevariablen linevon stdin, read -r line.

Was in dieser Syntax speziell ist, ist, dass die IFS-Beeinflussung nur für den readBefehl gültig ist .

Wenn ich nichts verpasse, hat das Löschen in diesem speziellen Fall IFSkeine Auswirkung. Wie auch immer IFSeingestellt, wird die gesamte Zeile in der lineVariablen gelesen . Eine Verhaltensänderung wäre nur dann eingetreten, wenn mehr als eine Variable als Parameter an die readAnweisung übergeben worden wäre.

Bearbeiten:

Das -rsoll ermöglichen, dass Eingaben, die mit enden, \nicht speziell verarbeitet werden, dh dass der Backslash in der lineVariablen enthalten ist und nicht als Fortsetzungszeichen, um mehrzeilige Eingaben zu ermöglichen.

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

Das Löschen von IFS hat den Nebeneffekt, dass das Lesen verhindert, dass potenzielle führende und nachfolgende Leerzeichen oder Tabulatorzeichen abgeschnitten werden, z.

$ 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]

Vielen Dank an rici für den Hinweis auf diesen Unterschied.

jlliagre
quelle
Was Sie vermissen, ist, dass, wenn IFS nicht geändert read -r linewird, führende und nachfolgende Leerzeichen abgeschnitten werden, bevor die Eingabe der lineVariablen zugewiesen wird.
rici
@rici Ich habe so etwas vermutet, aber nur IFS-Zeichen zwischen Wörtern überprüft, keine führenden / nachfolgenden. Vielen Dank für den Hinweis!
12.
Durch das Löschen von IFS wird auch die Zuweisung mehrerer Variablen verhindert (Nebeneffekt). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"wird zeigen-aa bb--
Kyodev