Stream Editor in der Praxis nutzen Texteditor sed – mehr als nur Suchen und Ersetzen

Von Mirco Lang

Der Texteditor Stream Editor, kurz sed, ist omnipräsent – allerdings fast immer beschränkt auf das Substitute-Kommando. Das verwundert nicht, immerhin beschreiben die Autoren diese Funktion doch selbst Schweizer Taschenmesser. Aber sed ist so viel mehr.

Mit dem Stream Editor lässt sich deutlich mehr Struktur in Dokumente und dergleichen bringen.
Mit dem Stream Editor lässt sich deutlich mehr Struktur in Dokumente und dergleichen bringen.
(© artfoto53 – stock.adobe.com)

Sed-Kommandos sind nicht hübsch anzusehen, selbst wenn auf komplexe reguläre Ausdrücke verzichtet wird – Beispiel gefällig?

sed -n -e '/X/ { H ; d } ; /Zeile/ { p ; d } ; G ; p' test

Kryptisch, ja – aber auch nützlich! Mehr dazu später, aber so viel: Hier geht es um eine sortierte Ausgabe von Zeilen. Wer die sed-Anweisung aus dem Effeff versteht, kann sich an dieser Stelle direkt auf vordergründig weniger kryptische Anweisungen wie „You lying stupid idiot! stürzen.

Im Grunde ist sed gar nicht sonderlich kompliziert, zumindest, wenn man bei den Basics und dem Suchen-und-Ersetzen-Befehl (s für Substitut) bleibt: sed arbeitet Zeile für Zeile eines Dokuments ab, sucht in jeder Zeile nach einem gegebenen Muster und ersetzt dieses durch einen gegebenen Text – was zum altbekannten Kommando führt:

sed s/MUSTER/KOMMANDO
sed s/foo/bar

Und möchte man nicht „foo“ durch „bar“ ersetzen, sondern zu „foobar“ ergänzen, kommen Dinge aus der Welt der regulären Ausdrücke hinzu:

sed s/(foo)/\1bar

Aufgrund der Klammern wird „foo“ als Matchgruppe betrachtet und in der Variablen „1“ gespeichert, die im Kommando-Teil mit „\1“ wieder genutzt werden kann. Solche Fälle machen vermutlich 70 Prozent aller sed-Vorkommen im realen Leben aus – wobei die Komplexität der regulären Ausdrücke kaum Grenzen kennt.

Von der Syntax mal abgesehen, ist die Arbeitsweise (zumindest im Abstrakten) ziemlich einfach zu verstehen – Suchen-und-Ersetzen-Funktionen gibt es schließlich sogar in Word & Co. Über die interne Funktionsweise von sed sagt das aber wenig aus; eine simple Sortierung hilft da schon eher.

Sortieren mit sed

Um sed produktiv einsetzen zu können, muss man die Logik dahinter verstehen. Sed arbeitet zeilenweise, in Zyklen und mit zwei Puffern. Zunächst liest sed eine Zeile ein (Input), speichert den deren Inhalt im so genannten Pattern Space (Puffer 1) und führt dann die gegebenen Kommandos auf diesen Pattern Space aus. Damit ist ein Zyklus beendet und mit der nächsten Zeile beginnt das Prozedere von vorn.

Standardmäßig gibt sed jeden Pattern Space (also jede Zeile) aus, egal, ob in dieser das gewünschte Muster vorkommt oder nicht. Daher starten sehr viele sed-Kommandos mit dem Kommandozeilenparameter „-n“, der dieses Verhalten unterdrückt. Gewünschte Ausgaben müssen dann explizit mit dem Kommando „p“ (print) provoziert werden.

Möchten Sie als zum Beispiel nur und alle Zeilen eines Dokuments „test“ ausgeben, die mit „Zeile“ beginnen, sieht sed auf einmal ziemlich freundlich aus:

sed -n '/Zeile/ p' test

Wie beim bekannten s-Kommando leitet der Slash die Suche nach dem Muster (hier „Zeile“) ein und nach dem zweiten Slash folgt das Kommando, hier schlichtes Drucken (p für print).

Zeilen mit gefundenem Muster können auch mehrere Anweisungen bekommen: Angenommen, es sollen die Zeilen mit dem Wort „Zeile“ plus die jeweils nächste Zeile gedruckt werden. Dann muss dem Pattern Space vor dem Druck noch diese nächste Zeile übergeben werden:

