Wie grep-inverse-match und "vor" und "nach" Zeilen auszuschließen

26

Betrachten Sie eine Textdatei mit den folgenden Einträgen:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

Mit einem vorgegebenen Muster (zB fff) würde ich gerne die obige Datei sehen, um in die Ausgabe zu gelangen:

all_lines except (pattern_matching_lines  U (B lines_before) U (A lines_after))

Zum Beispiel, wenn B = 2und A = 1, sollte die Ausgabe mit pattern = fffsein:

aaa
bbb
ccc
hhh
iii

Wie kann ich das mit grep oder anderen Kommandozeilen-Tools machen?


Beachten Sie, wenn ich versuche:

grep -v 'fff'  -A1 -B2 file.txt

Ich verstehe nicht was ich will. Ich bekomme stattdessen:

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii
Amelio Vazquez-Reina
quelle

Antworten:

9

In den meisten Fällen ist die Verwendung von Don's vielleicht besser, aber nur für den Fall, dass die Datei wirklich groß ist und Sie nicht sedmit einer so großen Skriptdatei umgehen können (was bei mehr als 5000 Skriptzeilen der Fall sein kann). . Hier ist es mit plain sed:

sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Dies ist ein Beispiel für ein sogenanntes Schiebefenster bei der Eingabe. Es funktioniert durch den Bau eines Look-Ahead- Puffer mit $B-count-Zeilen erstellt wird, bevor jemals versucht wird, etwas zu drucken.

Und eigentlich sollte ich wahrscheinlich meinen vorherigen Punkt klarstellen: Der primäre Leistungsbegrenzer sowohl für diese Lösung als auch für die Don's wird direkt mit dem Intervall zusammenhängen. Diese Lösung wird mit größeren Intervall langsam Größen , während Don mit größeren Intervall verlangsamt Frequenzen . Mit anderen Worten, auch wenn die Eingabedatei sehr groß ist und das tatsächliche Intervall immer noch sehr selten auftritt, ist seine Lösung wahrscheinlich der richtige Weg. Wenn die Intervallgröße jedoch relativ überschaubar ist und häufig auftritt, sollten Sie diese Lösung wählen.

Also hier ist der Workflow:

  • Wenn $matchim Musterbereich eine \newline vorangestellt ist, sedwird Djede \newline, die davor steht, rekursiv gelöscht .
    • Ich räumte auf $match vorher den Musterraum komplett freigelegt - aber um Überlappungen einfach zu handhaben, scheint es viel besser zu sein, einen Orientierungspunkt zu hinterlassen.
    • Ich habe auch versucht s/.*\n.*\($match\)/\1/, es auf einen Streich zu bringen und der Schleife auszuweichen, aber wenn $A/$Bgroß sind, dieD Streich erweist sich Elete-Schleife als erheblich schneller.
  • Dann ziehen wir die Next-Eingabezeile vor einem \newline-Begrenzer ein und versuchen erneut, a zu Dlöschen/\n.*$match/ weiteres Mal indem wir auf unseren zuletzt verwendeten regulären Ausdruck w / verweisen //.
  • Wenn der Musterraum übereinstimmt $match, kann dies nur mit erfolgen$match am $BAnfang der Zeile geschehen - alle vorhergehenden Zeilen wurden gelöscht.
    • Also fangen wir an, eine Schleife zu machen $A weiter.
    • Jeder Lauf dieser Schleife werden wir versuchen, s///für ubstitute &selbst den $Ath \newline Charakter in Musterraum und, falls erfolgreich,t est wird verzweigen uns - und unseren ganzen $After Puffer - aus dem Skript vollständig das Skript über von oben zu beginnen mit der nächsten Eingabezeile, falls vorhanden.
    • Wenn das test nicht erfolgreich ist, werden wir bzum :top-Label zurückkehren und für eine andere Eingabezeile zurückkehren - möglicherweise wird die Schleife erneut gestartet, wenn dies $matchwährend des Sammelns von $After auftritt.
  • Wenn wir an einem bekommen $matchFunktion Schleife, dann werden wir versuchen, pdie rucken $letzten Zeile , wenn diese es ist, und wenn !nicht versuchen, s///für ubstitute &sich der$B th \newline Charakter in Musterraum.
    • Wir werden das auch ttesten und wenn es erfolgreich ist, verzweigen wir zum :PRint-Label.
    • Wenn nicht, verzweigen wir zurück zu :top und erhalten eine weitere Eingabezeile, die an den Puffer angehängt wird.
  • Wenn wir es machen :PRINT wir PRINT dann Dzum ersten ÉLETE bis \nin Musterraum ewline und das Skript erneut ausführen von oben mit dem, was bleibt.

