Software

sed: Suche nach Mustern über mehrere Zeilen

Die Suche nach RegExes in einzelnen Zeilen ist simpel - aber über mehrere Zeilen? Puh ...

sed ist auf Zeilen angelegt und in der Regel kommt man damit wunderbar klar. sed ist aber auch darauf ausgelegt, unfassbar viel zu können, wenn man etwas über die Standards hinaus geht. Da ist so eine simple Aufgabe wie die Suche nach zwei bestimmten aufeinanderfolgenden Zeilen gar nicht so trivial – zumindest sieht der Befehl gemein aus. Aber hey, sonst wäre es auch nicht sed ;) Das wirklich Gemeine steht aber erst ganz am Ende!

Simple Aufgabenstellung

Kurz vorweg: Wir haben auch eine kleine Einführung in reguläre Ausdrücke – versprochen, die ist weniger dröge als dieser Artikel ;)

Als Beispiel soll hier mal die Suche nach einem durchaus gängigen Muster sein: Bilder in Asciidoc-Dateien, samt Bildunterschriften (BUs). In Asciidoc werden Bilder mit „image::“ gesetzt und BUs in der Zeile direkt darüber mit einem führenden Punkt (.). Und all diese Vorkommen von zwei Zeilen sollen gefunden werden.

Das zu findende Muster sieht in der Quelle also so aus:

.Hier steht irgendein Text
image::bild.jpg oder sonstwas - Hauptsache "image"

Hier mal der Inhalt einer Datei test.txt:

foo
bar
foobar
peter wohnt hier nicht
.Eine Zeile mit einem Punkt am Anfnag              // Soll gefunden werden
image::Eine Zeile mit **image::** am Anfang          // Soll gefunden werden
hier ein bild:
image::nein doch oh
foo
bar
foobar

Würde man darin nun nur die image-Zeilen finden wollen, sähe das Ganze recht simpel aus:

sed -n '/^image/ p' test.txt

sed sucht also einfach Zeilen mit image am Anfang (^) und druckt (p) sie aus. Das -n sorgt dafür, dass nicht der gesamte Inhalt der Datei ausgegeben wird.

Wenn aber nun nur Bilder mit BUs gefunden werden sollen, sieht es vielleicht für den einen oder anderen ein wenig weniger intuitiv aus:

sed -n '/^\./ { N ; /\nimage/ p }' test.txt

Da etwas komplexer, hier mal Schritt für Schritt:

  • -n unterdrückt wieder die Ausgabe aller Zeilen.
  • ^. sucht nach Zeilen mit einem Punkt am Anfang.
  • –> Nun ist diese Zeile im aktiven Puffer.
  • { startet ein weiteres Kommando, das auf den aktiven Puffer angewendet wird.
  • N fügt die nächste Zeile dem aktiven Puffer hinzu, wenn …
  • … via \nimage ein Umbruch (\n) gefolgt von „image“ im aktiven Puffer gefunden wird.
  • p druckt dann den aktiven Puffer aus.
  • } beendet das sed-Kommando.

Im Grunde handelt es sich also um zwei verschachtelte sed-Abfragen, die Syntax ist beide Male /Suche nach Muster/Kommando.

Kleine Erweiterung

Jens hat im Kommentar unten eine hübsche Ergänzung ins Spiel gebracht, denn wenn zwei Zeilen nacheinander mit einem Punkt anfangen, bricht das Skript. Das zusätzliche Kommando D schafft Abhilfe:

sed -n '/^\./ { N ; /\nimage/ p ; D}' test.txt

Die Aktion ist ein wenig erkläfungebedürftig: Wenn im Puffer ein Umbruch ist, löscht D den Text bis zum ersten Umbruch und startet den Zyklus erneut. Hier mal ein Beispieltext:

start
.Zeile 1 mit Punkt
.Zeile 2 mit Punkt
image foobar
ende

Der Ablauf von sed sieht dann verkürzt so aus:

Zyklus 1:
    Pattern: start --> passt nicht zum Gesuchten
Ende Zyklus 1

Zyklus 2:
    Pattern: .Zeile 1 mit Punkt --> passt, Kommando N ergänzt zu:
    Pattern 2: .Zeile 1 mit Punkt\n.Zeile 2 mit Punkt --> passt nicht, Kommando D löscht bis zum ersten Umbruch:
    Pattern 3: .Zeile 2 mit Punkt --> passt, Kommando N ergänzt zu:
    Pattern 4: .Zeile 2 mit Punkt\nimage foobar --> passt, Kommando p gibt den Puffer aus
    D wird erneut ausgeführt und löscht bis zum ersten Umbruch:
    Pattern 5: image foobar --> passt nicht, Zyklus bricht ab
