Sed Teil 1

Lesezeit: 14 Minuten

Sed steht für Stream Editor und ist ein Unix-Werkzeug zur Verarbeitung von Text. Es kann Text aus Dateien und Pipes (inklusive stdin) einlesen und wird üblicherweise dafür genutzt, Text anhand von Mustern zu filtern oder zu ersetzen.

Sed ist in den Jahren 1973 und 1974 in den Bell Labs von Lee E. McMahon entwickelt worden. Das Tool verwendet einen ähnlichen Befehlssatz wie der zeilenorientierte Editor ed.

Einordnung

Genau wie Awk sehe ich Sed als nützliches Werkzeug im CI/CD-Umfeld und für gelegentliche Textverarbeitungsaufgaben. Ein Sed-Aufruf, der bestimmte Informationen aus einer Datei ausliest, ist wesentlich kompakter als z.B. ein Python-Skript, das den gleichen Zweck erfüllen könnte, und daher für einfache Aufgaben für mich die erste Wahl.

Es sind mehrere Varianten von Sed im Umlauf, die sich mehr oder weniger geringfügig unterscheiden. Mir bekannt sind GNU Sed, welches mit den meisten Linux-Distributionen mit ausgeliefert wird und FreeBSD Sed, welches der Standard auf OS X ist. Die beiden Varianten unterscheiden sich in einigen Details hinsichtlich der Sed-Flags und der Escape-Sequenzen. Für diesen Beitrag verwende ich GNU Sed.

Beispieldatei

Um die Möglichkeiten von Sed zu demonstrieren, verwende ich eine Beispieldatei mit Informationen zu sechs verschiedenen Programmiersprachen. Diese Datei liegt als lang.txt vor:

Python, dynamisch, Guido van Rossum, 1991
Java, statisch, James Gosling, 1995
Ruby, dynamisch, Yukihiro Matsumoto, 1995
Pascal, statisch, Niklaus Wirth, 1970
Perl, dynamisch, Larry Wall, 1987
JavaScript, dynamisch, Brendan Eich, 1995

Hello World

Die grundsätzliche Syntax von Sed zur Verarbeitung von Dateiinhalten ist wie folgt: sed [flags] [commands] [files]

Die Commands (Befehle) sind in Hochkommata eingebettet. Die Befehle werden auf den Inhalt der Files angewandt.

Die einfachste Möglichkeit diese Datei mit Sed zu verarbeiten, ist sie ohne Befehl auszuführen. Der folgende Aufruf führt dazu, dass der Dateiinhalt ausgegeben wird, ähnlich dem Programm cat.

sed '' lang.txt

Sed verarbeitet den Inhalt Zeile für Zeile. Per Default wird dabei jede Zeile selbst ausgegeben. Befehle gibt es in dem Beispiel nicht (Leerstring zwischen den Hochkommata). Die Zieldatei ist lang.txt.

Mit folgendem Aufruf lässt sich das gleiche Ergebnis erreichen:

sed -n 'p' lang.txt

Das Flag -n unterdrückt die Ausgabe jeder Zeile. Die Anweisung p steht für den Print Command, der in dieser Form einfach die gesamte Zeile ausgibt.

Das Ergebnis ist in beiden Fällen identisch mit cat lang.txt.

Zeilen nach Nummern selektieren

Der Print Command, sowie viele weitere Commands, spielen ihre Stärke in Kombination mit weiteren Angaben aus. Der folgende Aufruf gibt nur die Zeilen 1 bis 3 der Datei aus, also die Sprachen Python, Java und Ruby:

sed -n '1,3p' lang.txt

Dieser Aufruf hingegen gibt die Zeilen 4 bis 6 der Datei aus, also die Sprachen Pascal, Perl und JavaScript:

sed -n '4,6p' lang.txt

Mit diesem Befehl wird das Gleiche erreicht, allerdings steht $ für die letzte Zeile einer Datei:

sed -n '4,$p' lang.txt

Der nachfolgende Aufruf gibt die Zeilen 1, 4 und 5 aus, also die Sprachen Python, Pascal und Perl:

sed -n '1p;4p;5p' lang.txt

Zeilen nach Mustern selektieren

