Wie erhalte ich den Teil einer Datei nach der ersten Zeile, der einem regulären Ausdruck entspricht?

169

Ich habe eine Datei mit ungefähr 1000 Zeilen. Ich möchte den Teil meiner Datei nach der Zeile, die meiner grep-Anweisung entspricht.

Das ist:

$ cat file | grep 'TERMINATE'     # It is found on line 534

Ich möchte also die Datei von Zeile 535 bis Zeile 1000 zur weiteren Verarbeitung.

Wie kann ich das machen?

Yugal Jindle
quelle
34
UUOC (nutzloser Gebrauch von Katze):grep 'TERMINATE' file
Jacob
30
Ich weiß das, es ist so, als würde ich es so benutzen. Kommen wir auf die Frage zurück.
Yugal Jindle
3
Dies ist eine sehr gute Programmierfrage, die sich gut für den Stapelüberlauf eignet.
Aioobe
13
@ Jacob Es ist überhaupt nicht nutzlos, Katze zu benutzen. Es wird verwendet, um eine Datei in die Standardausgabe zu drucken. Dies bedeutet, dass wir die grepStandardeingabeschnittstelle zum Einlesen von Daten verwenden können, anstatt lernen zu müssen, auf welchen Schalter grepund sed, und awk, und pandocund ffmpegusw. angewendet werden sollen , wenn wir lesen möchten aus einer Datei. Das spart Zeit, da wir nicht jedes Mal einen neuen Schalter lernen müssen, wenn wir dasselbe tun möchten: Aus einer Datei lesen.
Runeks
@runeks Ich stimme Ihrem Gefühl zu - aber das können Sie ohne Katze erreichen : grep 'TERMINATE' < file. Vielleicht macht es das Lesen ein bisschen schwieriger - aber das ist Shell-Scripting, also wird das immer ein Problem sein :)
LOAS

Antworten:

307

Im Folgenden wird die Zeilenübereinstimmung TERMINATEbis zum Ende der Datei gedruckt :

sed -n -e '/TERMINATE/,$p'

Erklärt: -n Deaktiviert das Standardverhalten sedbeim Drucken jeder Zeile nach dem Ausführen des Skripts. -eGibt ein Skript an sed, /TERMINATE/,$ist eine Auswahl des Adressbereichs (Zeile), dh die erste Zeile entspricht dem TERMINATEregulären Ausdruck (wie grep) am Ende der Datei ( $). und pist der Druckbefehl, der die aktuelle Zeile druckt.

Dies wird von der Zeile gedruckt, die auf die Zeilenübereinstimmung folgt, TERMINATEbis zum Ende der Datei:
(von NACH der übereinstimmenden Zeile bis EOF, ohne die übereinstimmende Zeile)

sed -e '1,/TERMINATE/d'

Erklärt: 1,/TERMINATE/ ist eine Adressbereichsauswahl (Zeile), die die erste Zeile für die Eingabe in die erste Zeile darstellt, die dem TERMINATEregulären Ausdruck entspricht, und dist der Löschbefehl, der die aktuelle Zeile löscht und zur nächsten Zeile springt. Da das sedStandardverhalten darin besteht, die Zeilen zu drucken, werden die Zeilen nach TERMINATE dem Ende der Eingabe gedruckt .

Bearbeiten:

Wenn Sie die Zeilen vorher wollen TERMINATE:

sed -e '/TERMINATE/,$d'

Und wenn Sie beide Zeilen vorher und nachher TERMINATEin 2 verschiedenen Dateien in einem Durchgang haben möchten :

sed -e '1,/TERMINATE/w before
/TERMINATE/,$w after' file

Die Vorher- und Nachher-Dateien enthalten die Zeile mit terminate. Um jede zu verarbeiten, müssen Sie Folgendes verwenden:

head -n -1 before
tail -n +2 after

Edit2:

Wenn Sie die Dateinamen im sed-Skript nicht fest codieren möchten, können Sie:

before=before.txt
after=after.txt
sed -e "1,/TERMINATE/w $before
/TERMINATE/,\$w $after" file

Aber dann müssen Sie die $Bedeutung der letzten Zeile umgehen, damit die Shell nicht versucht, die $wVariable zu erweitern (beachten Sie, dass wir jetzt doppelte Anführungszeichen um das Skript anstelle von einfachen Anführungszeichen verwenden).