Und diesmal, wenn wir es tun würden A=2 B=2 match=5; seq 5 | sed...

Der Musterraum für die erste Iteration bei :Print würde wie folgt aussehen:

^1\n2\n3$

Und so sedsammelt sich sein $Bvorheriger Puffer. Und so wird sedgedruckt, um $B-count-Zeilen hinter der erfassten Eingabe auszugeben . Dies bedeutet , dass unser vorheriges Beispiel gegeben, sedwürde PRINT 1ausgeben, und dann Delete das und sende wie einen Musterraum an die Spitze des Skripts zurück die aussieht:

^2\n3$

... und oben im Skript wird die Next-Eingabezeile abgerufen und die nächste Iteration sieht so aus:

^2\n3\n4$

Wenn wir also das erste Vorkommen von 5in input finden, sieht der Musterraum tatsächlich so aus:

^3\n4\n5$

Dann Dstartet die elete-Schleife und wenn sie durch ist, sieht es so aus:

^5$

Und wenn die Next-Eingabezeile gezogen wird, sedtrifft EOF und wird beendet. Zu diesem Zeitpunkt wurden nur die PLinien 1 und 2 gedruckt.

Hier ist ein Beispiellauf:

A=8 B=7 match='[24689]0'
seq 100 |
sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Das druckt:

1
2
3
4
5
6
7
8
9
10
11
12
29
30
31
32
49
50
51
52
69
70
71
72
99
100
mikeserv
quelle
Eigentlich arbeite ich mit riesigen Dateien, und Dons Antwort war merklich langsamer als diese Lösung. Anfangs zögerte ich, meine akzeptierte Antwort zu ändern, aber der Geschwindigkeitsunterschied ist deutlich sichtbar.
Amelio Vazquez-Reina
4
@Amelio - Dies funktioniert mit einem Stream beliebiger Größe, und es muss nicht die Datei durchgelesen werden, um zu funktionieren. Der größte Leistungsfaktor ist die Größe von $Aund / oder $B. Je größer Sie diese Zahlen machen, desto langsamer wird es - aber Sie können sie einigermaßen groß machen.
mikeserv
1
@ AmelioVazquez-Reina - wenn du die ältere verwendest, ist das besser, denke ich.
mikeserv
11

Sie können gnu grepmit -Aund -Bgenau die Teile der Datei drucken, die Sie ausschließen möchten. Fügen Sie jedoch den -nSchalter hinzu, um auch die Zeilennummern zu drucken. Formatieren Sie dann die Ausgabe und übergeben Sie sie als Befehlsskript sed, um diese Zeilen zu löschen:

grep -n -A1 -B2 PATTERN infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Dies sollte auch mit Dateien von Mustern funktionieren, an die grepüber -fFolgendes übergeben wird:

grep -n -A1 -B2 -f patterns infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Ich denke, dies könnte leicht optimiert werden, wenn drei oder mehr aufeinanderfolgende Zeilennummern in Bereiche zusammengefasst würden, um z. B. 2,6dstatt 2d;3d;4d;5d;6d... zu haben. Wenn die Eingabe nur wenige Übereinstimmungen aufweist, lohnt es sich nicht, dies zu tun.


Andere Methoden, die die Zeilenreihenfolge nicht beibehalten und wahrscheinlich langsamer sind:
mit comm:

comm -13 <(grep PATTERN -A1 -B2 <(nl -ba -nrz -s: infile) | sort) \
<(nl -ba -nrz -s: infile | sort) | cut -d: -f2-