Anstelle von Zeilennummern können auch auch Muster angegeben werden, um zu bestimmen, welche Zeilen einer Datei ausgegeben werden. Muster werden durch Forward Slashes begrenzt. Mit diesem Aufruf werden die Sprachen aus dem Jahr 1995 ausgegeben, also Java, Ruby und JavaScript:

sed -n '/1995/ p' lang.txt

Dieser Aufruf hingegen gibt nur die Zeilen aus, die mit dem Großbuchstaben P beginnen, also Python, Pascal und Perl. Das Zeichen ^ steht dabei für den Anfang der Zeile:

sed -n '/^P/ p' lang.txt

Muster lassen sich durch Komma-Separierung kombinieren. Der folgende Aufruf gibt nur die Zeilen derjenigen Sprachen aus, die entweder mit P beginnen oder aus dem Jahr 1995 stammen. Dies sind alle bis auf Ruby:

sed -n '/^P/, /1995/ p' lang.txt

Nachfolgende Zeilen selektieren

Nach einem Treffer können weitere darauffolgende Zeilen mit ausgegeben werden. Der folgende Befehl sucht nach Gosling, dem Erfinder von Java, und gibt auch noch die zwei darauffolgenden Zeilen, die Sprachen Ruby und Pascal, mit aus:

sed -n '/Gosling/, +2 p' lang.txt

Alternativ können auch alle Zeilen ab dem Treffer bis zu einer angegebenen Zeile ausgegeben werden. Die Zeile 4 beschreibt Pascal, so dass der folgende Befehl die Sprachen Java, Ruby und Pascal ausgibt.

sed -n '/Gosling/, 4 p' lang.txt

Ersetzt man den Wert vor p durch 0, 1, oder 2, so ist das Ergebnis in allen Fällen das Gleiche: Die Zeile Java wird ausgegeben, da sie das Pattern erfüllt. Die darauffolgende Zeile hat bereits eine größere Zeilennummer als alle drei Werte, weshalb sie nicht mit ausgegeben wird.

Delete Command

Die Anweisung d repräsentiert den Delete Command. Um nur Sprachen auszugeben, die nicht aus dem Jahr 1995 stammen, können folgende Befehle verwendet werden:

sed -n '/1995/ d' lang.txt
sed '/1995/ d; p' lang.txt

Dass beide Varianten zum gleichen Ergebnis führen, habe ich schon im Hello World gezeigt. Es werden jeweils die Sprachen Python, Pascal und Perl ausgegeben.

Zeilennummer ermitteln

Die Anweisung = (Gleichheitszeichen) gibt die Zeilennummer einer Zeile aus. Der folgende Aufruf gibt die Zahlen 2, 3 und 6 aus:

sed -n '/1995/ =' lang.txt

Der nachfolgende Befehl gibt die Zeilennummer der letzte Zeile (6) der Datei aus, also ähnlich wie wc -l die Anzahl der Zeilen:

sed -n '$ =' lang.txt

Quit Command

Um die Verarbeitung einer Datei zu beenden, wird der Quit Command verwendet. Dieser Befehl ist nützlich, wenn man nur einen kleinen Teil einer sehr großen Datei betrachten möchte, oder die Verarbeitung abbrechen möchte, wenn ungültige Muster gefunden werden. Der folgende Aufruf gibt die ersten drei Zeilen der Datei aus und beendet dann die Verarbeitung:

sed '3 q' lang.txt

Wie Print und Delete lässt sich auch der Quit Command beliebig kombinieren. Darüber hinaus kann auch ein Exit Code gesetzt werden, um zu signalisieren dass die Verarbeitung nicht erfolgreich war. Im nachfolgenden Beispiel bricht die Verarbeitung ab, sobald eine Sprache aus dem Jahr 1995 verarbeitet wurde. Über den Aufruf echo $? wird der Exit-Code des Sed-Aufrufes ausgegeben und demonstriert, dass dieser nicht 0 (erfolgreich) sondern 1 (Fehler) ist:

$ sed '/1995/ q 1' lang.txt 
Python, dynamisch, Guido van Rossum, 1991
Java, statisch, James Gosling, 1995
/tmp
$ echo $?
1

Substitute Command

Ersetzungen sind ein sehr mächtiges Instrument von Sed. Sie werden in dieser Form geschrieben: s/Muster/Ersetzung/Modifikatoren

