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 gequotet werden, also '\/'. sed gibt einem die Möglichkeit, ein anderes Zeichen als den Slash als RE-Begrenzer zu verwenden. Man kann also '/\/bin\/ls/' oder beispielsweise '\@/bin/ls@' verwenden. In gleicher Weise kann das mit dem s- oder y-Kommando geschehen: 's//' ist gleichwertig zu 's@@' Hat man deshalb 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 inne hat, 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 heraus zu picken 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 ein File, in dem verschiedene Namen eingetragen sind:

John Fitzgerald Kennedy
Franz Josef Strauss
Ernst Theodor Amadeus Hoffmann
Theo Lingen

die in die Form <VORNAME> [<INITIAL ZWEITER NAME>.] <NACHNAME> gebracht werden soll. Dazu muss man erst die Regionen 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:

John F. Kennedy
Franz J. Strauss
Ernst T. Hoffmann
Theo Lingen

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 da ein falsches Ergebnis, wenn der zweite Vorname fehlt.

name: John F. Kennedy
name: Franz J. Strauss
name: Ernst T. Hoffmann
Theo Lingen

Einem solchen nur teilweise formatierten Datenschwulst 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 Herrn Lingen 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 nach einander 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.

Diese 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 behandeln. 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.