Kapitel 3. Erste Schritte mit sed

Ein- und Ausgabe

sed liest gewöhnlich von stdin (standard input, normalerweise die Tastatur) und schreibt auf stdout (standard output, normalerweise der Bildschirm). Man kann aber nach den Kommandozeilenoptionen einen (oder mehrere) Dateinamen angeben, von dem die Eingabe gelesen werden soll. Weiters kann man sich der Umleite-Operatoren der Shell bedienen (>, <, |). Die drei folgenden Zeilen liefern das selbe Ergebnis:

sed -n -e '/root/p' /etc/passwd
sed -n -e '/root/p' < /etc/passwd
cat /etc/passwd | sed -n -e '/root/p'

Das Script '/root/p' liest die Eingabedatei ein (/etc/passwd) und schreibt nur jene Zeile(n) auf den Bildschirm die das Wort 'root' enthalten.

Anmerkung

Noch eine kleine Besserwisserei meinerseits, die mit sed eigentlich nichts zu tun hat, sondern mit Shell-Scripting. Von den beiden Zeilen
programm 2>&1 >file
und
programm >file 2>&1
ist die zweite Version vorzuziehen, da die erste Kommandozeile stderr auf den alte stdout setzt, und erst anschließend stdout auf filename umlenkt; stderr wird also i.d.R. nicht nach filename umgelenkt werden.

Die meisten UNIX-Kommandos lassen sich als Filter einsetzen. Filter werden dazu verwendet, um einen Stream von Daten durch mehrere mit Pipes (|) verkettete Programme zu jagen. Eine Pipe macht nichts anderes als stdout des Programms auf der linken Seite mit stdin des Programms auf der rechten Seite zu verknüpfen. Auf diese Weise lassen sich in Verwendung von verschiedenen spezialisierten Programmen sehr komplexe Aufgaben erledigen.

Kommandos

Das Programm

sed -e 'd' /etc/services

liefert erst mal gar nix.

Wie die Verarbeitung einer Zeile zu erfolgen hat, wird in einem Script oder Programm, festgelegt, das auf der Kommandozeile der Option '-e' folgen muss. Ein sed-Script enthält mindestens ein Kommando (in diesem Fall 'd' für delete).

Anmerkung

Eigentlich ist die Angabe der Option '-e' nicht zwingend notwendig. Wenn diese Option weggelassen wird, dann wird das erste Argument als Script und alle folgenden Argumente als Dateinamen interpretiert. In dieser Einführung wird die Option '-e' aber immer explizit angegeben um Missverständnisse zu vermeiden.

Die Funktionsweise eines Scriptes ist wie folgt: eine Zeile des Eingabe-Streams wird in den Arbeitsspeicher (pattern space) geladen, welcher dann nach den angegebenen Regeln bearbeitet (im Beispiel oben wird er gelöscht) und anschließend ausgegeben wird. Die Eingabedatei wird dabei nicht verändert. Diese Schritte werden Zeile für Zeile wiederholt, bis das Dateiende erreicht ist.

Bitte die beiden Anführungszeichen ' ' um das sed-Script 'd' beachten. Diese verhindern eine Re-Interpretation der dazwischen liegenden Zeichen seitens der Shell. Diese Anführungszeichen sollten immer verwendet werden, da man sich dadurch unerwartetes Verhalten des Scriptes ersparen kann.

Anmerkung

Die zeilenorientierte Arbeitsweise von sed eignet sich sehr gut, um Texte zu bearbeiten. Binäre Daten werden kaum mit sed bearbeitet, da sie sich nur umständlich als reguläre Ausdrücke angeben lassen und weil je nach Beschaffenheit der Eingabe-Daten sehr große Blöcke in den pattern space geladen werden müssen.

Adressen

Den meisten Kommandos kann man eine Adresse voranstellen. Eine solche Adresse bestimmt, welche Zeilen mit dem betreffenden Kommando zu bearbeiten sind. Somit kann man Kommandos selektiv auf bestimmte Zeilen, Blöcke oder wie wir später sehen werden, auf bestimmte Zeichenketten, anwenden.

Eine Adresse ist zum Beispiel eine fixe Zeilennummer in der Eingabe oder ganze Bereiche, oder aber Zeilen, die auf einen bestimmten regulären Ausdruck passen.

sed -e '1d' /etc/services