Der Einfachheit halber habe ich den Forward Slash als Trennzeichen verwendet. Es kann aber jedes beliebige Zeichen als Trennzeichen fungieren, selbst Zahlen und Buchstaben. Mit Semikolon als Trennzeichen sieht das Schema so aus: s;Muster;Ersetzung;Modifikatoren

Der folgende Aufruf ersetzt in der gesamten Datei den Großbuchstaben P durch den Großbuchstaben J:

sed 's/P/J/g' lang.txt

Das Ergebnis sieht wie folgt aus:

Jython, dynamisch, Guido van Rossum, 1991
Java, statisch, James Gosling, 1995
Ruby, dynamisch, Yukihiro Matsumoto, 1995
Jascal, statisch, Niklaus Wirth, 1970
Jerl, dynamisch, Larry Wall, 1987
JavaScript, dynamisch, Brendan Eich, 1995

Mit dem Kleinbuchstaben g als Trennzeichen sieht der Aufruf so aus:

sed 'sgPgJgg' lang.txt

Der nächste Ausdruck ersetzt das erste Komma jeder Zeile durch „Leerzeichen“ programming language „Komma“.

sed 's/,/ programming language,/' lang.txt

Da im Ausdruck kein Modifikator enthalten ist, wird nur der erste Treffer einer jeden Zeile ersetzt. Das Ergebnis sieht wie folgt aus:

Python programming language, dynamisch, Guido van Rossum, 1991
Java programming language, statisch, James Gosling, 1995
Ruby programming language, dynamisch, Yukihiro Matsumoto, 1995
Pascal programming language, statisch, Niklaus Wirth, 1970
Perl programming language, dynamisch, Larry Wall, 1987
JavaScript programming language, dynamisch, Brendan Eich, 1995

Das nächste Beispiel hat zwei Besonderheiten. Als Muster wird eine beliebige Ziffer gesucht und in der Ersetzung mit dem Et-Zeichen & wiederverwendet. Mit der Ersetzung wird erreicht, dass deutlicher wird, wofür die Jahreszahlen stehen:

sed 's/[[:digit:]]/erschien zuerst &/' lang.txt

Die Ausgabe ist:

Python, dynamisch, Guido van Rossum, erschien zuerst 1991
Java, statisch, James Gosling, erschien zuerst 1995
Ruby, dynamisch, Yukihiro Matsumoto, erschien zuerst 1995
Pascal, statisch, Niklaus Wirth, erschien zuerst 1970
Perl, dynamisch, Larry Wall, erschien zuerst 1987
JavaScript, dynamisch, Brendan Eich, erschien zuerst 1995

Was wäre das Ergebnis, wenn man das Et-Zeichen aus dem Ausdruck entfernt? Was wäre das Ergebnis, wenn man den Modifikator g „global“ hinzufügen würde? Was wäre das Ergebnis, wenn man beides tut?

In-Place-Ersetzungen

Bisher habe ich das Verarbeitungsergebnis von Sed immer auf stdout ausgegeben. In zwei Schritten soll nun eine Datei lang2.txt mit einem anderen Inhalt abgeleitet werden. Der folgende Aufruf ersetzt das zweite Komma durch einen Zeilenumbruch und schreibt das Ergebnis in eine neue Datei lang2.txt. Um nur das zweite Komma jeder Zeile zu ersetzen, wird der Modifikator 2 verwendet:

sed 's/, /\n/2' lang.txt > lang2.txt

Im zweiten Schritt soll das Komma durch einen senkrechten Strich ersetzt werden. Um dies direkt in der Datei zu tun, die verarbeitet wird, verwende ich das Sed-Flag -i.

sed -i 's/, / | /g' lang2.txt

Die Datei lang2.txt sieht nun wie folgt aus:

Python | dynamisch
Guido van Rossum | 1991
Java | statisch
James Gosling | 1995
Ruby | dynamisch
Yukihiro Matsumoto | 1995
Pascal | statisch
Niklaus Wirth | 1970
Perl | dynamisch
Larry Wall | 1987
JavaScript | dynamisch
Brendan Eich | 1995

Über eine Ersetzung können beliebige Muster entfernt werden, indem man sie durch einen Leerstring ersetzt, also zwei aufeinanderfolgende Trennzeichen. Der folgende Befehl entfernt alle Großbuchstaben und die Kleinbuchstaben a, e, i, o und u:

sed 's/[A-Zaeiou]//g' lang2.txt 

