Kapitel 4. Ein paar interessantere Beispiele

Wer bis hierher gekommen ist, sollte wirklich verstanden haben, was Adressen und was Kommandos sind. Das ist wichtig, denn ab jetzt werden diese in einem sed-Script hintereinander gehängt, und das kann sonst schon für einige Verwirrung sorgen.

Hin und wieder trifft man in Scripten nicht die gewohnte Form '/r/' einer RE vor - die Slashes '/' scheinen zu fehlen. Das hat den Grund, dass es manchmal nötig ist, in einer RE den Slash selber anzugeben. Damit dieser aber nicht fälschlicherweise interpretiert wird, muss er mit dem Backslash escaped werden, also '\/'. sed gibt einem die Möglichkeit, ein anderes Zeichen als den Slash als RE-Begrenzer zu verwenden. Man kann als Adresse '/\/bin\/ls/' oder beispielsweise '\@/bin/ls@' verwenden. In ähnlicher Weise kann das mit dem s- oder y-Kommando geschehen: 's//' ist gleichwertig zu 's@@' Hat man nicht genau verstanden, was Adresse, was Kommando und was RE ist, kommt man da leicht ins Schleudern.

Probleme mit REs

Reguläre Ausdrücke sind greedy, finden also immer den längsten passenden String. Das kann manchmal unerwünscht sein. Will man zum Beispiel eine HTML-Seite in Text umwandeln, dann könnte man in Versuchung kommen folgendes Script zu verwenden:

sed -e 's/<.*>//g' text.html

Das liefert aber nicht den gewünschten Effekt, denn eine Zeile

Das <b>ist</b> ein <i>Beispiel</i>.

wird zu

Das .

verkrüppelt. Man muss also nur jene Zeichen bis zum ersten '>' löschen:

sed -e 's/<[^>]*>//g' text.html

Muss man einen Text nicht bis zum ersten Vorkommen eines Zeichens sondern einer Zeichenkette bearbeiten, wird die RE ein bisschen komplizierter. Im Kapitel mit den Beispielen findet sich dazu ein Lösungsansatz (Löschen von Kommentaren).

Selektives Ersetzen

Das s/// Kommando kann nicht nur fixe Strings einsetzen, sondern auch den gefundenen String oder Substrings davon. Der Ampersand '&' steht dabei für den gesamten gefundenen String.

In meiner Kindheit hatten wir die elleff-Sprache, unsere Geheimsprache, bei der man jeden Vokal (oder Gruppe von Vokalen) in einem Wort mit <VOKAL>l<VOKAL>f<VOKAL> ersetzen muss. Kompliziert? Da ist die sed-Schreibweise einfacher:

sed -e 's/[aeiou][aeiou]*/&l&f&/g'

Die Mächtigen der Welt, als 'Bilifill Clilifintolofon' oder 'Boloforilifis Jelefelzilifin' ausgesprochen, gewinnen damit in meinen Augen sofort an Sympathie. Meine Hochachtung jedem, der ein verellefftes 'ukulele' aussprechen kann ohne es vom Bildschirm zu lesen.

Mit GNU sed kann man folgende Zeile schreiben:

sed -e 's/[aeiou]\+/&l&f&/g'

Bitte den Backslash '\' vor dem Plus beachten, da dieses Zeichen - weil GNU-Erweiterung - zuerst als normaler Charakter angesehen wird und seine Bedeutung, die er bei REs innehat, erst durch den Backslash gewinnt. Gleiches gilt auch für das Fragezeichen (Questionmark) '?', nicht aber für den Asterisken '*'.

Hier weise ich noch einmal auf die Grenzen von regulären Ausdrücken hin. Es ist nicht möglich, die Rücktransformation aus der elleff-Sprache mit REs auszudrücken. Ein [aeiou]l[aeiou]f[aeiou] kann man wohl angeben, nicht aber die Bedingung, dass alle drei Vokale gleich sein müssen. Ob dies hinreichend ist, um die elleff-Sprache als sichere Verschlüsselungsmethode zu bezeichnen, müssen wohl findigere Kryptologen entscheiden.

Mit sed ist es auch möglich, Teile von Strings herauszupicken, um diese später zu verwenden. Diese Teile werden mit '\(' und '\)' markiert, und man kann auf diese Strings mit '\1', '\2' usw. zugreifen. Nehmen wir einmal an, wir hätten eine Datei, in der verschiedene Namen eingetragen sind:

Alan Mathison Turing
Claude Elwood Shannon
Grace Murray Hopper
John von Neumann
Ada Lovelace

die in die Form <VORNAME> [<INITIAL ZWEITER NAME>.] <NACHNAME> gebracht werden soll. Dazu muss man erst die Bereiche definieren:

sed -e 's/^[^ ][^ ]* [[:alpha:]].* [^ ][^ ]*$//'

Nun gibt man um die gewünschten Zonen die Klammern und stellt sich das Ergebnis mit '\1' und '\2' und '\3' zusammen:

sed -e 's/\(^[^ ][^ ]*\) \([[:alpha:]]\).* \([^ ][^ ]*\)$/\1 \2. \3/'

und voilà das Ergebnis:

Alan M. Turing
Claude E. Shannon
Grace M. Hopper
John v. Neumann
Ada Lovelace

Will man das Ergebnis noch in eine Adressdatenbank importieren, dann muss man einen Feldbezeichner vor die Namen setzen. Ein erster Versuch wäre der, das gleich in einem Rutsch mit dem Script

sed -e 's/\(^[^ ][^ ]*\) \([[:alpha:]]\).* \([^ ][^ ]*\)$/name: \1 \2. \3/'