Ich habe vergessen zu sagen, dass die neue Zeile nach den Dateinamen im Skript wichtig ist, damit sed weiß, dass die Dateinamen enden.


Bearbeiten: 2016-0530

Sébastien Clément fragte: "Wie würden Sie das Hardcodierte TERMINATEdurch eine Variable ersetzen ?"

Sie würden eine Variable für den passenden Text erstellen und dann auf die gleiche Weise wie im vorherigen Beispiel vorgehen:

matchtext=TERMINATE
before=before.txt
after=after.txt
sed -e "1,/$matchtext/w $before
/$matchtext/,\$w $after" file

So verwenden Sie eine Variable für den übereinstimmenden Text mit den vorherigen Beispielen:

## Print the line containing the matching text, till the end of the file:
## (from the matching line to EOF, including the matching line)
matchtext=TERMINATE
sed -n -e "/$matchtext/,\$p"
## Print from the line that follows the line containing the 
## matching text, till the end of the file:
## (from AFTER the matching line to EOF, NOT including the matching line)
matchtext=TERMINATE
sed -e "1,/$matchtext/d"
## Print all the lines before the line containing the matching text:
## (from line-1 to BEFORE the matching line, NOT including the matching line)
matchtext=TERMINATE
sed -e "/$matchtext/,\$d"

Die wichtigsten Punkte beim Ersetzen von Text durch Variablen in diesen Fällen sind:

  1. $variablenameIn single quotes[ '] eingeschlossene Variablen ( ) werden nicht "erweitert", Variablen in double quotes[ "] jedoch. Sie müssen also alle in ändern single quotes, double quoteswenn sie Text enthalten, den Sie durch eine Variable ersetzen möchten.
  2. Die sedBereiche auch enthalten $und werden unmittelbar gefolgt von einem Buchstaben wie: $p, $d, $w. Sie werden auch wie Variablen aussehen erweitert werden, so dass Sie diese fliehen müssen , um $Zeichen mit einem Backslash [ \] wie: \$p, \$d, \$w.
jfg956
quelle
Wie können wir die Zeilen vor TERMINATE abrufen und alles Folgende löschen?
Yugal Jindle
Wie würden Sie das fest codierte TERMINAL durch eine Variable ersetzen?
Sébastien Clément
2
Ein Anwendungsfall, der hier fehlt, ist das Drucken von Zeilen nach dem letzten Marker (wenn mehrere davon in der Datei enthalten sein können. Denken Sie an Protokolldateien usw.).
Mato
Das Beispiel sed -e "1,/$matchtext/d"funktioniert nicht, wenn $matchtextes in der ersten Zeile auftritt. Ich musste es ändern sed -e "0,/$matchtext/d".
Karalga
61

Als einfache Annäherung könnten Sie verwenden

grep -A100000 TERMINATE file

Hiermit werden TERMINATEbis zu 100000 Zeilen erfasst und ausgegeben, die dieser Zeile folgen.

Von der Manpage

-A NUM, --after-context=NUM

Drucken Sie NUM-Zeilen des nachfolgenden Kontexts, nachdem Sie die Zeilen abgeglichen haben. Platziert eine Zeile mit einem Gruppentrennzeichen (-) zwischen zusammenhängenden Gruppen von Übereinstimmungen. Mit der Option -o oder --only-match hat dies keine Auswirkung und es wird eine Warnung ausgegeben.

aioobe
quelle
Das mag funktionieren, aber ich muss es in mein Skript codieren, um viele Dateien zu verarbeiten. Zeigen Sie also eine generische Lösung.
Yugal Jindle
3
Ich denke, das ist eine praktische Lösung!
Michelgotta
2
ähnlich -B NUM, --before-context = NUM ​​Druckt NUM-Zeilen des führenden Kontexts, bevor die Zeilen abgeglichen werden. Platziert eine Zeile mit einem Gruppentrennzeichen (-) zwischen zusammenhängenden Gruppen von Übereinstimmungen. Mit der Option -o oder --only-matching hat dies keine Auswirkung und es wird eine Warnung ausgegeben.
PiyusG
Diese Lösung hat bei mir funktioniert, da ich leicht Variablen als Zeichenfolge verwenden kann, um nach ihnen zu suchen.
Jose Martinez
3
Gute Idee! Wenn Sie sich über die Größe des Kontexts nicht sicher sind, können Sie filestattdessen die Zeilen zählen :grep -A$(cat file | wc -l) TERMINATE file
Lemming
26

Ein hier zu verwendendes Tool ist awk:

cat file | awk 'BEGIN{ found=0} /TERMINATE/{found=1}  {if (found) print }'

Wie funktioniert das:

  1. Wir setzen die Variable 'found' auf Null und bewerten false
  2. Wenn mit dem regulären Ausdruck eine Übereinstimmung für 'TERMINATE' gefunden wird, setzen wir sie auf eins.
  3. Wenn unsere 'gefundene' Variable True ergibt, drucken Sie :)

