
Im ersten Teil habe ich anhand vieler Beispiele die absoluten Basics von Sed gezeigt. In diesem zweiten Teil möchte ich ein bisschen tiefer einsteigen, einen kurzen Blick auf die genaue Funktionsweise von Sed werfen und eine Einführung in die zur Verfügung stehenden Puffer, die gleichzeitige Betrachtung mehrerer Zeilen und Kontrollflussmöglichkeiten geben.
Execution Cycle von Sed
Wir haben bereits im ersten Teil gelernt, dass Sed Texte zeilenweise einliest. Auf jede Zeile werden die mitgegebenen Commands angewandt. Ein Command beinhaltet bis zu zwei Addresses. Viele Addresses haben wir im ersten Teil kennengelernt, allerdings nicht unter diesem Namen. Addresses stellen eine Bedingung dar, und der Command wird für die jeweilige Zeile nur ausgeführt, wenn die Bedingung erfüllt ist. Der Aufruf sed -n '4,6p' lang.txt
aus dem ersten Teil enthält zwei Addresses, die zusammen eine Range bilden. Nur wenn die aktuelle Zeile eine der Zeilennummern 4, 5 und 6 trägt, wird der Command p
ausgeführt.
Mehrere Commands können durch Semikolon getrennt hintereinander weggeschrieben werden. Alternativ können Commands auch separat über das Flag -e
mitgegeben werden. Die folgenden drei Aufrufe sind äquivalent:
sed -n '1p;4p;5p' lang.txt # Trennung durch Semikolon
sed -n -e '1p' -e '4p' -e '5p' lang.txt # Trennung durch -e
sed -n -e '1p' -e '4p;5p' lang.txt # beides kombiniert
Das Einlesen jeder Zeile stellt eine Iteration im Execution Cycle von Sed dar. Dabei geschieht Folgendes:
- Sed liest die nächste Zeile Text ein, entfernt den Zeilenumbruch am Ende der Zeile und schreibt die Zeile in den Pattern Space
- Die Addresses werden ausgewertet und entscheiden darüber, ob die damit verknüpften Commands ausgeführt werden
- Nach Abarbeitung aller Commands wird, falls nicht durch
-n
unterdrückt, der Inhalt des Pattern Spaces plus des zuvor entfernten Zeilenumbruches ausgegeben und die nächste Iteration gestartet - Der Inhalt des Pattern Spaces wird bis auf ein paar durch bestimmte Commands herbeigeführte Ausnahmen zwischen den Iterationen zurückgesetzt
Hold Space
Der Hold Buffer oder Hold Space ist ein separater Puffer, der frei verwendet werden kann und zwischen Iterationen nicht zurückgesetzt wird. Zur Arbeit mit dem Hold Space existieren fünf wesentliche Commands:
h
kopiert den Inhalt des Pattern Spaces in den Hold Space (insert).H
hängt den Inhalt des Pattern Spaces an den Inhalt des Hold Spaces an (append).g
kopiert den Inhalt des Hold Spaces in den Pattern Space (insert).G
hängt den Inhalt des Hold Spaces an den Inhalt des Pattern Spaces an (append).x
vertauscht den Inhalt von Pattern Space und Hold Space (swap).
Was lässt sich mit diesen Möglichkeiten erreichen? Nun zum Beispiel kann man den Inhalt eines Textes in verkehrter Reihenfolge ausgeben, also die letzte Zeile zuerst und die erste Zeile zum Schluss. Das lässt sich z.B. so erreichen:
for i in {1..10}; do echo $i; done > num.txt # erzeugt eine Datei mit den Zahlen 1 bis 10 in je einer Zeile
sed '1!G;h;$!d' num.txt
Warum führt der Sed-Ausdruck dazu, dass die Zahlen 1 bis 10 in absteigender Folge ausgegeben werden? Aus dem ersten Teil noch nicht bekannt ist der !
-Operator. Hierbei handelt es sich um eine Negierung. Es ist also Folgendes festzuhalten:
G
wird für alle Zeilen mit Ausnahme der ersten ausgeführt.h
wird für alle Zeilen ausgeführt.d
wird für alle Zeilen mit Ausnahme der letzten ausgeführt.
Was nun passiert kann man leicht Schritt für Schritt im Kopf durchspielen:
- Die erste Zeile wird in den Pattern Space geschrieben. Der Inhalt des Pattern Spaces ist nun
1
. Der Pattern Space wird in den Hold Space kopiert. Der Inhalt des Pattern Spaces wird nicht ausgegeben. - Die Zeile zwei wird in den Pattern Space geschrieben. Der Inhalt des Pattern Spaces ist nun
2
. Der Inhalt des Hold Spaces wird an den Inhalt des Pattern Spaces angehängt. Der Inhalt des Pattern Spaces ist nun2\n1
. Der Pattern Space wird in den Hold Space kopiert. Der Inhalt des Pattern Spaces wird nicht ausgegeben. - Die Befehle wiederholen sich für die Zeilen drei bis neun. In der neunten Zeile ist der Inhalt des Pattern Spaces
9\n8\n7\n6\n5\n4\n3\n2\n1
. Dieser wird in den Hold Space kopiert und nicht ausgegeben. - In Zeile zehn wird an den Inhalt des Pattern Spaces,
10
, der Inhalt des Hold Spaces angehängt. Der Inhalt des Pattern Spaces wird nicht gelöscht, was zur Ausgabe von10\n9\n8\n7\n6\n5\n4\n3\n2\n1
führt.
Gleichzeitige Betrachtung mehrerer Zeilen
Der Hold Space ist ein geeignetes Mittel um mehrere Zeilen gleichzeitig zu verarbeiten. Um dies zu veranschaulichen, definiere ich zunächst eine neue Textdatei, die etwas komplizierter strukturiert ist als der Inhalt von lang.txt
aus dem ersten Teil, ich nenne sie lang3.txt
:
Sprache: Python
Erfinder: Guido van Rossum
Jahr: 1991
Sprache: Java
Erfinder: James Gosling
Jahr: 1995
Typisierung: statisch
Paradigmen: imperativ, objektorientiert, funktional, aspektorientiert
Sprache: Perl
Erfinder: Larry Wall
Jahr: 1987
Webseite: https://www.perl.org
Sprache: JavaScript
Erfinder: Brendan Eich
Jahr: 1995
In dieser Datei sind vier Programmiersprachen beschrieben, allerdings nicht in jeweils einer Zeile, sondern über eine variable Anzahl an Zeilen hinweg, durch Leerzeilen voneinander getrennt. Ich nutze folgenden Befehl, um alle Angaben zu den Sprachen wieder in einer Zeile pro Sprache auszugeben:
sed '/./{H;$!d;};/./!{x;s/\n/, /g;s/^..//}' lang3.txt
/./
ist eine Address, um auf Zeilen zu filtern, die nicht leer sind././!
filtert auf Leerzeilen././{H;$!d;}
die geschweiften Klammern ermöglichen es, dass sich eine Address auf mehrere Commands bezieht; damit lassen sich auch beliebig viele Addresses einem Command zuordnen, hier sowohl/./
als auch$!
für den Commandd
.s/\n/, /g
sollte aus dem ersten Teil ableitbar sein, alle Zeilenumbrüche werden durch Komma plus Leerzeichen ersetzt.s/^..//
dieser Ersetzungsausdruck entfernt die ersten beiden Zeichen einer Zeile.
Was passiert hier?
- Die ersten drei Zeilen werden dem Hold Space per
H
hinzugefügt, da es sich um Zeilen mit Inhalt zur Sprache Python handelt. - Der Befehl
d
verhindert, dass diese Zeilen ausgegeben werden. - In der ersten Leerzeile, zwischen den Sprachen Python und Java, werden Pattern Space und Hold Space vertauscht.
- Der aus dem Hold Space übernommene Inhalt des Pattern Spaces wird nun durch Ersetzungen formatiert, zunächst werden die Zeilenumbrüche ersetzt, so dass dieser Absatz in einer Zeile dargestellt wird, dann werden die ersten beiden Zeichen entfernt.
- Da die Ausgabe nicht mit
-n
unterdrückt wird, werden die formatierten Angaben zur Sprache nun ausgegeben. - In der nächsten Zeile wiederholt sich das Spiel für den zweiten Block (Java), anschließend auch für den dritten (Perl) und den letzten (JavaScript).
Das Ergebnis des Aufrufes ist wie folgt:
Sprache: Python, Erfinder: Guido van Rossum, Jahr: 1991
Sprache: Java, Erfinder: James Gosling, Jahr: 1995, Typisierung: statisch, Paradigmen: imperativ, objektorientiert, funktional, aspektorientiert
Sprache: Perl, Erfinder: Larry Wall, Jahr: 1987, Webseite: https://www.perl.org
Sprache: JavaScript, Erfinder: Brendan Eich, Jahr: 1995
Weitere Befehle zum Ausprobieren und Rumspielen
Dieser Befehl gibt nur die Absätze derjenigen Sprachen aus, die 1995 erschienen sind: sed '/./{H;$!d;};x;/1995/!d' lang.txt
Dieser Befehl gibt die Erfinder der Sprachen aus: sed -n '/Jahr/{g;1!p;};h' lang3.txt
Dieser Befehl entfernt die letzte Zeile jedes Absatzes: sed '/./!{h;d;};/./x' lang3.txt
Die Befehle n und N
n
kopiert die nächste Zeile in den Pattern Space (insert)N
hängt die nächste Zeile an den Inhalt des Pattern Spaces an (append)
Quizfrage: Was tut der Aufruf sed 'n' num.txt
? Antwort: Er gibt den Inhalt der Datei unverändert aus, allerdings anders als man möglicherweise annimmt. Durch den Command n
wird der Zeiger auf den Eingabetext weiter gesetzt, d.h. die nachgeladene Zeile wird in der nächsten Iteration nicht erneut eingelesen. Deutlich machen dies die Aufrufe sed -n 'p;n' num.txt
und sed -n 'n;p' num.txt
, die jeweils nur die ungeraden bzw. geraden Zeilen der Textdatei ausgeben.
Wir können den Befehl n
beispielsweise verwenden, um die erste Zeile eines Absatzes (nach einer Leerzeile) zu entfernen:
sed '1d;/./!{n;d}' lang3.txt
Was passiert hier? Die erste Zeile der Datei wird gelöscht, also nicht ausgegeben. Für jede Leerzeile wird unmittelbar die nächste Zeile eingelesen und ebenfalls gelöscht.
Wie kann man die ersten beiden Zeilen eines Absatzes löschen? Die logische Schlussfolgerung könnte auf den ersten Blick sed '1,2{d};/./!{n;d;n;d}' lang3.txt
sein. Als Erklärung, warum dies nicht funktioniert, muss ich ein kleines Detail nachreichen. Wir haben zwar d
schon oberflächlich in Teil 1 kennengelernt, aber noch nicht erfahren, dass d
unmittelbar die aktuelle Iteration beendet. Das zweite n;d
ist also unerreichbarer Code. Es gibt aber eine Lösung, und da kommt N
ins Spiel:
sed '1,2d;/./!{n;N;d;}' lang3.txt
Der Aufruf unterscheidet sich vom vorherigen darin, dass die ersten beiden Zeilen gelöscht werden (gegenüber nur der ersten) und nach einer Leerzeile nicht nur eine Zeile nachgeladen wird, sondern zwei und zwar beide zur gleichen Zeit in den Pattern Space. Das erste n
muss übrigens klein bleiben, sonst geht die Leerzeile als Trenner der verschiedenen Sprachen verloren.
Kontrollflusselemente
Diesen letzten Part reiße ich nur kurz an, da ich ihn schwer zu durchdringen finde und es mir echt schwer fällt, Anwendungsbeispiele zu benennen, wo ich dafür eher Sed als Awk, eine Kombination verschiedener GNU-Programme oder eine höhere Programmiersprache verwenden würde.
Sed kann Schleifen via Labels und drei verschiedener Commands abbilden:
b
springt anhand einer Address innerhalb der gleichen Iteration zu einem Label.t
springt innerhalb der gleichen Iteration zu einem Label, wenn es in der aktuellen Iteration eine erfolgreiche Ersetzung gegeben hat.T
springt innerhalb der gleichen Iteration zu einem Label, wenn es in der aktuellen Iteration keine erfolgreiche Ersetzung gegeben hat.
Wann setzt man Schleifen ein? Nach meinem Verständnis lohnt sich der Einsatz einer Schleife, wenn man eine Zeile mehrfach verarbeiten möchte. Dies kann der Fall sein, wenn man es mit einer komplexen Struktur zu tun hat, etwa mit verschachtelten Elementen wie in HTML oder JSON, die sich mit einer einfach ausgeführten Ersetzung nicht ausreichend bearbeiten lassen.
Ein weiterer Anwendungsfall ist eine unterschiedliche Behandlung nachfolgender Zeilen nach einem bestimmten Ereignis. Mit einer Schleife kann man z.B. erreichen, dass ein Befehl auf einen Treffer innerhalb einer Datei nur ein einziges Mal angewandt wird, und nicht auf alle passenden Zeilen.
Das nachfolgende und letzte Beispiel in meinem zweiteiligen Beitrag zu Sed zeigt die Anwendung einer Schleife und nebenbei auch noch ein paar andere spannende Dinge:
sed -e '/1995/ a (die erste Sprache in der Liste, die 1995 erschien)' -e ':foo n; t foo' lang3.txt
a
ist ein Command um Inhalt an eine Zeile anzuhängen (append).i
(insert) undc
(change) können auf die gleiche Weise wiea
verwendet werden, fügen allerdings Inhalt vor der aktuellen Zeile ein (insert) bzw. ersetzen diese (change).- der Text nach dem
a
kann nicht durch ein Semikolon terminiert werden, was für mich einen guten Anwendungsfall für-e
statt;
darstellt. Grundsätzlich kann man mit-e
auch gut verwenden um in langen Sed-Skripten logische Gruppen von Befehlen voneinander zu trennen, was dieses Beispiel auch tut. :foo
ist ein Label, das aus beliebigen alphanumerischen Zeichen und auch einigen Sonderzeichen wie Unterstrichen und Punkten bestehen darf.t
haben wir oben bereits kennengelernt, unmittelbar dahinter wird die Sprungmarke (foo) angegeben. Der Effekt dieser Schleife ist, dass die Zeile die erste Sprache… nur nach dem ersten Treffer für/1995/
eingefügt wird, die restlichen Zeilen werden innerhalb der Schleife verarbeitet.
Fazit
Man merkt Sed sein hohes Alter von stolzen fast 50 Jahren an. Sed ist älter als Awk und die meisten höheren Programmiersprachen. Die Syntax ist nicht einsteigerfreundlich und ohne tiefes Verständnis der Grammatik kann man schnell die falschen Schlüsse ziehen. Die meisten Informationen aus diesem zweiten Teil meines Beitrags fallen wohl eher unter nett zu wissen als das muss ich mal ausprobieren. Bei allem was über einfache Filter und Ersetzungen hinaus geht, würde ich mir dreimal überlegen ob ich dafür Sed einsetze, vor allem wenn ich es im Team verwende und andere den Code mit verantworten und weiterentwickeln. Meist gibt es angemessenere Alternativen.
Damit endet meine Einführung in Sed. Wer sein oder ihr Wissen weiter vertiefen möchte, dem empfehle ich die unten verlinkten Ressourcen.
- Dokumentation von GNU-Sed - Hervorragendes tiefgehendes Sed-Tutorial - Weiteres sehr gutes Sed-Tutorial mit vielen Beispiel-Aufrufen