Shell-Skript lesen fehlende letzte Zeile

73

Ich habe ein ... seltsames Problem mit einem Bash-Shell-Skript, auf das ich gehofft hatte, einen Einblick zu bekommen.

Mein Team arbeitet an einem Skript, das Zeilen in einer Datei durchläuft und in jeder Zeile nach Inhalten sucht. Wir hatten einen Fehler, bei dem beim Ausführen über den automatisierten Prozess, bei dem verschiedene Skripte zusammengeführt werden, die letzte Zeile nicht angezeigt wurde.

Der Code, der zum Durchlaufen der Zeilen in der Datei verwendet wird (Name gespeichert in DATAFILEwar

cat "$DATAFILE" | while read line 

Wir könnten das Skript über die Befehlszeile ausführen und es würde jede Zeile in der Datei sehen, einschließlich der letzten, ganz gut. Bei Ausführung durch den automatisierten Prozess (der das Skript ausführt, das die DATENDATEI unmittelbar vor dem betreffenden Skript generiert) wird die letzte Zeile jedoch nie angezeigt.

Wir haben den Code aktualisiert, um Folgendes zu verwenden, um die Zeilen zu durchlaufen, und das Problem wurde behoben:

for line in `cat "$DATAFILE"` 

Hinweis: In DATAFILE wurde am Ende der Datei noch nie eine neue Zeile geschrieben.

Meine Frage besteht aus zwei Teilen ... Warum wird die letzte Zeile vom Originalcode nicht gesehen und warum ändert sich dies?

Ich dachte nur, ich könnte mir überlegen, warum die letzte Zeile nicht zu sehen ist:

  • Der vorherige Prozess, der die Datei schreibt, war darauf angewiesen, dass der Prozess beendet wird, um den Dateideskriptor zu schließen.
  • Das Problemskript wurde gestartet und die Datei zuvor so schnell geöffnet, dass der vorherige Prozess zwar "beendet", aber nicht so heruntergefahren / bereinigt wurde, dass das System den Dateideskriptor automatisch schließen konnte.

Abgesehen davon scheint es so, als ob, wenn Sie zwei Befehle in einem Shell-Skript haben, der erste vollständig heruntergefahren werden sollte, wenn das Skript den zweiten ausführt.

Jeder Einblick in die Fragen, insbesondere die erste, wäre sehr dankbar.

RHSeeger
quelle
Übrigens: Beachten Sie, cat somefile | while readdass alle in der whileSchleife festgelegten Variablen beim Beenden der Schleife zerstört werden. Sie wollen wahrscheinlich while read ...; done <somefilestattdessen; siehe BashFAQ # 24 .
Charles Duffy

Antworten:

97

Der C-Standard besagt, dass Textdateien mit einem Zeilenumbruch enden müssen, da sonst die Daten nach dem letzten Zeilenumbruch möglicherweise nicht richtig gelesen werden.

ISO / IEC 9899: 2011 §7.21.2 Streams

Ein Textstrom ist eine geordnete Folge von Zeichen, die zu Zeilen zusammengesetzt sind. Jede Zeile besteht aus null oder mehr Zeichen plus einem abschließenden Zeichen für neue Zeilen. Ob für die letzte Zeile ein abschließendes Zeichen für eine neue Zeile erforderlich ist, ist implementierungsdefiniert. Bei der Eingabe und Ausgabe müssen möglicherweise Zeichen hinzugefügt, geändert oder gelöscht werden, um den unterschiedlichen Konventionen für die Darstellung von Text in der Hostumgebung zu entsprechen. Daher muss es keine Eins-zu-Eins-Entsprechung zwischen den Zeichen in einem Stream und denen in der externen Darstellung geben. Aus einem Textstrom eingelesene Daten werden notwendigerweise nur dann mit den Daten verglichen, die zuvor in diesen Strom geschrieben wurden, wenn: die Daten nur aus Druckzeichen und der horizontalen Registerkarte und der neuen Zeile der Steuerzeichen bestehen; Vor keinem Zeilenumbruchzeichen stehen Leerzeichen. und das letzte Zeichen ist ein Zeilenumbruchzeichen. Ob Leerzeichen, die unmittelbar vor einem Zeilenumbruch ausgeschrieben werden, beim Einlesen angezeigt werden, ist implementierungsdefiniert.

Ich hätte nicht unerwartet einen fehlenden Zeilenumbruch am Ende der Datei, der Probleme in bash(oder einer Unix-Shell) verursachen könnte, aber das scheint das Problem reproduzierbar zu sein ( $ ist die Eingabeaufforderung in dieser Ausgabe):

$ echo xxx\\c
xxx$ { echo abc; echo def; echo ghi; echo xxx\\c; } > y
$ cat y
abc
def
ghi
xxx$
$ while read line; do echo $line; done < y
abc
def
ghi
$ bash -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ ksh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ zsh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ for line in $(<y); do echo $line; done      # Preferred notation in bash
abc
def
ghi
xxx
$ for line in $(cat y); do echo $line; done   # UUOC Award pending
abc
def
ghi
xxx
$

Es ist auch nicht auf bash- Korn shell ( ksh) beschränkt und zshverhält sich auch so. Ich lebe, ich lerne; Vielen Dank, dass Sie das Problem angesprochen haben.

Wie im obigen Code gezeigt, catliest der Befehl die gesamte Datei. Die for line in `cat $DATAFILE` Technik sammelt alle Ausgaben und ersetzt beliebige Sequenzen von Leerzeichen durch ein einzelnes Leerzeichen (ich schließe daraus, dass jede Zeile in der Datei keine Leerzeichen enthält).

Getestet unter Mac OS X 10.7.5.


Was sagt POSIX?

Die POSIX- readBefehlsspezifikation lautet:

Das Lese-Dienstprogramm liest eine einzelne Zeile von der Standardeingabe.

Sofern die -rOption nicht angegeben ist, fungiert <backslash> standardmäßig als Escape-Zeichen. Ein nicht entkoppelter <backslash> behält den Literalwert des folgenden Zeichens bei, mit Ausnahme eines <newline>. Wenn ein <newline> dem <backslash> folgt, interpretiert das Dienstprogramm read dies als Zeilenfortsetzung. Der <Backslash> und <newline>muss entfernt werden, bevor die Eingabe in Felder aufgeteilt wird. Alle anderen nicht entkoppelten <Backslash> -Zeichen werden entfernt, nachdem die Eingabe in Felder aufgeteilt wurde.

Wenn es sich bei der Standardeingabe um ein Endgerät handelt und die aufrufende Shell interaktiv ist, fordert read beim Lesen einer Eingabezeile, die mit einem <backlash> <newline> endet, eine Fortsetzungszeile auf, sofern die -rOption nicht angegeben ist.

Die abschließende <newline> (falls vorhanden) wird aus der Eingabe entfernt und die Ergebnisse werden wie in der Shell für die Ergebnisse der Parametererweiterung in Felder aufgeteilt (siehe Feldaufteilung). [...]

Beachten Sie, dass '(falls vorhanden)' (Hervorhebung im Zitat hinzugefügt)! Es scheint mir, dass wenn es keine neue Zeile gibt, es immer noch das Ergebnis lesen sollte. Auf der anderen Seite heißt es auch:

STDIN

Die Standardeingabe ist eine Textdatei.

und dann kehren Sie zur Debatte zurück, ob eine Datei, die nicht mit einem Zeilenumbruch endet, eine Textdatei ist oder nicht.

Die Begründung auf derselben Seite dokumentiert jedoch:

Obwohl die Standardeingabe eine Textdatei sein muss und daher immer mit einer <neuen Zeile> endet (es sei denn, es handelt sich um eine leere Datei), kann die Verarbeitung von Fortsetzungszeilen, wenn die -rOption nicht verwendet wird, dazu führen, dass die Eingabe nicht mit endet eine <newline>. Dies tritt auf, wenn die letzte Zeile der Eingabedatei mit einem <backlash> <newline> endet. Aus diesem Grund wird in der Beschreibung "falls vorhanden" in "Die abschließende <newline> (falls vorhanden) aus der Eingabe entfernt" verwendet. Es ist keine Lockerung der Anforderung, dass die Standardeingabe eine Textdatei sein muss.

Diese Begründung muss bedeuten, dass die Textdatei mit einem Zeilenumbruch enden soll.

Die POSIX-Definition einer Textdatei lautet:

3.395 Textdatei

Eine Datei, die Zeichen enthält, die in null oder mehr Zeilen organisiert sind. Die Zeilen enthalten keine NUL-Zeichen und keines darf die Länge von {LINE_MAX} Bytes überschreiten, einschließlich des Zeichens <newline>. Obwohl POSIX.1-2008 nicht zwischen Textdateien und Binärdateien unterscheidet (siehe ISO C-Standard), erzeugen viele Dienstprogramme nur vorhersehbare oder aussagekräftige Ausgaben, wenn sie mit Textdateien arbeiten. Die Standarddienstprogramme mit solchen Einschränkungen geben in ihren Abschnitten STDIN oder INPUT FILES immer "Textdateien" an.

Dies legt nicht fest, dass 'endet mit einer <newline>' direkt, sondern widerspricht dem C-Standard.


Eine Lösung für das Problem "No Terminal Newline"

Hinweis Gordon Davisson ‚s Antwort . Ein einfacher Test zeigt, dass seine Beobachtung korrekt ist:

$ while read line; do echo $line; done < y; echo $line
abc
def
ghi
xxx
$

Daher ist seine Technik von:

while read line || [ -n "$line" ]; do echo $line; done < y

oder:

cat y | while read line || [ -n "$line" ]; do echo $line; done

funktioniert für Dateien ohne Zeilenumbruch am Ende (zumindest auf meinem Computer).


Ich bin immer noch überrascht, dass die Shells das letzte Segment (es kann nicht als Zeile bezeichnet werden, da es nicht mit einer neuen Zeile endet) der Eingabe löschen, aber in POSIX gibt es möglicherweise eine ausreichende Begründung dafür. Und natürlich ist es am besten sicherzustellen, dass Ihre Textdateien wirklich Textdateien sind, die mit einem Zeilenumbruch enden.

Jonathan Leffler
quelle
Vielen Dank für die umfangreiche Berichterstattung. Ich denke, der Unterschied zwischen dem Verhalten der beiden Befehle ist sehr gut beschrieben. Ich bin immer noch ein wenig verwirrt darüber, warum der erste Befehl fehlschlägt, wenn er als Teil einer Pipeline ausgeführt wird, die die Datei generiert, aber nicht, wenn er unabhängig ausgeführt wird. Erwähnenswert ist auch, dass das Verhalten im Widerspruch zu Ihren Erfahrungen mit dem No-Newline-Verhalten beim Lesen zu stehen scheint. Möglicherweise muss ich zum Skript zurückkehren und sicherstellen, dass ich die Ergebnisse nicht falsch interpretiert habe.
RHSeeger
@adrelanos: Ich benutze, readweil es vor 30 Jahren gut funktioniert hat und immer noch für mich funktioniert. Moderner Stil ist zu verwenden, read -rweil er readdurch den POSIX-Prozess geschlachtet wurde. Ihr Anruf - Ich werde nicht beleidigt sein, wenn Sie ihn verwenden read -r, solange Sie erklären können, wovor er Sie im Vergleich zur Verwendung schützt read, und Sie können erklären, warum Ihnen dieser Schutz am Herzen liegt.
Jonathan Leffler
Dies löste mein Problem. Und diese Antwort sollte als Akzeptiert markiert sein. Vielen Dank.
K.Sopheak
Eine Möglichkeit, diese Einschränkung zu printf '\n' | cat myfile.txt - | while IFS= read -r VAR; do echo "$VAR"; done
umgehen,
67

Gemäß der POSIX-Spezifikation für den Lesebefehl sollte ein Status ungleich Null zurückgegeben werden, wenn "Dateiende erkannt wurde oder ein Fehler aufgetreten ist". Da EOF beim Lesen der letzten "Zeile" erkannt wird, wird $lineein Fehlerstatus festgelegt und anschließend zurückgegeben. Der Fehlerstatus verhindert, dass die Schleife in dieser letzten "Zeile" ausgeführt wird. Die Lösung ist einfach: Lassen Sie die Schleife ausführen, wenn der Lesebefehl erfolgreich ist ODER wenn etwas eingelesen wurde $line.

while read line || [ -n "$line" ]; do
Gordon Davisson
quelle
1
+1: Interessante Beobachtung, Gordon. Anhand meiner Beispieldatei habe yich ausgeführt: while read line; do echo $line; done < y; echo $lineund tatsächlich vier verschiedene Werte wiedergegeben. Ich bin nicht sicher, ob es ein besonders hilfreiches oder intuitives Verhalten ist, aber ...
Jonathan Leffler
Dies löste mein Problem, Wörter aus einer Textdatei zu lesen, ohne einen Zeilenumbruch am Ende der Textdatei zu haben.
tauseef_CuriousGuy
28

Zusätzliche Informationen hinzufügen:

  1. Es ist nicht erforderlich, die catwhile-Schleife zu verwenden. while ...;do something;done<filereicht.
  2. Lies keine Zeilen mit for.

Bei Verwendung der while-Schleife zum Lesen von Zeilen:

  1. Stellen Sie das IFSrichtig ein (andernfalls können Sie die Einrückung verlieren).
  2. Sie sollten fast immer die Option -r beim Lesen verwenden.

Wenn die oben genannten Anforderungen erfüllt sind, sieht eine ordnungsgemäße while-Schleife folgendermaßen aus:

while IFS= read -r line; do
  ...
done <file

Und damit es mit Dateien ohne Zeilenumbruch am Ende funktioniert (meine Lösung von hier aus neu veröffentlichen ):

while IFS= read -r line || [ -n "$line" ]; do
  echo "$line"
done <file

Oder grepmit while-Schleife verwenden:

while IFS= read -r line; do
  echo "$line"
done < <(grep "" file)
Jahid
quelle
Vielen Dank. Ich habe das gleiche Problem und das hat bei mir
funktioniert
1

Verwenden Sie sed, um die letzte Zeile einer Datei abzugleichen. Anschließend wird eine neue Zeile angehängt, falls keine vorhanden ist, und die Datei wird inline ersetzt:

sed -i '' -e '$a\' file

Der Code stammt von diesem Stackexchange- Link

Hinweis: Ich habe leer Apostrophe hinzugefügt , -i ''weil zumindest in OS X, -iwurde mit -eder Sicherungsdatei als Dateierweiterung. Ich hätte den ursprünglichen Beitrag gerne kommentiert, aber es fehlten 50 Punkte. Vielleicht bringt mir das ein paar in diesem Thread, danke.

Joel Bruner
quelle
0

Ich habe dies in der Kommandozeile getestet

# create dummy file. last line doesn't end with newline
printf "%i\n%i\nNo-newline-here" >testing

Testen Sie mit Ihrem ersten Formular (Rohrleitung zur while-Schleife)

cat testing | while read line; do echo $line; done

Dies übersieht die letzte Zeile, was sinnvoll ist, da readnur Eingaben erhalten werden, die mit einer neuen Zeile enden.


Testen Sie mit Ihrem zweiten Formular (Befehlsersetzung)

for line in `cat testbed1` ; do echo $line; done

Dies erhält auch die letzte Zeile


read Wird nur eingegeben, wenn es durch eine neue Zeile beendet wird. Deshalb verpassen Sie die letzte Zeile.

Auf der anderen Seite in der zweiten Form

`cat testing` 

erweitert sich auf die Form von

line1\nline2\n...lineM 

Das wird durch die Shell in mehrere Felder mit IFS getrennt, so dass Sie erhalten

line1 line2 line3 ... lineM 

Deshalb bekommen Sie immer noch die letzte Zeile.

p / s: Was ich nicht verstehe ist, wie Sie das erste Formular zum Laufen bringen ...

doubleDown
quelle
Ich gehe zurück zum Skript und stelle sicher, dass ich etwas nicht falsch interpretiere. Dies alles wurde im Rahmen einer Arbeit erledigt, bei der ich helfe, und es ist möglich, dass wir in unserer Eile etwas falsch gelesen haben, damit es funktioniert.
RHSeeger
0

Um dieses Problem zu umgehen, kann vor dem Lesen aus der Textdatei eine neue Zeile an die Datei angehängt werden.

echo "\n" >> $file_path

Dadurch wird sichergestellt, dass alle Zeilen, die zuvor in der Datei enthalten waren, gelesen werden.

ArunGJ
quelle
0

Ich hatte ein ähnliches Problem. Ich habe eine Katze einer Datei erstellt, sie an eine Sortierung weitergeleitet und dann das Ergebnis an ein 'beim Lesen von var1 var2 var3' weitergeleitet. dh: cat $ FILE | sort -k3 | beim Lesen Count IP Name do Die Arbeit unter "do" war eine if-Anweisung, die sich ändernde Daten im Feld $ Name identifizierte und basierend auf Änderungen oder keiner Änderung Summen von $ Count ergab oder gedruckt wurde die summierte Zeile zum Bericht. Ich bin auch auf das Problem gestoßen, bei dem ich nicht die letzte Zeile zum Drucken in den Bericht bekommen konnte. Ich ging mit dem einfachen Mittel vor, die Katze / Sortierung in eine neue Datei umzuleiten, eine neue Zeile in diese neue Datei zu wiederholen und dann mein "beim Lesen des IP-Namens zählen" für die neue Datei mit erfolgreichen Ergebnissen auszuführen. dh: cat $ FILE | sort -k3> NEWFILE echo "\ n" >> NEWFILE cat NEWFILE | beim Lesen Count IP Name do Manchmal ist der einfache, unelegante Weg der beste.

Gulesbaron
quelle