zu bewerkstelligen; das liefert aber genau dann ein falsches Ergebnis, wenn der zweite Vorname fehlt.

name: Alan M. Turing
name: Claude E. Shannon
name: Grace M. Hopper
name: John v. Neumann
Ada Lovelace

Einem solchen nur teilweise formatierten Datenhaufen ist nur schwer beizukommen. Deshalb den Output ungetesteter Scripte immer zuerst auf eine temporäre Datei umleiten, diese auf Korrektheit prüfen und dann die Zieldatei ersetzen. Wie man die Namen nun richtig formatiert, wird im nächsten Kapitel beschrieben. Warum hat das Script aber nicht richtig gearbeitet? Damit die RE auf eine Zeile zutrifft, muss diese mindestens 3 Felder, durch Leerzeichen getrennt, enthalten. Das ist bei Frau Lovelace nicht der Fall, deshalb wird auch das Kommando nicht ausgeführt und der pattern space wird unberührt gelassen.

Gruppieren von Kommandos

Ein sed-Script kann mehrere Kommandos enthalten, die nacheinander abgearbeitet werden. Das kann man auf mehrere Wege erreichen: Man kann zwei Kommandos im selben Script durch einen Semicolon (;) trennen oder man gibt mehrere Scripts mit der Option -e an. Für längere Scripte empfiehlt es sich, diese in eine Datei zu schreiben und diese Scriptdatei mit der Option -f aufzurufen.

Eine mögliche Lösung des obigen Problems benutzt zwei Kommandos: das erste kürzt den Namen, ein zweites setzt vor alle Zeilen den String name:.

sed -e 's/\(^[^ ][^ ]*\) \([[:alpha:]]\)..* \([^ ][^ ]*\)$/\1 \2. \3/' \
  -e 's/..*/name: &/'

oder man trennt die zwei Anweisungen durch einen Strichpunkt (;). Zu beachten ist in der zweiten Anweisung die RE '..*'; würde man nur einen Punkt schreiben, passte dieser Ausdruck auch auf leere Zeilen. Das wird mit zwei Punkten vermieden.

Dieses Script, in eine Datei geschrieben, schaut so aus:

s/\(^[^ ][^ ]*\) \([[:alpha:]]\)..* \([^ ][^]*\)$/\1 \2. \3/
s/..*/name: &/

Anmerkung

Und wieder eine Bemerkung, die nichts mit sed zu tun hat: Die Shell gibt einem die Möglichkeit, Scripte wie normale Programme zu verwenden. Dazu muss man nur an den Anfang des Scriptes die Zeile '#!/pfad/zum/programm' setzen und die Scriptdatei als ausführbar markieren. Wenn diese Datei nun gestartet wird, ruft die Shell den angegebenen Interpreter mit dem Scriptnamen als Parameter auf. Auf das vorhergehende Beispiel angewandt sieht das so aus:
#!/bin/sed -f
s/\(^[^ ][^ ]*\) \([[:alpha:]]\)..* \([^ ][^]*\)$/\1 \2. \3/
s/..*/name: &/
Die Option -f weist sed an, den nachfolgenden Dateinamen (den die Shell hinzufügt) als Script zu nehmen. Dieser Trick funktioniert nur mit Scriptsprachen, bei denen das Zeichen '#' einen Kommentar einleitet, da sonst auch die erste Zeile als Programmcode interpretiert wird. Die Zeichenkombination '#!' nennt man shebang.

Wie weiter oben beschrieben, kann man die geschwungenen Klammern '{}' verwenden, um mehrere Kommandos auf eine Adresse anzuwenden. Dies lässt sich auch für einen kleinen Trick missbrauchen. Will man zum Beispiel das shebang ('#!') in der ersten Zeile einer Datei entfernen, kann man das so machen:

sed -e '1{/^#!/d;}'

Dieses Script löscht die erste Zeile, aber nur wenn sie mit '#!' beginnt. Es ist ein schönes Beispiel für die Kombination von mehreren Adressen.

Eindeutige Kodierung der Eingabe

Gelegentlich will ein Script einfach nicht funktionieren, und man verbringt Stunden damit, zu rätseln, warum eine RE partout nicht auf eine Zeile passen will. Manchmal liegt das an der Eingabe-Datei, nämlich wenn sie Zeichen enthält, die man nicht erwartet. In solchen Fällen ist das Kommando 'l' nützlich. Es gibt den pattern space in einer eindeutigen Schreibweise (welche an ANSI C angelehnt ist) auf den Bildschirm:

echo "versuch	mich zu haschen! " | sed -ne 'l'
versuch\tmich zu haschen\307\203 $

Jetzt wird klar, warum die Adresse '/^versuch mich zu haschen!$/' nicht zur Eingabe passt: erstens ist das Zeichen zwischen den ersten beiden Wörtern kein Leerzeichen sondern ein Tabulator, zweitens ist das vermeintliche Rufezeichen in Wahrheit das Unicode Zeichen "latin letter retroflex click", und schließlich hat sich ein Leerzeichen am Ende der Zeile eingeschlichen.

Anmerkung

Das obige Beispiel ist möglicherweise nicht durch Copy&Paste nachzuvollziehen, da Sonderzeichen nicht immer korrekt kopiert werden. Wenn auf einem System das Programm base64 vorhanden ist, dann kann man das Beispiel mit der folgenden Eingabe nachvollziehen:
echo dmVyc3VjaAltaWNoIHp1IGhhc2NoZW7HgyAK | base64 -d | sed -ne 'l'