sed -n '/Zeile/ { N ; p }' test

Mehrere Kommandos stehen schlicht in geschweiften Klammern und werden durch Semikola getrennt. Neu ist hier nur das „N“. Nach der Suche nach „Zeile“ entspricht der Pattern Space dieser einen Zeile. Das Kommando N fügt diesem Pattern Space dann ein Newline (\n) hinzu und anschließend die nächste Zeile (Input) der übergebenen Datei. Über p wird dann der ganze Pattern Space ausgedruckt, in der Art:

[Zyklus 1]
Zeile 1
irgendetwas
[Zyklus 2]
Zeile 2
blabla

Fällt Ihnen etwas auf? Sed arbeitet normalerweise auf Zeilenebene – nun stehen im Pattern Space für den aktuellen Zyklus aber plötzlich zwei Zeilen zur Verfügung. Das heißt auch, dass sich über solch ein Konstrukt Muster finden lassen, die sich über zwei oder mehr Zeilen erstrecken.

Sollen solche „Doppel-Zeilen“ etwa nur ausgegeben werden, wenn in der zweiten Zeile „irgendetwas“ steht, ließe sich die Bedingung ganz simpel realisieren:

sed -n '/Zeile/ { N ; /irgendetwas/ p }' test

Hier ist lediglich „/irgendetwas/“ als Filter/Suche/Bedingung hinzugekommen: Wird „irgendetwas“ im neuen Zweizeiligen Pattern Space gefunden, wird er ausgedruckt, falls nicht, endet der Zyklus. Die Ausgabe wäre folglich:

Jetzt Newsletter abonnieren

Täglich die wichtigsten Infos zu Softwareentwicklung und DevOps

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung.

Aufklappen für Details zu Ihrer Einwilligung
Zeile 1
irgendetwas

Man kann sed-Anweisungen also im Grunde oft als if-Abfragen begreifen:

if foobar; then echo wahr; fi
sed -n '/foobar/ a wahr'

Das Kommando „a“ fügt schlicht einen beliebigen Text an (append) – beziehungsweise gibt nur diesen Text aus, da weitere Ausgaben über „-n“ unterbunden sind.

Der Pattern Space lässt sich so also erweitern, als Ganzes manipulieren und dann ausgeben – jeweils bezogen auf einen Zyklus. Fehlt noch der zweite Puffer, der Hold Space, der Daten über Zyklen hinweg speichern kann.

Der Hold Space

Der zweite Puffer namens Hold Space speichert Daten nicht für einen Zyklus, sondern die ganze Abarbeitung des sed-Statements. Und natürlich können Daten vom Pattern Space in den Hold Space und umgekehrt kopiert werden.

Auch hier leistet das obige Beispiel gute Dienste. Wegen der Komplexität hier aber zunächst der Inhalt einer Datei „test“:

Zeile 1: foobar
Zeile 2: hallo welt
Zeile 3: foo
Zeile 4: bar
X1
Zeile 5: ende
X2
     [Anmerkung: Diese leere letzte Zeile gehört zum Inhalt.]

Die Aufgabe lautet nun: Erst alle Zeilen ausgeben, die „Zeile“ enthalten, dann die X-Zeilen. Konzeptionell klingt das recht simpel: Zeilen mit „Zeile“ werden wie gewohnt ausgegeben, X-Zeilen landen im Hold Space und werden am Ende gesammelt in den Pattern Space kopiert, der dann wiederum ausgegeben werden kann (ein Kommando „Drucke Hold Space“ gibt es nicht).

Praktisch bedeutet das drei neue Kommandos, nämlich „d“ (delete), „H“ (Pattern Space an Hold Space anfügen) und „G“ (Hold Space an Pattern Space anfügen). Etwas genauer: H und G fügen zunächst ein Newline und dann den jeweiligen Puffer-Inhalt hinzu.

sed -n '/X/ { H ; d } ; /Zeile/ { p ; d } ; G ; p' test

