Erste Schritte mit sed

Ein- und Ausgabe

sed liest von stdin und schreibt auf stdout. Man kann aber als letzten Parameter auf der Kommandozeile 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'

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 durch mehrere mit pipes (|) verkettete Programme zu jagen, jedes davon verändert den Stream nach vorgegebenen Regeln. Auf diese Weise lassen sich in Verwendung von verschiedenen Filtern sehr komplexe Aufgaben erledigen.

Kommandos

Das Programm

sed -e 'd' /etc/services

liefert erstmal 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). Die Zeile wird also in den pattern space geladen, welcher dann nach den angegebenen Regeln bearbeitet (in diesem Fall gelöscht) und anschließend ausgegeben wird - die bearbeitete Datei wird dabei nicht verändert. Diese Schritte werden für jede Zeile wiederholt, bis zum Dateiende.

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

Vor manche Kommandos kann man eine Adresse stellen, die angibt welche Zeilen mit dem betreffenden Kommando zu bearbeiten sind. Somit kann man Kommandos selektiv auf bestimmte Zeilen (oder wie wir später sehen werden, auf bestimmte Zeichenketten) anwenden. Mehr zu Adressen im nächsten Kapitel.

Adressen

Adressen können fixe Zeilen in einer Datei sein oder ganze Bereiche, oder aber Zeilen die auf einen bestimmten Reguläre 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 mit dem Resultat dass in der Ausgabe die erste Zeile fehlt.

Man kann auch Adress-Bereiche 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 Adresse 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 /r/ wendet das nachfolgende sed-Kommando auf jede Zeile an, auf die der Reguläre Ausdruck r 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, also den pattern space ausgeben. Die Ausgabe ist aber alles Andere als erwartet: jede Zeile wird ausgegeben, die erwünschten Zeilen sogar zwei mal. 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 Compilieren 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 DollarZeichen '$' 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 bis auf den Block von der n-ten bis zur m-ten Zeile zu. /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 ich aber nicht alle beschreiben kann. 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. Es verbirgt einfach alle nachfolgenden Zeichen im Script bis zum Ende der Zeile. Das ist nützlich in Scripten, die in Files geschrieben wurden und die an trickreichen Stellen ein paar erklärende Worte verlangen.

Ein wichtiges Kommando ist 's/r/rep/flag'. Hierbei wird diejenige Portion im pattern space, auf welche die RE 'r' 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 umzuleiten, der wird sich schön wundern: die Datei ist dann nämlich leer. Der richtige wenn auch umständliche Weg ist die Ausgabe in einen temporäre Datei umzuleiten und diese dann auf den Namen der Quelldatei umzubenennen. 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 '1337' 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.

Es sei noch einmal angemerkt, daß Basic Regular Expressions die Zeichen + und ? nicht kennen. GNU sed führt dagegen \+ und \?. Da die gewünschten Effekte oft leicht mit Standard-Bordmitteln von sed zu erreichen sind, empfiehlt es sich, diese Konstrukte 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 verschoben - aus 'a' wird 'n', aus 'b' wird 'o' usw: (Der Einfachheit halber werden hier nur Kleinbuchstaben verändert)

sed -e 'y/abcdefghijklmnopqrstuvwxyz/nopqrstuvwxyzabcdefghijklm/'

was auch ein schönes Beispiel für eine unüberlegte Benutzung von sed ist. Den selben Effekt kann man mit tr sehr viel einfacher und weniger fehlerträchtig 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 ist nicht zu beanstanden. Der Effekt dieses Programms ist dass die Zeilen von 1 bis 9 mit vorangestellten Zeilennummern ausgegeben werden, der Rest des Files wird unverändert wiedergegeben.

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