it-swarm.com.de

Grep vom Ende einer Datei bis zum Anfang

Ich habe eine Datei mit ungefähr 30.000.000 Zeilen (Radius Accounting) und muss die letzte Übereinstimmung eines bestimmten Musters finden.

Der Befehl:

tac accounting.log | grep $pattern

gibt was ich brauche, aber es ist zu langsam, weil das Betriebssystem zuerst die gesamte Datei lesen und dann an die Pipe senden muss.

Ich brauche also etwas schnelles, das die Datei von der letzten bis zur ersten Zeile lesen kann.

45
Hábner Costa

tac hilft nur, wenn Sie auch grep -m 1 verwenden (vorausgesetzt GNU grep)), damit grep nach dem ersten Match stoppt ::

tac accounting.log | grep -m 1 foo

Von man grep:

   -m NUM, --max-count=NUM
          Stop reading a file after NUM matching lines.  

In dem Beispiel in Ihrer Frage müssen sowohl tac als auch grep die gesamte Datei verarbeiten, sodass die Verwendung von tac irgendwie sinnlos ist.

Wenn Sie also nicht grep -m Verwenden, verwenden Sie tac überhaupt nicht. Analysieren Sie einfach die Ausgabe von grep, um die letzte Übereinstimmung zu erhalten:

grep foo accounting.log | tail -n 1 

Ein anderer Ansatz wäre die Verwendung von Perl oder einer anderen Skriptsprache. Zum Beispiel (wobei $pattern=foo):

Perl -ne '$l=$_ if /foo/; END{print $l}' file

oder

awk '/foo/{k=$0}END{print k}' file
48
terdon

Der Grund warum

tac file | grep foo | head -n 1

hört beim ersten Spiel nicht auf, liegt an der Pufferung.

Normalerweise wird head -n 1 nach dem Lesen einer Zeile beendet. Also sollte grep ein SIGPIPE erhalten und beenden, sobald es seine zweite Zeile schreibt.

Aber was passiert ist, dass grep es puffert, weil seine Ausgabe nicht an ein Terminal geht. Das heißt, es wird erst geschrieben, wenn es sich genug angesammelt hat (4096 Bytes in meinem Test mit GNU grep).

Das bedeutet, dass grep nicht beendet wird, bevor 8192 Datenbytes geschrieben wurden, also wahrscheinlich einige Zeilen.

Mit GNU grep können Sie das Beenden früher durchführen, indem Sie --line-buffered verwenden, das es anweist, Zeilen zu schreiben, sobald sie gefunden werden, unabhängig davon, ob sie an ein Terminal gesendet werden oder nicht. Also würde grep in der zweiten Zeile, die es findet, beendet.

Aber mit GNU grep können Sie stattdessen -m 1 verwenden, wie @terdon gezeigt hat, was besser ist, wenn es beim ersten Match beendet wird.

Wenn Ihr grep nicht das GNU grep ist, können Sie stattdessen sed oder awk verwenden. Da tac ein GNU Befehl ist, bezweifle ich, dass Sie ein System mit tac finden, bei dem grep nicht GNU grep ist.

tac file | sed "/$pattern/!d;q"                             # BRE
tac file | P=$pattern awk '$0 ~ ENVIRON["P"] {print; exit}' # ERE

Einige Systeme haben tail -r, um dasselbe zu tun wie GNU tac.

Beachten Sie, dass für reguläre (durchsuchbare) Dateien tac und tail -r effizient sind, da sie die Dateien rückwärts lesen, sondern nicht nur die Datei vollständig im Speicher lesen, bevor sie rückwärts gedruckt werden (as @ slms sed Ansatz oder tac für nicht reguläre Dateien würde).

Auf Systemen, auf denen weder tac noch tail -r verfügbar sind, besteht die einzige Möglichkeit darin, das Rückwärtslesen von Hand mit Programmiersprachen wie Perl zu implementieren oder Folgendes zu verwenden:

grep -e "$pattern" file | tail -n1

Oder:

sed "/$pattern/h;$!d;g" file

Aber das bedeutet, alle Übereinstimmungen zu finden und nur die letzte auszudrucken.

12

Hier ist eine mögliche Lösung, die den Ort des ersten Auftretens des Musters vom letzten findet:

tac -s "$pattern" -r accounting.log | head -n 1

Dies nutzt das -s und -r Schalter von tac, die wie folgt sind:

-s, --separator=STRING
use STRING as the separator instead of newline

-r, --regex
interpret the separator as a regular expression
4
mkc

Mit sed

Zeigen einiger alternativer Methoden zu @ Terdons feine Antwort using sed:

$ sed '1!G;h;$!d' file | grep -m 1 $pattern
$ sed -n '1!G;h;$p' file | grep -m 1 $pattern

Beispiele

$ seq 10 > file

$ sed '1!G;h;$!d' file | grep -m 1 5
5

$ sed -n '1!G;h;$p' file | grep -m 1 5
5

Verwenden von Perl

Als Bonus ist hier eine etwas einfachere Notation in Perl zu merken:

$ Perl -e 'print reverse <>' file | grep -m 1 $pattern

Beispiel

$ Perl -e 'print reverse <>' file | grep -m 1 5
5
2
slm