Zunächst prüft das sed-Kommando hier also für jede Zeile, ob im Input ein „X“ vorkommt. Falls ja, wird diese Zeile zunächst dem Hold Space hinzugefügt (H) und dann aus dem Pattern Space gelöscht (d). Dann wird geprüft, ob im Input „Zeile“ vorkommt. Falls ja, wird der Pattern Space ausgedruckt und anschließend ebenfalls gelöscht.

Sobald eine Zeile keine der beiden Bedingungen erfüllt, hier also die letzte leere Zeile, werden die Inhalte aus dem Hold Space (also alle X-Zeilen) in den Pattern Space kopiert (G), welcher dann gedruckt wird (p).

Das Ergebnis:

Zeile 1: foobar
Zeile 2: hallo welt
Zeile 3: foo
Zeile 4: bar
Zeile 5: ende
X1
X2

Das ist nun ein sehr einfaches (fragiles und konstruiertes) Beispiel, um die Funktionsweise der beiden Puffer und der zyklischen Verarbeitung zu veranschaulichen (ein Schelm wer meint, die sed-Macher selbst hätten Verständnisprobleme, nur weil das Puffer-Kapitel in der offiziellen Dokumentation lediglich ein „TODO“ zeigt – seit nunmehr 47 Jahren.

In der Praxis würde solch eine Sortierung mit sed mindestens noch allerlei reguläre Ausdrücke verschlingen, bis hin zur völligen Unlesbarkeit. Und vielleicht würde man manche Dinge ganz anders lösen. Aber mit dem Verständnis der Puffer und der sed-Kommando-Syntax hat man so zumindest zwei sauber trennbare Baustellen, schließlich sind reguläre Ausdrücke alles andere als sed-spezifisch.

Sed hat mittlerweile auch eine eingebaute Debug-Funktion, die sich einfach per „--debug“-Schalter aktivieren lässt. Die Ausgabe sieht dann so aus, hier ein kompletter Zyklus für eine der X-Zeilen aus dem obigen Beispiel:

INPUT: 'test' line 7
PATTERN: X2
COMMAND: /X/ {
COMMAND: H
HOLD: \nX1\nX2
COMMAND: d
END-OF-CYCLE:

Input ist hier also die 7. Zeile, im Pattern Space liegt „X2“, das Kommando „/X/“ sucht nach einem X im Pattern Space, da dieses gefunden wurde wird „H“ ausgeführt, was zu einem Hold Space von „\nX1\nX2“ führt – bevor zum Schluss mittels „d“ der Pattern Space gelöscht wird. Mit der Debug-Funktion lässt sich wunderbar arbeiten, sofern die grundlegende sed-Arbeitsweise verstanden ist.

Ein kleiner Bonus

Hier noch ein kleiner Ausflug auf einen Nebenschauplatz, einfach, weil es den praktischen Einstieg ungemein erleichtert: Es ist gute Sitte, dass Dateien mit einer leeren Zeile enden. Sich auf solche zu verlassen ist vielleicht nicht immer die beste Idee; wenn man aber zum Beispiel sicher ist, dass im obigen Beispiel nur Zeilen mit „Zeile“ oder „X“ vorkommen, kann man die letzte leere Zeile durchaus verwenden, um ein letztes Kommando auszuführen, wie eben etwa das finale Drucken des Pattern Spaces. Also gilt es zu prüfen, ob eine leere Zeile am Ende steht und falls nicht, muss sie hinzugefügt werden:

if [ $(tail -c1 test; echo x) != $'\nx' ]; then echo "" >> test; fi

Die eckigen Klammern stehen für das test-Kommando – und wenn der Ausdruck wahr ist, fügt echo eine Leerzeile an die Datei „test“ an. Innerhalb der Klammern wird über tail das letzte Zeichen (-c1) der Datei „test“ plus „x“ ausgegeben. Dann wird geprüft, ob dieser Ausdruck nicht gleich „\nx“ (also Newline plus x) ist.

Man möchte meinen, sed würde einen eleganteren Weg dafür bereitstellen, tut es aber nicht. Wenn Sie Dateien nicht verändern wollen, können Sie dieses Hilfsmittel natürlich auch für eine Pipe nutzen, wobei cat einfach den Inhalt der Datei und das echo-Kommando konkateniert:

if [ $(tail -c1 test; echo x) != $'\nx' ]; then echo "" ; fi | cat test - | sed-KOMMANDO

(ID:47970449)