Die wichtigsten Dinge sind erklärt. Wir wissen, dass sed mit Adressen arbeitet, die entweder mittels regulärem Ausdruck (basic regular expression) oder per Zeilennummer angegeben werden können. Darüber hinaus unterstüzt sed die unterschiedlichsten Kommandos, von denen der Ersetzen-Befehl :s sicherlich der bekannteste und meistgebrauchte ist.
In diesem Teil möchte ich weiter Beispiele posten und erklären. Die meisten dieser Beispiele stammen aus der sed Oneliner Referenz.
cat -n simulieren
Ein einfaches cat -n macht nichts weiter als die Datei inklusive Zeilennummern ausgeben.
$ cat -n datei
1 eins
2 zwei
3 drei
Mit sed kann man das so simulieren:
$ sed -e '=' datei | sed -e '/^[0-9]\+$/{N;s/\n/ /}'
1 eins
2 zwei
3 drei
Hier werden 2 sed Prozesse benötigt, weil der erste nichts weiter macht, als in die Ausgabe die Zeilennummer mit Zeilenendezeichen '\n' getrennt zu schreiben. Nachfolgende sed-Befehle können diese Zeilen nicht mehr ändern. Daher wird ein zweiter sed Prozess gestartet, der alle Zeilen auswählt, die nur mit Zahlen anfangen (regulärer Ausdruck: ^[0-9]\+$), für diese Zeile die nächste Zeile einliest (N) und danach per substitute Befehl das Zeilenendezeichen '\n' durch ein Leerzeichen ersetzt. Es ist wichtig, dass zunächst die nachfolgende Zeile eingelesen wird, ansonsten sieht sed nicht das Zeilenendezeichen '\n'.
Die Lösung ist schon fast perfekt. Fehlt noch, dass man die Eingabe eingerückt ausgibt. Außerdem trennt cat -n die Ausgaber per Tabulatorzeichen '\t', wie ein Hexdump bestätigt. Die vollständige Lösung ist daher:
$ sed -e '=' datei | sed -e '/^[0-9]\+$/{N;s/\n/\t/;s/^/ /}'
1 eins
2 zwei
3 drei
wc -l simulieren
wc -l gibt die Anzahl der Zeilen für die aufgerufene Datei aus.
$ wc -l datei
3 lines
Sed kann auch die Anzahl der Zeilen angeben:
$ sed -n '$=' datei
3
Hier wurde der Schalter -n benutzt. Normalerweise kopiert sed jede Zeile des Inputs in den Output. Dies wird durch den Schalter -n verhindert und sed schreibt nur eine Ausgabe, wenn es explizit verlangt wird (zum Beispiel mit dem Print Kommando). $= ist wieder eine Adressangabe ($ steht für die letzte Zeile) und die Anweisung die Zeilennummer zu schreiben. D.h. wenn sed die letzte Zeile einliest, wird es die aktuelle Zeilennummer ausgeben, die zufällig mit der Anzahl der Zeilen im Input übereinstimmt.
tac emulieren
$ sed '1!G;h;$!d' datei
drei
zwei
eins
Sed kennt neben dem Verarbeitungspuffer (Pattern Space) noch den Hold Space, in dem Daten temporär abgelegt werden können. Von diesem temporären Puffer wird hier Gebrauch gemacht. 'h' als Anweisung bedeutet, dass die aktuell bearbeitete Zeile in diesen Puffer kopiert wird. Die erste Anweisung 1!G weist sed an, den Inhalt dieses Zwischenspeichers an die aktuell zu bearbeitende Zeile mit Zeilenendezeichen '\n' getrennt anzuhängen (außer in der ersten Zeile, denn da ist der Zwischenspeicher noch leer). Dieser Input wird dann wiederum in den temporären Puffer geschrieben. Die letzte Anweisung $!d bedeutet lösche jede Zeile außer der letzten. (! entspricht einem Not Operator).
Wenn sed die letzte Zeile liest, fügt es den Inhalt aller vorherigen Zeilen an und gibt diese wieder aus. Durch die Art, wie die Zeilen angehängt werden, entspricht diese Reihenfolge genau der umgekehrten Reihenfolge der Eingabe.
Eine alternative Anweisung, wäre diese, in der sed explizit nur nach der letzte Zeile etwas ausgibt und ansonsten keine Ausgabe erzeugt:
$ sed -n '1!G;h;$p' datei
drei
zwei
eins
Dieser Sed Einzeiler enthält 3 Anweisungen: 1!G, h, $p. 1!G bedeutet, dass für jede Zeile außer der ersten der Inhalt in den temporären Puffer (Hold Space) geschrieben wird.
Zeilen, die mit einem Backslash enden, in eine Zeile schreiben
$ sed -e ':a' -e '/\\$/{N;s/\\\n//;ta}' datei
In der ersten Anweisung wird die Sprungmarke :a definiert. Das erlaubt es, später wieder dorthin zu springen. In der nächsten Anweisung sucht sed nach Zeilen, die mit einem Backslash aufhören. (\\$, der Backslash muß einmal maskiert werden, sonst würde sed nach dem $ literal suchen). Für diese Zeilen liest sed die nächste Zeile ein (Kommando N) und löscht anschließend die Zeichenfolge \ gefolgt von einem Zeilenende.
Dadurch sind nun die ehemals 2 getrennten Zeilen wieder zu einer zusammengefügt. Nach dem dies geschehen ist, springt sed zur Sprungmarke a und fängt von vorne an (falls die neue Zeile ebenfalls mit einem \ endet). Wenn nichts gefunden wird, wird die Zeile ausgegeben.
grep emulieren
$ sed '/eins/!d' datei
eins
Sed sucht nach Zeilen die das Suchmuster enthalten. Alle Zeilen, die nicht auf das Muster passen werden gelöscht und nicht ausgegeben. Eine alternative Lösung ist:
$ sed -n '/eins/p' datei
eins
Hier wird sed nur dann etwas ausgeben, wenn es dazu angewiesen wird. In diesem Fall wird sed dazu angewiesen, wenn das Muster /eins/ auf die Zeile passt.
tail -10 emulieren
$ sed -e :a -e '$q;N;11,$D;ba' /var/log/syslog
In diesem Fall wird sed immer maximal 10 Zeilen in seinem Puffer halten. Sobald es die letzte Zeile erreicht, wird der komplette Puffer ausgegeben. Jede eingelesene Zeile veranlasst sed dazu, die nächste Zeile einzulesen (N) und nachdem es 10 Zeilen eingelesen hat, wird sed immer die erste eingelesene Zeile löschen (D) und mit der nächsten Zeile fortsetzen. Zum Schluß wird sed angewiesen zur Sprungmarke a zu springen und von vorne anzufangen. Dadurch wird sichergestellt, dass niemals mehr als 10 Zeilen im Puffer sind.
Leere Zeilen löschen
$ sed /./!d datei
Der '.' matcht nur auf Zeilen, die mindestens ein Zeichen enthalten. Leere Zeilen erfüllen diese Bedingung nicht und würden daher aus dem Input gelöscht. Alternativ kann man mit
$ sed /^$/d datei
das gleiche erreichen. Hier wird sed angewiesen, explizit alle Zeilen, die nur aus Zeilenanfang und Zeilenende (also keinen Inhalt haben) bestehen zu löschen.
Alle Zeilenendezeichen entfernen
$ sed -e ':x' -e 'N;$!bx;s/\n//g' datei
einszweidrei
Dies ist eine interessante Aufgabenstellung. Normalerweise kann man mit sed nicht auf das Zeilenende matchen, weil sed seinen Input zeilenweise verarbeitet. Man muß also sed anweisen, erst die komplette Datei in seinen Puffer zu kopieren, bevor man anfängt das Zeilenendezeichen '\n' zu ersetzen. Daher wird zunächst die Sprungmarke :x definiert. Anschließend wird sed angewiesen, so lange die nächste Zeile einzulesen, bis es die letzte Zeile gelesen hat. Nachdem sed die nächste Zeile eingelesen hat, wir so lange zur Sprungmarke :x gesprungen, bis die letzte Zeile im Zwischenspeicher enthalten ist. Wenn dies der Fall ist, kann auch nach den Zeilenendezeichen '\n' gesucht werden und es gelöscht werden.
Da in diesem Fall immer die komplette Datei in den Puffer gelesen wird, kann es bei großen Dateien zu Geschwindigkeitseinbußen kommen. Es ist daher eigentlich auch nicht empfehlenswert, diese Aufgabe durch sed erledigen zu lassen. Geeignetere Tools wären zum Beispiel tr:
$ tr -d '\n' <datei
einszweidrei
Fazit
Abschließend kann festgestellt werden, dass sed ein durchaus flexibles Werkzeug ist, wenn man per Stapelverarbeitung Dateien bearbeiten muß. Durch seine Eigenschaft, Dateien immer zeilenweise zu verarbeiten und die Verwendung von Basic/Extended Regular Expressions, ist es jedoch nur mit Einschränkungen zu empfehlen, wenn man komplexere Bearbeitungen über mehrere Zeilen vornehmen möchte oder wenn man kompliziertere Reguläre Ausdrücke mit Look-around Assertions benötigt. Es lohnt sich aber auf jeden Fall sich mit den vielfältigen Möglichkeiten von sed vertraut zu machen, gerade für kleinere Aufgaben ist sed hervorragend geeignet und es muß ja auch nicht jedesmal gleich die Scriptsprache $foo ausgepackt werden. Sed entspricht daher ganz der Unix-Tradition "Do one thing well".