Die anderen Lösungen verbrauchen möglicherweise viel Speicher, wenn Sie sie für sehr große Dateien verwenden.

Jos De Graeve
quelle
Einfach, elegant und sehr allgemein. In meinem Fall wurde alles bis zum zweiten Auftreten von '###' gedruckt:cat file | awk 'BEGIN{ found=0} /###/{found=found+1} {if (found<2) print }'
Aleksander Stelmaczonek
3
Ein hier nicht zu verwendendes Tool ist cat. awkist perfekt in der Lage, einen oder mehrere Dateinamen als Argumente zu verwenden. Siehe auch stackoverflow.com/questions/11710552/useless-use-of-cat
Tripleee
9

Wenn ich Ihre Frage richtig verstehe, möchten Sie die Zeilen danach TERMINATE , ohne die TERMINATE-zeile. awkkann dies auf einfache Weise tun:

awk '{if(found) print} /TERMINATE/{found=1}' your_file

Erläuterung:

  1. Obwohl dies keine bewährte Methode ist, können Sie sich darauf verlassen, dass alle Variablen standardmäßig 0 oder die leere Zeichenfolge sind, wenn sie nicht definiert sind. Der erste Ausdruck ( if(found) print) gibt also zunächst nichts aus.
  2. Nach dem Drucken prüfen wir, ob dies die Starterzeile ist (die nicht enthalten sein sollte).

Dadurch werden alle Zeilen nach der Zeile gedruckt TERMINATE.


Verallgemeinerung:

  • Sie haben eine Datei mit Start - und Ende -Linien und Sie die Linien zwischen den Zeilen ohne den Start - und Ende -Linien.
  • Start - und Ende -Linien durch einen regulären Ausdruck definiert werden, um die Linie entspricht.

Beispiel:

$ cat ex_file.txt 
not this line
second line
START
A good line to include
And this line
Yep
END
Nope more
...
never ever
$ awk '/END/{found=0} {if(found) print} /START/{found=1}' ex_file.txt 
A good line to include
And this line
Yep
$

Erläuterung:

  1. Wenn das Ende -Linie kein Druck gefunden wird , sollte getan werden. Beachten Sie, dass diese Prüfung erfolgt vor dem eigentlichen Druck das auszuschließen Ende -Linie aus dem Ergebnis.
  2. Drucken Sie die aktuelle Zeile, wenn foundeingestellt ist.
  3. Wenn die Startzeile gefunden wird, stellen Sie sie found=1so ein, dass die folgenden Zeilen gedruckt werden. Beachten Sie, dass diese Überprüfung nach dem eigentlichen Druck durchgeführt wird, um die Startzeile vom Ergebnis auszuschließen .

Anmerkungen:

  • Der Code basiert auf der Tatsache, dass alle awk-vars standardmäßig 0 oder die leere Zeichenfolge sind, wenn sie nicht definiert sind. Dies ist gültig, aber möglicherweise keine bewährte Methode, sodass Sie BEGIN{found=0}am Anfang des awk-Ausdrucks ein hinzufügen können .
  • Wenn mehrere Start-Ende- Blöcke gefunden werden, werden alle gedruckt.