Das Ergebnis sieht so aus:

ythn | dynmsch
d vn ssm | 1991
v | sttsch
ms slng | 1995
by | dynmsch
khr tsmt | 1995
scl | sttsch
kls rth | 1970
rl | dynmsch
rry ll | 1987
vcrpt | dynmsch
rndn ch | 1995

Step

Der durch eine Tilde ~ eingeleitete Step ermöglicht es, nur jed x-te Zeile zu verarbeiten. Der nachfolgende Aufruf verarbeitet nur die ungeraden Zeilen:

sed -n '1~2 p' lang2.txt

Ausgabe:

Python | dynamisch
Java | statisch
Ruby | dynamisch
Pascal | statisch
Perl | dynamisch
JavaScript | dynamisch

Beginnt man in Zeile zwei, werden die geraden Zeilen verarbeitet:

sed -n '2~2 p' lang2.txt

Ausgabe:

Guido van Rossum | 1991
James Gosling | 1995
Yukihiro Matsumoto | 1995
Niklaus Wirth | 1970
Larry Wall | 1987
Brendan Eich | 1995

Gruppierungen

Gruppierungen sind ein Aspekt von regulären Ausdrücken und werden von Sed unterstützt. Der folgende Ausdruck gruppiert die Sprachen und gibt sie besonders aufbereitet aus:

sed 's/\([^,]\+\).*/\U\l\1/' lang.txt

Es lohnt sich den obenstehenden Aufruf etwas genauer anzusehen. Gruppierungen werden durch runde Klammern () angegeben. Viele Zeichen mit besonderer Funktion in regulären Ausdrücken müssen durch einen Back Slash \ escaped werden. Eckige Klammern werden nicht escaped. Im Muster wird eine Gruppe aus allen Zeichen vor dem ersten Komma gebildet. In der Ersetzung wird die Gruppierung dann über \1 eingefügt. Die Zeichenfolge \U führt dazu, dass die gesamte Ersetzung in Großbuchstaben geschrieben wird. Die Zeichenfolge \l („Klein-L“) führt dazu, dass der erste Buchstabe der Zeile jedoch klein geschrieben wird. Das Ergebnis ist wie folgt:

pYTHON
jAVA
rUBY
pASCAL
pERL
jAVASCRIPT

Da der Inhalt von lang.txt einem strukturierten Aufbau folgt, lassen sich Sprache, Typisierung, Erfinder und Erscheinungsjahr über separate Gruppen erfassen und neu zusammensetzen. Man merkt bei folgendem Beispiel, dass dies durch die vielen escapten Zeichen schnell unübersichtlich wird:

sed 's|\([[:alpha:]]\+\),\s\([[:alpha:]]\+\),\s\([A-Za-z ]\+\),\s\([[:digit:]]\+\).*|\1 ist eine \2 typisierte Sprache, die \4 von \3 erfunden wurde.|' lang.txt

Der Aufruf verwandelt die Komma-separierten Inhalte in vollständige Sätze:

Python ist eine dynamisch typisierte Sprache, die 1991 von Guido van Rossum erfunden wurde.
Java ist eine statisch typisierte Sprache, die 1995 von James Gosling erfunden wurde.
Ruby ist eine dynamisch typisierte Sprache, die 1995 von Yukihiro Matsumoto erfunden wurde.
Pascal ist eine statisch typisierte Sprache, die 1970 von Niklaus Wirth erfunden wurde.
Perl ist eine dynamisch typisierte Sprache, die 1987 von Larry Wall erfunden wurde.
JavaScript ist eine dynamisch typisierte Sprache, die 1995 von Brendan Eich erfunden wurde.

Fortsetzung folgt

In diesem ersten Teil habe ich die Möglichkeiten aufgezeigt, die sich mit den Commands Print, Delete, Quit und Substitute ergeben. Ich habe demonstriert, wie man mit Zeilennummern und Mustern arbeitet, und wie mächtig reguläre Ausdrücke in Sed sind.

Im kommenden zweiten Teil werde ich mich auf die fortgeschrittenen Features von Sed konzentrieren, wie man mit dem Pattern Space und Hold Space umgehen, Zeilen nachladen und damit gleichzeitig mehrere Zeilen betrachten kann und was Sed an Kontrollflusselementen zu bieten hat.