Ende Zyklus 2

Der Durchlauf ist immer noch ein wenig verkürzt, Ihr könnt Euch die sed-Abarbeitung Schritt für Schritt mit der Option –debug anschauen:

SED PROGRAM:
  /^\\./ {
    N
    /\nimage/ p
    D
  }
INPUT:   'zeilen' line 1
PATTERN: start
COMMAND: /^\\./ {
COMMAND: }
END-OF-CYCLE:
INPUT:   'zeilen' line 2
PATTERN: .Zeile 1 mit Punkt
COMMAND: /^\\./ {
COMMAND:   N
PATTERN: .Zeile 1 mit Punkt\n.Zeile 2 mit Punkt
COMMAND:   /\nimage/ p
COMMAND:   D
PATTERN: .Zeile 2 mit Punkt
COMMAND:   /^\\./ {
COMMAND:     N
PATTERN: .Zeile 2 mit Punkt\nimage foobar
COMMAND:     /\nimage/ p
.Zeile 2 mit Punkt
image foobar
COMMAND:     D
PATTERN: image foobar
COMMAND:     /^\\./ {
COMMAND:     }
END-OF-CYCLE:
INPUT:   'zeilen' line 5
PATTERN: ende
COMMAND:     /^\\./ {
COMMAND:     }
END-OF-CYCLE:
INPUT:   'zeilen' line 6
PATTERN:
COMMAND:     /^\\./ {
COMMAND:     }
END-OF-CYCLE:

Weitere Hilfe und Gemeinheit

In sed-Sprech heißt das alles übrigens bisweilen etwas anders und im Detail ist es bisweilen noch ein wenig komplizierter, natürlich ;) Bei Stack Exchange gibt es ein schönes ähnliches Beispiel mit weiteren Erläuterungen und das sed-Handbuch ist sowieso äußerst hilfreich. Direkt im Terminal könnt Ihr unsere hauseigene Terminal-Hilfe cli.help nutzen – Hilfe für den Terminal direkt im Terminal, zum Beispiel mit:

curl cli.help/regex
curl cli.help/sed

Dieses konkrete sed-Problem ist dort zwar nicht zu finden, aber nützliche Basics für den Fall, dass Ihr mal wieder die Syntax vergessen habt. Und jetzt am Ende wird es nochmal super gemein:

rg --multiline "^\..*\nimage.*" test.txt

Ripgrep erledigt diese Aufgabe vielleicht ein klein wenig verständlicher? :O Man sollte rg definitiv auf dem Schirm haben, zumal es auch Ersetzungen beherrscht. Aber die sed-Lösung hat natürlich ihre Berechtigung, sei es fürs Scripting oder einfach nur, um das eigene Tool-Arsenal schlank zu halten.

Und noch eine Kleinigeit zum Mitnehmen: am Anfang einer Zeile seht Ihr bei ripgrep und bei sed in der Form \nfoobar – also Umbruch foobar. Bei sed seht Ihr aber zusätzlich ^foobar. Praktisch läuft das natürlich auf dasselbe Ergebnis hinaus, denn den Anfang einer Zeile leitet nun mal ein Umbruch ein. Nun, außer bei Zeile 1, aber dann bräuchte es ja keiner Multiline-Statements, gell?!

Mehr zu CLI-Themen.

Mirco Lang

Freier Journalist, Exil-Sauerländer, (ziemlich alter) Skateboarder, Dipl.-Inf.-Wirt, Einzelhandelskaufmann, Open-Source-Nerd, Checkmk-Handbuchschreiber. Ex-Saturn'ler, Ex-Data-Becker'ler, Ex-BSI'ler. Computer-Erstkontakt: ca. 1982 - der C64 des großen Bruders eines Freunds. Wenn Ihr hier mehr über Open Source, Linux und Bastelkram lesen und Tutonaut unterstützen möchtet: Über Kaffeesponsoring via Paypal.freue ich mich immer. Schon mal im Voraus: Danke! Nicht verpassen: cli.help

2 Kommentare

  1. Das Skript funktioniert nicht mehr korrekt, wenn das Muster der ersten Zeile mehrfach direkt aufeinander folgt, im Beispiel also mehre Zeilen mit einem Punkt direkt nacheinander stehen. Abhilfe schafft, mit der zweiten Zeile einen neuen Zyklus innerhalb von sed mit dem Kommando „D“ zu beginnen:

    sed -n '/^\./ { N ; /\nimage/ p ; D}' test.txt
    

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.

Empfohlene Beiträge

Schaltfläche "Zurück zum Anfang"
Tutonaut.de