commerfordert eine sortierte Eingabe, was bedeutet, dass die Zeilenreihenfolge in der endgültigen Ausgabe nicht beibehalten wird (es sei denn, Ihre Datei ist bereits sortiert). Sie nlwird verwendet, um die Zeilen vor dem Sortieren zu nummerieren, comm -13druckt nur die für 2nd FILE eindeutigen Zeilen und cutentfernt dann den von hinzugefügten Teil nl(dh das erste Feld und das Trennzeichen :)
mit join:

join -t: -j1 -v1 <(nl -ba -nrz -s:  infile | sort) \
<(grep PATTERN -A1 -B2 <(nl -ba -nrz -s:  infile) | sort) | cut -d: -f2-
don_crissti
quelle
Danke Don! Kurze Frage, würden Sie die Lösung erwarten , commschneller zu sein als das Original mit sedund grep?
Amelio Vazquez-Reina,
1
@ AmelioVazquez-Reina - Das glaube ich nicht, da die Eingabedatei immer noch zweimal gelesen wird (plus ein bisschen sortiert wird), im Gegensatz zu Mikes Lösung, die die Datei nur einmal verarbeitet.
don_crissti
9

Wenn es Ihnen nichts ausmacht vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -NesSchaltet den nicht kompatiblen, stillen Ex-Modus ein. Nützlich für die Skripterstellung.
  • +{command}Sagen Sie vim, er soll {command}in der Datei ausgeführt werden.
  • g/${PAT}/ - In allen übereinstimmenden Zeilen /fff/ . Dies wird schwierig, wenn das Muster Sonderzeichen mit regulären Ausdrücken enthält, die Sie nicht so behandeln wollten.
  • .-${B} - von 1 Zeile darüber
  • .+${A}- bis 2 Zeilen darunter (siehe :he cmdline-rangesfür diese beiden)
  • d - Löschen Sie die Zeilen.
  • +w !tee Schreibt dann auf die Standardausgabe.
  • +q! wird beendet, ohne die Änderungen zu speichern.

Sie können die Variablen überspringen und das Muster und die Zahlen direkt verwenden. Ich habe sie nur aus Gründen der Klarheit verwendet.

muru
quelle
3

Wie wäre es (mit GNU grepund bash):

$ grep -vFf - file.txt < <(grep -B2 -A1 'fff' file.txt)
aaa
bbb
ccc
hhh
iii

Hier finden wir die zu grep -B2 -A1 'fff' file.txtverwerfenden Zeilen und verwenden diese als Eingabedatei, um die gewünschten Zeilen zu finden, die diese verwerfen.

heemayl
quelle
Hmm, dies gibt nichts auf meinem Computer aus (OS X)
Amelio Vazquez-Reina
@ AmelioVazquez-Reina Entschuldigung. Ich kannte Ihr Betriebssystem noch nicht. Trotzdem habe ich dies auf Ubuntu getestet.
heemayl
2
Dies hätte das gleiche Problem wie kosdie (jetzt gelöschte) Lösung, als ob doppelte Zeilen in der Eingabedatei vorhanden wären und einige außerhalb des Bereichs und andere innerhalb dieses Bereichs liegen. Dadurch werden alle gelöscht. Bei mehreren Vorkommen eines Musters werden Linien wie --in der Eingabedatei (außerhalb der Bereiche) gelöscht, da das Trennzeichen --in grepder Ausgabe angezeigt wird , wenn mehr als eine Linie mit dem Muster übereinstimmt (letzteres ist höchst unwahrscheinlich, aber wertvoll) Erwähnung, denke ich).
don_crissti
@don_crissti Danke .. du hast recht .. obwohl ich das Beispiel von OP wörtlich genommen habe .. ich lasse es für den Fall, dass jemand es später hilfreich findet ..
heemayl
1

Sie können ein ausreichend gutes Ergebnis erzielen, indem Sie temporäre Dateien verwenden:

my_file=file.txt #or =$1 if in a script

#create a file with all the lines to discard, numbered
grep -n -B1 -A5 TBD "$my_file" |cut -d\  -f1|tr -d ':-'|sort > /tmp/___"$my_file"_unpair