UlfR
quelle
1
Awesome Awesome Beispiel. Ich habe gerade 2 Stunden damit verbracht, mir csplit, sed und alle möglichen überkomplizierten awk-Befehle anzusehen. Dies hat nicht nur das getan, was ich wollte, sondern es wurde auch einfach genug gezeigt, um daraus zu schließen, wie man es modifiziert, um ein paar andere verwandte Dinge zu tun, die ich brauchte. Erinnert mich daran, dass awk großartig ist und nicht nur in einem nicht zu entzifferenden Mist. Vielen Dank.
user1169420
{if(found) print}ist ein bisschen ein Anti-Pattern in awk, es ist idiomatischer, den Block durch nur zu ersetzen foundoder found;wenn Sie danach einen anderen Filter benötigen.
user000001
@ user000001 bitte erklären. Ich verstehe nicht, was und wie zu ersetzen ist. Wie auch immer, ich denke, die Art und Weise, wie es geschrieben ist, macht sehr deutlich, was los ist.
UlfR
1
Sie würden ersetzen awk '{if(found) print} /TERMINATE/{found=1}' your_filemit awk 'found; /TERMINATE/{found=1}' your_file, sollten sie beide das gleiche tun.
user000001
7

Verwenden Sie die Bash-Parametererweiterung wie folgt:

content=$(cat file)
echo "${content#*TERMINATE}"
Mu Qiao
quelle
Können Sie erklären, was Sie tun?
Yugal Jindle
Ich habe den Inhalt von "file" in die Variable $ content kopiert. Dann habe ich alle Zeichen entfernt, bis "TERMINATE" angezeigt wurde. Es wurde kein gieriger Abgleich verwendet, aber Sie können den gierigen Abgleich mit $ {content ## * TERMINATE} verwenden.
Mu Qiao
Hier ist der Link des Bash-Handbuchs: gnu.org/software/bash/manual/…
Mu Qiao
6
Was passiert, wenn die Datei 100 GB groß ist?
Znik
1
Downvote: Dies ist schrecklich (Einlesen der Datei in eine Variable) und falsch (Verwenden der Variablen ohne Anführungszeichen; und Sie sollten sie richtig verwenden printfoder sicherstellen, dass Sie genau wissen, an was Sie übergeben echo.).
Tripleee
6

grep -A 10000000 'TERMINATE'-Datei

  • ist viel, viel schneller als sed, besonders wenn man an wirklich großen Dateien arbeitet. Es funktioniert mit bis zu 10 Millionen Zeilen (oder was auch immer Sie eingeben), sodass es nicht schadet, wenn diese groß genug sind, um mit allem fertig zu werden, was Sie treffen.
user8910163
quelle
4

Es gibt viele Möglichkeiten, dies zu tun mit sedoder awk:

sed -n '/TERMINATE/,$p' file

Dies sucht TERMINATEin Ihrer Datei und druckt von dieser Zeile bis zum Ende der Datei.

awk '/TERMINATE/,0' file

Dies ist genau das gleiche Verhalten wie sed.

Wenn Sie die Nummer der Zeile kennen, von der aus Sie mit dem Drucken beginnen möchten, können Sie diese zusammen mit NR(Nummer des Datensatzes, der schließlich die Nummer der Zeile angibt) angeben:

awk 'NR>=535' file

Beispiel

$ seq 10 > a        #generate a file with one number per line, from 1 to 10
$ sed -n '/7/,$p' a
7
8
9
10
$ awk '/7/,0' a
7
8
9
10
$ awk 'NR>=7' a
7
8
9
10
fedorqui 'SO hör auf zu schaden'
quelle
Für die Nummer können Sie auch verwendenmore +7 file
123
Dies schließt die übereinstimmende Zeile ein, die in dieser Frage nicht gewünscht wird.
Mivk
@mivk Nun, dies ist auch der Fall bei der akzeptierten Antwort und der am zweithäufigsten bewerteten, so dass das Problem möglicherweise in einem irreführenden Titel liegt.
Fedorqui 'SO hör auf,'
3

Wenn Sie aus irgendeinem Grund die Verwendung von sed vermeiden möchten, wird die Zeilenübereinstimmung TERMINATEbis zum Ende der Datei wie folgt gedruckt :

tail -n "+$(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)" file

und das Folgende wird von der folgenden Zeilenübereinstimmung TERMINATEbis zum Ende der Datei gedruckt :

tail -n "+$(($(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)+1))" file

Es sind zwei Prozesse erforderlich, um das zu tun, was sed in einem Prozess tun kann. Wenn sich die Datei zwischen der Ausführung von grep und tail ändert, kann das Ergebnis inkohärent sein. Daher empfehle ich die Verwendung von sed. Wenn die Datei nicht enthält TERMINATE, schlägt der 1. Befehl fehl.

jfg956
quelle
Datei wird zweimal gescannt. Was ist, wenn es 100 GB groß ist?
Znik
1
Abgestimmt, weil dies eine beschissene Lösung ist, aber dann hochgestimmt, weil 90% der Antwort Vorbehalte sind.
Mad Physicist
0

Dies könnte eine Möglichkeit sein, dies zu tun. Wenn Sie wissen, in welcher Zeile der Datei Sie Ihr Grep-Wort haben und wie viele Zeilen Sie in Ihrer Datei haben:

grep -A466 'TERMINATE'-Datei

Mariah
quelle
1
Wenn die Zeilennummer bekannt ist, grepist sie nicht einmal erforderlich. Sie können nur verwenden tail -n $NUM, so ist dies nicht wirklich eine Antwort.
Samveen
-1

sed ist ein viel besseres Werkzeug für den Job: sed -n '/ re /, $ p' Datei

wo re ist regexp.

Eine weitere Option ist das Flag --after-context von grep. Sie müssen eine Zahl eingeben, um mit zu enden. Wenn Sie wc für die Datei verwenden, sollte dies den richtigen Wert für den Stopp angeben. Kombinieren Sie dies mit -n und Ihrem Übereinstimmungsausdruck.

ckwang
quelle
--nach dem Kontext ist in Ordnung, aber nicht in allen Fällen.
Yugal Jindle
Können Sie etwas anderes vorschlagen .. ??
Yugal Jindle
-2

Diese drucken alle Zeilen von der zuletzt gefundenen Zeile "TERMINATE" bis zum Ende der Datei:

LINE_NUMBER=`grep -o -n TERMINATE $OSCAM_LOG|tail -n 1|sed "s/:/ \\'/g"|awk -F" " '{print $1}'`
tail -n +$LINE_NUMBER $YOUR_FILE_NAME
easyyu
quelle
Das Extrahieren einer Zeilennummer mit, grepdamit Sie sie füttern können, tailist ein verschwenderisches Antimuster. Das Finden der Übereinstimmung und das Drucken bis zum Ende der Datei (oder umgekehrt das Drucken und Stoppen bei der ersten Übereinstimmung) erfolgt hervorragend mit den normalen, wesentlichen Regex-Werkzeugen selbst. Das Massive grep | tail | sed | awkist auch an und für sich ein massiver nutzloser Gebrauch von grepund Freunden .
Tripleee
Ich denke, er hat versucht, uns etwas zu geben, das die / letzte Instanz / von 'TERMINATE' findet und die Zeilen von dieser Instanz an angibt. Andere Implementierungen geben Ihnen die erste Instanz weiter. Die LINE_NUMBER sollte stattdessen wahrscheinlich so aussehen: LINE_NUMBER = $ (grep -o -n 'TERMINATE' $ OSCAM_LOG | tail -n 1 | awk -F: '{print $ 1}') Vielleicht nicht die eleganteste Art, aber es scheint die Arbeit zu erledigen. ^. ^
fbicknel
... oder alles in einer Zeile, aber hässlich: tail -n + $ (grep -o -n 'TERMINATE' $ YOUR_FILE_NAME | tail -n 1 | awk -F: '{print $ 1}') $ YOUR_FILE_NAME
fbicknel
.... und ich wollte zurückgehen und $ OSCAM_LOG anstelle von $ YOUR_FILE_NAME bearbeiten ... kann es aber aus irgendeinem Grund nicht. Keine Ahnung, woher $ OSCAM_LOG kam; Ich habe es nur gedankenlos nachgeahmt. oO
fbicknel
Dies allein in Awk zu tun, ist eine häufige Aufgabe in Awk 101. Wenn Sie bereits ein leistungsfähigeres Werkzeug verwenden, um nur die Zeilennummer zu erhalten, lassen tailSie die Aufgabe los und erledigen Sie sie insgesamt in dem leistungsfähigeren Werkzeug. Auf jeden Fall steht auf dem Titel eindeutig "erstes Spiel".
Tripleee