Hier wird das Kommando 'd' auf die Zeile mit der Adresse '1' angewendet. Der Effekt des Programms ist der, dass die erste Zeile von /etc/services in den pattern space geladen, dieser dann gelöscht und anschließend der leere pattern space (also nichts) ausgegeben wird. Alle anderen Zeilen werden in den pattern space geladen, der nicht bearbeitet wird, da die Adresse nicht auf die Zeile zutrifft und anschließend wird der pattern space nach stdout geschrieben. Das Ergebnis des Scriptes ist eine Kopie der Eingabedatei, in der die erste Zeile fehlt.

Man kann auch Adressbereiche angeben, wie in

sed -e '1,10d' /etc/services

was die ersten 10 Zeilen löscht, oder man kann jede n-te Zeile bearbeiten, wie in

sed -e '10~2d' /etc/services

wo jede zweite Zeile, ausgehend von der 10. Zeile gelöscht wird. Letzteres ist eine GNU-Erweiterung von sed; dort wo Portabilität auf andere Umgebungen wichtig ist, ist diese Art der Adressierung zu vermeiden.

Manchmal ist es interessant, nur solche Zeilen einer Konfigurationsdatei anzuzeigen, die nicht auskommentiert sind. Das kann mit folgender Zeile geschehen:

sed -e '/^#.*/d' /etc/inetd

Die Adresse /re/ wendet das nachfolgende sed-Kommando auf jede Zeile an, auf die der reguläre Ausdruck re passt. Nur zur Erinnerung - die angegebene RE passt auf jede Zeile, welche "mit einem '#' beginnt und danach null oder mehr beliebige Zeichen enthält". Das ist aber nicht das, was wir eigentlich wollten. Denn enthält die Datei eine leere Zeile, dann wird diese auch ausgegeben. Also müssen wir unsere Strategie ändern und z.B. nur jene Zeilen ausgeben, die mit einem Zeichen beginnen, das nicht '#' ist:

sed -e '/^[^#].*/p' /etc/inetd

Das ist neu: das Kommando p, das für print steht, gibt den pattern space aus. Die Ausgabe ist aber alles andere als erwartet: jede Zeile wird ausgegeben, die erwünschten Zeilen sogar zweimal. Was ist passiert? Noch einmal müssen wir die Funktionsweise von sed durchkauen: Zeile einlesen, wenn die Adresse passt, dann pattern space bearbeiten (in unserem Falle ausgeben), dann pattern space ausgeben. Wir müssen also den letzten Schritt unterbinden. Das geht mit der Option -n (portabel!) oder --quiet oder --silent, je nach Geschmack. Das richtige Programm schaut nun so aus:

sed -n -e '/^[^#].*/p' /etc/inetd

Wir haben gesehen, dass mit der Adresse 'n,m' die n-te bis m-te Zeile bearbeitet wird. Das geht auch mit REs: die Adresse '/BEGIN/,/END/' selektiert alle Zeilen ab der ersten Zeile, auf die die RE 'BEGIN' passt bis zu der Zeile, auf die die RE 'END' passt, oder bis zum Dateiende, je nach dem was früher kommt. Wird 'BEGIN' nicht gefunden, dann wird keine Zeile bearbeitet. Es ist oft so, dass man beim Kompilieren eines umfangreichen Projektes regelrecht von Fehlermeldungen und Warnungen erschlagen wird. Das ist ein Job für sed: das folgende Beispiel liefert nur jene Ausgaben des gcc, die zwischen der ersten Warnung und der ersten Fehlermeldung liegen.

gcc sourcefile.c 2>&1 | sed -n -e '/warning:/,/error:/p'

Und wenn 'n,m' gilt und '/BEGIN/,/END/', warum nicht auch eine Kombination davon? Ein '/BEGIN/,m' heißt ab der Zeile, auf welche die RE 'BEGIN' passt bis zur m-ten Zeile usw.

Das folgende Beispiel ist wohl das kürzeste sinnvolle Script in sed, das es gibt. Es gibt die Zeilenanzahl der bearbeiteten Datei aus (wc -l):

sed -n -e '$='

