Wie kann man mit einem regulären Ausdruck mehrere Zeilen aus einer Datei entfernen?

10

Wie kann man mit einem regulären Ausdruck mehrere Zeilen aus einer Datei entfernen?

Ich möchte oft mehrere Zeilen erhalten / mehrere Zeilen durch einen regulären Ausdruck ändern. Ein Beispielfall:

Ich versuche, einen Teil einer XML / SGML-Datei zu lesen (sie sind nicht unbedingt gut geformt oder haben eine vorhersehbare Syntax, daher wäre ein regulärer Ausdruck sicherer als ein richtiger Parser. Außerdem möchte ich dies auch vollständig tun können unstrukturierte Dateien, in denen nur einige Schlüsselwörter bekannt sind.) in einem Shell-Skript (läuft unter Solaris und Linux).

Beispiel XML:

<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>

Daraus möchte ich das lesen, <tag1>wenn es fooirgendwo drin ist.

Ein Regex wie (<tag1>.*?foo.*?</tag1>)sollte den richtigen Teil geben, aber Werkzeuge mögen grepund sedfunktionieren nur für mich mit einzelnen Zeilen. Wie bekomme ich

<tag1>
 <tag2>foo</tag2>
</tag1>

in diesem Beispiel?

Den
quelle
@evilsoup Das stimmt, aber meine Frage bezieht sich nicht speziell auf XML / SGML-Dateien, sondern nur auf Textdateien.
Den

Antworten:

7

Wenn Sie GNU grep installiert haben können Sie mehrzeilige , indem in der Suche starten -P(Perl reguläre Ausdrücke) Flagge und Aktivierung PCRE_DOTALLmit(?s)

grep -oP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
<tag1>
<tag2>foo</tag2>
</tag1>

Wenn das oben Genannte auf Ihrer Plattform nicht funktioniert, versuchen Sie -zzusätzlich, das Flag zu übergeben. Dadurch wird grep gezwungen, NUL als Zeilentrennzeichen zu behandeln, sodass die gesamte Datei wie eine einzelne Zeile aussieht.