#number all the lines
nl -nln "$my_file"|cut -d\  -f1|tr -d ':-'|sort >  /tmp/___"$my_file"_all

#join the two, creating a file with the numbers of all the lines to keep
#i.e. of those _not_ found in the "unpair" file
join -v2  /tmp/___"$my_file"_unpair /tmp/___"$my_file"_all|sort -n > /tmp/___"$my_file"_lines_to_keep

#eventually use these line numbers to extract lines from the original file
nl -nln $my_file|join - /tmp/___"$my_file"_lines_to_keep |cut -d\  -f2- > "$my_file"_clean

Das Ergebnis ist gut genug, da Sie dabei Einrückungen verlieren können. Wenn es sich jedoch um eine XML- oder einrückungsunempfindliche Datei handelt, sollte dies kein Problem sein. Da dieses Skript ein RAM-Laufwerk verwendet, ist das Schreiben und Lesen dieser temporären Dateien genauso schnell wie das Arbeiten im Arbeitsspeicher.

RafDouglas
quelle
1

Wenn Sie nur einige Zeilen vor einem bestimmten Marker ausschließen möchten, können Sie Folgendes verwenden:

awk -v nlines=2 '/Exception/ {for (i=0; i<nlines; i++) {getline}; next} 1'

(Glenn Jackman unter https://stackoverflow.com/a/1492538 )

Durch die Weiterleitung einiger Befehle können Sie das Vorher / Nachher-Verhalten erhalten:

awk -v nlines_after=5 '/EXCEPTION/ {for (i=0; i<nlines_after; i++) {getline};print "EXCEPTION" ;next} 1' filename.txt|\
tac|\
awk -v nlines_before=1 '/EXCEPTION/ {for (i=0; i<nlines_before; i++) {getline}; next} 1'|\
tac
RafDouglas
quelle
1
Genial, verwenden Sie awkeine umgekehrte Datei, um folgende Zeilen zu behandeln, wenn Sie vorhaben, Zeilen zu beeinflussen, und kehren Sie das Ergebnis erneut um.
Karmakaze
0

Eine Möglichkeit, dies zu erreichen, besteht möglicherweise darin, eine Variable zu erstellen und Folgendes zu tun:

grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt

Auf diese Weise haben Sie immer noch Ihre Struktur. Und Sie können leicht von dem einen Liner sehen, was Sie entfernen möchten.

$ grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt
aaa
bbb
ccc
hhh
iii
Lordpavel
quelle
dieselbe lösung wie heemayl und dasselbe problem wie von don_crissti beschrieben: Dies hätte dasselbe problem wie die (jetzt gelöschte) lösung von kos, als ob doppelte zeilen in der eingabedatei vorhanden wären und einige außerhalb des bereichs liegen und andere innerhalb dieses bereichs Dadurch werden alle gelöscht. Wenn bei mehreren Vorkommen eines Musters Zeilen wie - in der Eingabedatei (außerhalb der Bereiche) vorhanden sind, werden diese gelöscht, da das Trennzeichen - in der Ausgabe von grep angezeigt wird, wenn mehr als eine Zeile mit dem Muster übereinstimmt (letzteres ist sehr hoch) unwahrscheinlich, aber erwähnenswert).
Bodo Thiesen,
0

Wenn es nur 1 Treffer gibt:

A=1; B=2; n=$(grep -n 'fff' file.txt | cut -d: -f1)
head -n $((n-B-1)) file.txt ; tail -n +$((n+A+1)) file.txt

Ansonsten (awk):

# -vA=a -vB=b -vpattern=pat must be provided
BEGIN{

    # add file again. assume single file
    ARGV[ARGC]=ARGV[ARGC-1]
    ++ARGC
}

# the same as grep -An -Bn pattern
FNR==NR && $0 ~ pattern{
    for (i = 0; i <= B; ++i)
        a[NR-i]++
    for (i = 1; i <= A; ++i)
        a[NR+i]++
}

FNR!=NR && !(FNR in a)
dedowsdi
quelle