Das Dollar-Zeichen '$' ist in diesem Fall nicht das Zeilenende einer RE (es fehlen nämlich die //), sondern ist die Adresse der letzten Zeile der letzten Eingabe-Datei, und das Kommando '=' gibt die aktuelle Zeilennummer vor der Ausgabe aus.

Ein Rufezeichen '!' nach einer Adresse verkehrt diese in ihr Gegenteil um. Die Adresse n,m! trifft auf alle Zeilen außer Zeilen n bis m zu. Die Adresse '/awk/!' selektiert alle Zeilen die nicht die Zeichenkette 'awk' enthalten.

Mehr Kommandos

Neben den Kommandos 'd' und 'p', die wir schon kennen, gibt es noch eine Reihe anderer Kommandos, die aber nicht alle in dieser Einführung beschrieben werden. Hat man erst einmal die Syntax eines sed-Programms verstanden, findet man sich leicht in der man/info-page zurecht und man kann sie dort nachschlagen.

Ein einfaches Kommando ist 'q', das das Programm abbricht. Ob der pattern space noch geschrieben wird, hängt davon ab, ob die Option -n angegeben wurde oder nicht. Als Beispiel folgen zwei funktionsmäßig äquivalente Emulationen des UNIX-Befehls head, wobei die zweite Lösung effizienter ist, da sie nur die ersten 10 Zeilen bearbeiten muss.

sed -n -e '1,10p'
sed -e '10q'

Weiters wird auch das Kommentarzeichen '#' als Kommando bezeichnet. sed ignoriert alle nachfolgenden Zeichen im Script bis zum Ende der Zeile. Das ist nützlich in Scripten, die in Dateien geschrieben wurden und die an trickreichen Stellen ein paar erklärende Worte verlangen.

Ein wichtiges Kommando ist 's/re/rep/flag'. Hierbei wird diejenige Portion im pattern space, auf welche die RE 're' passt, durch die Zeichenkette 'rep' ersetzt und zwar in der Modalität, die mit dem flag bestimmt wird. Ein 'd' ersetzt das erste Muster und fängt dann einen neuen Zyklus an. Das Flag 'g' ersetzt alle Muster in einer Zeile, eine Nummer 'n' veranlasst sed, das n-te übereinstimmende Muster zu ersetzen. Mit dem Einzeiler

sed -e '/ich/s/€1500/€3000/g' Gehaltsliste.dat

kann man ein bisschen träumen. (Bei den Träumen wird es wohl bleiben, denn sed verändert die Datei nicht!) Wer jetzt denkt, die Ausgabe mittels Ausgabeumleitung '>' wieder auf die Eingabedatei umleiten zu können, der wird sich schön wundern: die Datei ist dann nämlich leer.

Tipp

Der richtige - wenn auch umständliche - Weg, eine Datei mit sed zu verändern, ist die Ausgabe in eine temporäre Datei umzuleiten und diese dann auf den Namen der Quelldatei umzubenennen. GNUsed kennt die Option -i, welche das Programm veranlasst, die Eingabe-Datei direkt (inline) zu editieren.

Noch nicht verstanden, was das vorherige Beispiel gemacht hat? In der Zeile, die 'ich' enthält, wird das Gehalt von €1500 auf €3000 verdoppelt; alle anderen Zeilen werden unverändert ausgegeben.

Die folgende Zeile lässt die Ausgabe des Shell-Kommandos ls hingegen sehr 'l33t' aussehen:

ls -l /|sed -e 's/o/0/g'|sed -e 's/l/1/g'|sed -e 's/e/3/g'

Das ist als Kommandozeile ein wenig lang - könnte man nicht... ja, man kann das alles kompakter schreiben, indem man mehrere sed-Kommandos durch Strichpunkte trennt.

ls -l /|sed -e 's/o/0/g;s/l/1/g;s/e/3/g'

Will man dem blinden Zorn des Superusers aus dem Wege gehen und eine Verhunzung seiner Homedirectory vermeiden, muss man die Adresse '/ root$/!' den Kommandos voranstellen. Diese Adresse selektiert jede Zeile, die nicht mit ' root' endet. Um mehrere Kommandos auf eine Adresse zu binden, müssen diese gruppiert werden. Das geschieht mit den geschwungenen Klammern {}. Wichtig: auch nach dem letzten Kommando muss ein Strichpunkt gesetzt werden.

ls -l /|sed -e '/ root$/!{s/o/0/g;s/l/1/g;s/e/3/g;}'

Das folgende Script zeigt dazu ein Beispiel und kann dazu verwendet werden, 8 Leerzeichen in ein Tabulatorzeichen zu verwandeln.

sed -e 's/ \{8\}/^t/g'

wobei das ^t ein tab-Zeichen symbolisieren soll. Alles schön und gut, nur ist die tab-Taste unter der Shell für das schöne Wort Kommandozeilenvervollständigung reserviert; ein Tabulatorzeichen selber kann man nicht direkt eingeben. Der einfachste Weg dazu ist die Tastenkombination ^V^I zu drücken, was für CTRL-V CTRL-I steht. Ein ^V fügt das nachfolgende Zeichen ohne weitere Interpretation auf der Kommandozeile ein. Alternativ kann man also auch ^V<tab> tippen. Mehr dazu in den info-pages zu bash, tcsh oder readline, sowie bei Ihrem Arzt oder Apotheker.

Anmerkung

Es sei noch einmal angemerkt, dass für Basic Regular Expressions die Zeichen + und ? keine Sonderbedeutung haben. GNUsed kennt dagegen \+ und \?. Alternativ dazu kann man mit GNUsed auch die Option -r verwenden. Diese Option bewirkt, dass alle REs als Extended REs interpretiert werden. Da die gewünschten Effekte sich aber relativ leicht mit Standardbordmitteln von sed erreichen lassen, empfiehlt es sich, diese unportablen Erweiterungen selten oder gar nicht zu verwenden.

Ein weiteres nützliches Kommando ist 'y/SOURCE-CHARS/DEST-CHARS/', welches alle Zeichen in SOURCE-CHARS in das entsprechende Zeichen in DEST-CHARS umwandelt. Unnütz zu sagen, dass beide Charakter-Listen die gleiche Anzahl von Zeichen enthalten müssen. Das folgende Script 'verschlüsselt' den Text mit der sogenannten 'rot-13'-Methode: alle Buchstaben werden um 13 Zeichen im Alphabet verschoben - aus 'a' wird 'n', aus 'b' wird 'o' usw., der Einfachheit halber hier nur für Kleinbuchstaben:

sed -e 'y/abcdefghijklmnopqrstuvwxyz/nopqrstuvwxyzabcdefghijklm/'

Das obige Beispiel ist auch ein schönes Exempel für eine umständliche Benutzung von sed. Den gewünschten Effekt kann man mit tr und weniger Getippe erreichen;

tr '[a-z]' '[n-za-m]'

Ein Nachtrag zu den geschwungenen Klammern '{}': Aus der Sicht von sed ist die öffnende Klammer '{' ein Kommando, dem eine Adresse oder ein Adressbereich vorangestellt werden kann. Das lässt sich für einen Trick missbrauchen, denn wenn man die Kommandos '=', 'a', 'i', oder 'r' (erlauben höchstens eine Adresse; zur Bedeutung dieser Kommandos bitte die Dokumentation bemühen) auf einen Adressbereich anwenden will, kann man sie in geschwungene Klammern setzen. So ist z.B. '1,9=' ein ungültiges Kommando, aber '1,9{=;}' ist nicht zu beanstanden. Der Effekt dieses Programms ist, dass die Zeilen von 1 bis 9 mit vorangestellten Zeilennummern ausgegeben werden, der Rest der Datei wird unverändert wiedergegeben.

Tipp

Weil es oft gebraucht wird, stelle ich noch Scripte zur Umwandlung von Dateien im DOS-Format (CR/LF) ins UNIX-Format (LF) und umgekehrt vor. Sie wurden der schon erwähnten sedfaq von Eric Pement entnommen.
# 3. Under UNIX: convert DOS newlines (CR/LF) to Unix format
sed 's/.$//' file    # assumes that all lines end with CR/LF
sed 's/^M$//' file   # in bash/tcsh, press Ctrl-V then Ctrl-M
# 4. Under DOS: convert Unix newlines (LF) to DOS format
C:\> sed 's/$//' file                     # method 1
C:\> sed -n p file                        # method 2
Eine Randbemerkung: Ist keine -e Option angegeben, dann wird der erste Parameter, der keine Option ist, als das auszuführende Programm genommen. Um Verwirrung zu vermeiden, empfiehlt es sich, immer ein -e anzugeben. Einem Guru wie Herrn Pement sei es aber gestattet, sich über diese Faustregel hinwegzusetzen.

UNIX wäre nicht UNIX, wenn es nicht unzählige andere Methoden dafür gäbe: beispielsweise die Programme dos2unix bzw. unix2dos, oder der Befehl tr -d [^M] < inputfile > outputfile um vom DOS- ins UNIX-Format zu konvertieren, oder :set fileformat=dos bzw. :set fileformat=unix unter vim oder...