grep -ozP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
iruvar
quelle
Dies gibt keine Ausgabe auf meinem System, wenn es in der Beispieldatei des OP ausgeführt wird.
Terdon
Funktioniert bei mir. +1. Vielen Dank für den (?s)Tipp
Nathan Wallace
@terdon, welche Version von GNU grep laufen Sie?
iruvar
@ 1_CR (GNU grep) 2.14auf Debian. Ich habe das OPs-Beispiel so wie es ist kopiert (nur die letzte neue Zeile hinzugefügt) und Ihr grepdarauf ausgeführt, aber keine Ergebnisse erhalten.
Terdon
1
@slm, ich bin auf pcre 6.6, GNU grep 2.5.1 auf RHEL. Haben Sie etwas dagegen, es grep -ozPstatt grep -oPauf Ihren Plattformen zu versuchen ?
iruvar
3
#begin command block
#append all lines between two addresses to hold space 
    sed -n -f - <<\SCRIPT file.xml
        \|<tag1>|,\|</tag1>|{ H 
#at last line of search block exchange hold and pattern space 
            \|</tag1>|{ x
#if not conditional ;  clear buffer ; branch to script end
                \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
#do work ; print result; clear buffer ; close blocks
    s?*?*?;p;s/.*//;h;b}}
SCRIPT

Wenn Sie angesichts der angezeigten Daten vor der letzten Bereinigungszeile die oben genannten Schritte ausführen, sollten Sie mit einem sedMusterbereich arbeiten, der wie folgt aussieht:

 ^\n<tag1>\n<tag2>foo</tag2>\n</tag1>$

Sie können Ihren Musterbereich jederzeit mit look ausdrucken . Sie können dann \nZeichen adressieren .

sed l <file

Zeigt Ihnen, dass jede Zeile sedsie in der Phase verarbeitet, in der sie laufgerufen wird.

Also habe ich es gerade getestet und es brauchte noch eines \backslashnach dem ,commain der ersten Zeile, aber ansonsten funktioniert es so wie es ist. Hier habe ich es in ein _sed_functionFeld eingefügt, damit ich es in dieser Antwort leicht zu Demonstrationszwecken aufrufen kann: (funktioniert mit eingeschlossenen Kommentaren, wird hier jedoch der Kürze halber entfernt)

_sed_function() { sed -n -f /dev/fd/3 
} 3<<\SCRIPT <<\FILE 
    \|<tag1>|,\|</tag1>|{ H
        \|</tag1>|{ x
            \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
    s?*?*?;p;s/.*//;h;b}}
#END
SCRIPT
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
FILE


_sed_function
#OUTPUT#
<tag1>
 <tag2>foo</tag2>
</tag1>

Jetzt wechseln wir das pfür ein, ldamit wir sehen können, womit wir arbeiten, während wir unser Skript entwickeln, und entfernen die Nicht-Op-Demo, s?so dass die letzte Zeile von uns sed 3<<\SCRIPTeinfach so aussieht:

l;s/.*//;h;b}}

Dann werde ich es wieder ausführen:

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

In Ordnung! Ich hatte also Recht - das ist ein gutes Gefühl. Mischen wir jetzt unseren lBlick, um die Linien zu sehen, die er zieht, aber löscht. Wir werden unseren Strom entfernen lund einen hinzufügen, !{block}damit es so aussieht:

!{l;s/.*//;h;b}

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$

So sieht es aus, kurz bevor wir es auslöschen.

Eine letzte Sache, die ich Ihnen zeigen möchte, ist der Halte Raum, während wir ihn aufbauen. Es gibt einige Schlüsselkonzepte, die ich hoffentlich demonstrieren kann. Also entferne ich den letzten lBlick wieder und ändere die erste Zeile, um Ham Ende einen Blick in den alten Raum zu werfen :

{ H ; x ; l ; x

_sed_function
#OUTPUT#
\n<tag1>$
\n<tag1>\n <tag2>bar</tag2>$
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$
\n<tag1>$
\n<tag1>\n <tag2>foo</tag2>$
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

HDer alte Raum überlebt Linienzyklen - daher der Name. Was die Leute oft stolpern - ok, worauf ich oft stolpere - ist, dass es gelöscht werden muss, nachdem Sie es benutzt haben. In diesem Fall xändere ich mich nur einmal, so dass der Haltebereich zum Musterraum wird und umgekehrt, und diese Änderung überlebt auch Linienzyklen.

Der Effekt ist, dass ich meinen Haltebereich löschen muss, der früher mein Musterbereich war. Ich mache das, indem ich zuerst den aktuellen Musterraum lösche mit:

s/.*//

Das wählt einfach jedes Zeichen aus und entfernt es. Ich kann nicht verwenden, dda dies meinen aktuellen Zeilenzyklus beenden würde und der nächste Befehl nicht abgeschlossen würde, was mein Skript so ziemlich in den Papierkorb werfen würde.

h

Dies funktioniert ähnlich wie, Haber es überschreibt den Haltebereich. Daher habe ich meinen leeren Musterbereich über den oberen Bereich meines Haltebereichs kopiert und ihn effektiv gelöscht. Jetzt kann ich einfach:

b

aus.

Und so schreibe ich sedSkripte.

mikeserv
quelle
Danke @slm! Du bist ein wirklich guter Typ, weißt du das?
Mikeserv
Danke, gute Arbeit, sehr schneller Aufstieg auf
3 km
Ich weiß nicht, @slm. Ich fange an zu sehen, dass ich hier immer weniger lerne - vielleicht bin ich seiner Nützlichkeit entwachsen. Ich muss darüber nachdenken. Ich bin in den letzten Wochen kaum auf die Seite gekommen.
Mikeserv
Mindestens 10 km erreichen. Alles, was es wert ist, freigeschaltet zu werden, befindet sich auf diesem Niveau. Chipping weiter weg, 5k werden jetzt ziemlich schnell kommen.
slm
1
Nun, @slm - du bist sowieso eine seltene Rasse. Ich stimme jedoch den mehrfachen Antworten zu. Deshalb nervt es mich, wenn einige Qs geschlossen werden. Aber das kommt eigentlich selten vor. Nochmals vielen Dank, slm.
Mikeserv
2

Die Antwort von @ jamespfinn funktioniert einwandfrei, wenn Ihre Datei so einfach ist wie Ihr Beispiel. Wenn Sie eine komplexere Situation haben, in der <tag1>sich mehr als 2 Zeilen erstrecken könnten, benötigen Sie einen etwas komplexeren Trick. Beispielsweise:

$ cat foo.xml
<tag1>
 <tag2>bar</tag2>
 <tag3>baz</tag3>
</tag1>
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
$ perl -ne 'if(/<tag1>/){$a=1;} 
            if($a==1){push @l,$_}
            if(/<\/tag1>/){
              if(grep {/foo/} @l){print "@l";}
               $a=0; @l=()
            }' foo.xml
<tag1>

  <tag2>foo</tag2>
 </tag1>
<tag1>
  <tag2>bar</tag2>

  <tag2>foo</tag2>
  <tag3>baz</tag3>
 </tag1>

Das Perl-Skript verarbeitet jede Zeile Ihrer Eingabedatei und

  • if(/<tag1>/){$a=1;}: Die Variable $awird auf gesetzt, 1wenn ein öffnendes Tag ( <tag1>) gefunden wird.

  • if($a==1){push @l,$_}: Wenn dies der Fall $aist 1, fügen Sie diese Zeile dem Array hinzu @l.

  • if(/<\/tag1>/) : Wenn die aktuelle Zeile mit dem schließenden Tag übereinstimmt:

    • if(grep {/foo/} @l){print "@l"}: Wenn eine der im Array gespeicherten Zeilen @l(dies sind die Zeilen zwischen <tag1>und </tag1>) mit der Zeichenfolge übereinstimmt foo, drucken Sie den Inhalt von @l.
    • $a=0; @l=(): Leere die Liste ( @l=()) und setze $asie auf 0 zurück.
terdon
quelle
Dies funktioniert gut, außer in dem Fall, dass mehr als ein <tag1> "foo" enthält. In diesem Fall druckt es alles vom Anfang des ersten <tag1> bis zum Ende des letzten </ tag1> ...
Den
@den Ich habe es mit dem Beispiel in meiner Antwort getestet, das 3 <tag1>mit enthält, foound es funktioniert gut. Wann scheitert es für Sie?
Terdon
Es fühlt sich so falsch an, XML mit Regex zu
analysieren
1

Hier ist eine sedAlternative:

sed -n '/<tag1/{:x N;/<\/tag1/!b x};/foo/p' your_file

Erläuterung

  • -n bedeutet, keine Zeilen ohne Anweisung zu drucken.
  • /<tag1/ stimmt zuerst mit dem Eröffnungs-Tag überein
  • :x ist eine Bezeichnung, um später zu diesem Punkt springen zu können
  • N Fügt die nächste Zeile zum Musterbereich hinzu (aktiver Puffer).
  • /<\/tag1/!b xWenn der aktuelle Musterbereich kein schließendes Tag enthält, verzweigen Sie zu dem xzuvor erstellten Label. Wir fügen dem Musterbereich daher so lange Linien hinzu, bis wir unser schließendes Tag gefunden haben.
  • /foo/pbedeutet, wenn der aktuelle Musterbereich übereinstimmt foo, sollte er gedruckt werden.
Joseph R.
quelle
1

Sie könnten es mit GNU awk tun, indem Sie das End-Tag als Datensatztrennzeichen behandeln, z. B. für ein bekanntes End-Tag </tag1>:

gawk -vRS="\n</tag1>\n" '/foo/ {printf "%s%s", $0, RT}'

oder allgemeiner (mit einem regulären Ausdruck für das End-Tag)

gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}'

Testen auf @ terdon's foo.xml:

$ gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}' foo.xml
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
Steeldriver
quelle
0

Wenn Ihre Datei genau so aufgebaut ist, wie Sie es oben gezeigt haben, können Sie die Flags -A (Zeilen nach) und -B (Zeilen vor) für grep verwenden ... zum Beispiel:

$ cat yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
$ grep -A1 -B1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
$ grep -A1 -B1 foo yourFile.txt 
<tag1>
 <tag2>foo</tag2>
</tag1>

Wenn Ihre Version dies grepunterstützt, können Sie auch die einfachere -COption (für den Kontext) verwenden, mit der die umgebenden N Zeilen gedruckt werden:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
Jamespfinn
quelle
Danke aber nein. Dies ist nur ein Beispiel und das echte Zeug sieht ziemlich unvorhersehbar aus ;-)
Den
1
Das ist kein Tag mit foo darin zu finden, das ist nur foo zu finden und Kontextlinien anzuzeigen
Nathan Wallace
@ NathanWallace ja, genau darum hat das OP gebeten, diese Antwort funktioniert in dem in der Frage angegebenen Fall einwandfrei.
Terdon
@terdon das ist überhaupt nicht das, was die Frage stellt. Zitat: "Ich würde gerne das <tag1> lesen, wenn es irgendwo foo enthält." Diese Lösung lautet wie folgt: "Ich möchte 'foo' und 1 Kontextzeile lesen, unabhängig davon, wo 'foo' angezeigt wird." Nach Ihrer Logik wäre eine ebenso gültige Antwort auf diese Frage tail -3 input_file.xml. Ja, es funktioniert für dieses spezielle Beispiel, aber es ist keine hilfreiche Antwort auf die Frage.
Nathan Wallace
@ NathanWallace Mein Punkt war, dass das OP ausdrücklich angegeben hat, dass dies kein gültiges XML-Format ist. In diesem Fall hätte es durchaus ausreichen können, die N Zeilen um die Zeichenfolge zu drucken, nach der das OP sucht. Mit den verfügbaren Informationen war diese Antwort anständig genug.
Terdon