# 1.103.7

# Durchsuchen von Textdateien mittels regulärer Ausdrücke

Beschreibung: Prüfungskandidaten sollten in der Lage sein, Dateien und Textdaten mittels regulärer Ausdrücke zu bearbeiten. Dieses Lernziel beinhaltet das Erzeugen einfacherer regulärer Ausdrücke mit verschiedenen Elementen. Ebenfalls enthalten ist die Verwendung von Regex-Werkzeugen zum Durchführen von Suchen über ein Dateisystem oder Dateiinhalte.

Die wichtigsten Dateien, Bezeichnungen und Anwendungen:

- grep
- regexp
- sed

### Das Grundprinzip regulärer Ausdrücke

Reguläre Ausdrücke (regular expressions - regexp) sind nah verwandt mit den Wildcards, aber nicht das Selbe. Es handelt sich um Suchmuster, die auch eine Art Jokerzeichen beinhalten, jedoch wesentlich mächtiger sind, als die Wildcards der Shell.

Die unterschiedlichsten Programme unter Linux können mit regulären Ausdrücken umgehen, allerdings unterscheiden sie sich manchmal im Detail, in der Interpretation. Es gibt aber eine gemeinsame Grundmenge von Ausdrücken, die von allen Programmen verstanden werden.

Die LPI-101-Prüfung verlangt nur das Wissen um einfache reguläre Ausdrücke, die die folgenden Elemente enthalten dürfen:

Ausdruck | Bedeutung
--- | ---
c | Ein einzelner Buchstabe passt auf sich selbst.
`.` | Ein Punkt steht für genau einen beliebigen Buchstaben, außer das Zeilenende.
`\?` | Das dem Operator `\?` voranstehende Zeichen null oder einmal.
`*` | >Das dem Operator `*` voranstehende Zeichen null mal oder öfter.
`\+` | Das dem Operator `\+` voranstehende Zeichen ein mal oder öfter.
`^` | Das Caret (`^`) bedeutet Zeilenanfang.
`$` | Das Dollarzeichen (`$`) bedeutet Zeilenende.
`\<` | Bedeutet Wortanfang.
`\>` | Bedeutet Wortende.
`[Buchstaben]` | Ein Buchstabe aus der Menge.
`[^Buchstaben]` | Ein Buchstabe, der NICHT in der Menge steht.
`\(...\)` | Zusammenfassung eines Ausdrucks. Wichtig zum Ersetzen.
`\|` | Ein logisches ODER mit dem verschiedene Ausdrücke verknüpft werden können.
`\` | Jedes Zeichen nach dem Backslash verliert seine Sonderbedeutung.

Mit Hilfe dieser Ausdrücke sind sehr komplexe Suchmuster möglich, nehmen wir ein Beispiel:

Wir haben eine Datei mit Namen und wir wollen alle Formen von Maier (Maier, Mayer, Meier, Meyer, Mayr) ansprechen. Der reguläre Ausdruck würde nun lauten:

      M[ae][iy]e\?r

Wir suchen also ein Wort, das mit einem `M` beginnt, danach entweder ein `a` oder ein `e` vorweist. Dem folgt entweder ein `i` oder ein `y`. Dann kommt ein oder kein `e`, gefolgt vom Buchstaben `r`.

Wichtig ist zu wissen, daß die einzelnen Ausdrücke auch kombinierbar sind. So bedeutet etwa `.*` eine Folge von beliebigen Zeichen, auch die leere Folge. Warum? Ganz einfach. Der Punkt meint ein beliebiges Zeichen, das Sternchen meint das dem Sternchen vorangehende Zeichen belibig oft, auch Null mal. In Kombination wird daraus eben die beliebige Folge beliebiger Zeichen.

Zu beachten ist, daß diese Suchmuster viele Zeichen enthalten, die von der Shell interpretiert würden. Wenn wir also ein solches Muster auf der Kommandozeile eingeben, dann müssen wir es in Anführungszeichen setzen.

Aber genug der Theorie, jetzt folgen die zwei Anwendungen, mit denen wir diese Ausdrücke nutzen können:

### Dateien durchsuchen mit grep

Das Programm grep durchsucht entweder Dateien oder einen Datenstrom (seine Standard-Eingabe) nach einem regulären Ausdruck. Dabei wird standardmäßig jede Zeile, die den Ausdruck enthält auf die Standard-Ausgabe geschrieben.

Damit ist es sowohl möglich, bestimmte Dateien zu durchsuchen, als auch die Ausgabe anderer Programme. Im einfachsten Fall durchsucht grep die Dateien nur nach einem festen Suchbegriff. Wenn Sie z.B. alle Useraccounts sehen wollen, die als Startshell die bash haben, könnten Sie das mit grep ganz leicht erledigen:

      grep bash /etc/passwd

Das heißt also, die Datei /etc/passwd wird nach dem Auftreten des Begriffs "bash" durchsucht und alle Zeilen, die den Begriff enthalten werden ausgegeben. Wenn mehrere Dateien durchsucht werden sollen, dann gibt grep in der Ausgabe auch noch den Dateinamen der Datei aus, in der der Suchbegriff gefunden wurde.

Wenn grep einen Datenstrom durchsuchen soll, so wird ihm einfach kein Dateiname mitgegeben. Ein Beispiel: Wir wollen alle Prozesse des Users "hans" aufgelistet bekommen. Der Befehl "ps uax" listet uns alle laufenden Prozesse samt ihrer Eigentümer auf. Der Befehl

      ps uax | grep hans

filtert die Ausgabe des ps uax Befehls durch grep und gibt nur die Zeilen aus, die den Begriff "hans" enthalten. Allerdings werden wir hier auch noch die Zeile angezeigt bekommen, die den Prozess "grep hans" beschreibt. Um das zu vermeiden, bedienen wir uns der regulären Ausdrücke:

      ps uax | grep "^hans"

Das "^" Zeichen steht in einem regulären Ausdruck für den Zeilenanfang. Also suchen wir jetzt nach dem Auftauchen des Wortes "hans" am Zeilenanfang. Jetzt haben wir tatsächlich nur die Prozesse des Users hans.

Ein wichtiger Parameter von grep ist das `-v`. Mit dieser Kommandozeilenoption gibt grep nur die Zeilen aus, die nicht den Suchbegriff enthalten.

Anstatt eines einfachen Suchbegriffs können wir natürlich auch mit komplexen regulären Ausdrücken suchen. Um etwa aus einer Datei mit Adressangaben alle Adressen aller Formen des Namens Maier zu suchen, könnten wir schreiben:

      grep "[Mm][ae][iy]e\?r" Adressen.txt

Bitte beachten Sie die Anführungszeichen. Sie verhindern, daß die Shell selbst versucht, die eckigen Klammerm und das Fragezeichen als Wildcard zu interpretieren. Grundsätzlich ist es von Vorteil, wenn Sie den Suchbegriff in solche Anführungszeichen setzen, sobald er mehr als nur Buchstaben und Zahlen beinhaltet.

### Suchen und Ersetzen mit sed

Mit grep haben wir Dateien nach Suchbegriffen durchsucht, wir konnten diese Begriffe aber nicht ändern. Dazu sind Editoren notwendig. Alle Unix-Editoren wie etwa vi, ex, ed oder emacs können reguläre Ausdrücke verwenden, um ein "Suchen und Ersetzen" durchzuführen. Aber eine kommandozeilenorientierte Funktion, die dieses Ersetzen bietet, wird mit einem ganz speziellen Editor durchgeführt, dem Stream-Editor oder kurz sed.

Dieser Editor ist dazu entworfen, einen Datenstrom oder eine Datei automatisch zu bearbeiten. Dazu werden bestimmte Befehle, die auf diese Datei oder diesen Datenstrom anzuwenden sind, entweder in einer Datei formuliert oder gleich auf der Kommandozeile miteingetragen. Der Stream-Editor bearbeitet dann die Datei oder den Datenstrom zeilenweise und führt die jeweiligen Befehle darauf aus. Das Ergebnis wird dann wiederum auf die Standard-Ausgabe geschrieben. Wir haben das schon auf der Seite über die Textfilter besprochen.

Die mit Abstand häufigste Anwendung dieses Stream-Editors ist das Suchen und Ersetzen mit regulären Ausdrücken. Diese Vorgehensweise soll hier eingehend beschrieben werden.

Ein sed-Kommando ist immer in einen Adressbereich und einen Befehl aufgeteilt. Das Adressfeld kann dabei folgende Werte aufnehmen:

Eine Zahl n
  Eine einfache Zahl n beschreibt die Zeile n. Der folgende Befehl wird nur auf diese Zeile angewandt.
Ein Zahlenbereich n,m
  Ein Bereich meint die Zeilen n bis m. Start- und Endzeile werden durch Komma getrennt. Ein Dollarzeichen ($) meint die letzte Zeile. So bedeutet also `1,$` alle Zeilen der Datei (von der Zeile 1 bis zur letzten Zeile). Der folgende Befehl wird auf jede Zeile angewandt, die innerhalb des genannten Bereiches liegt.
Ein regulärer Ausdruck /Ausdruck/
  Wenn als Adresse ein regulärer Ausdruck steht, dann ist jede Zeile gemeint, die ein Element enthält, das auf den Ausdruck passt. Damit sed das Konstrukt als Ausdruck erkennt, muß der Ausdruck in Slash-Zeichen (/) geklammert sein.
  Wenn ein regulärer Ausdruck innerhalb einer Adressbereichsangabe steht, dann ist immer die erste Zeile gemeint, auf die der Ausdruck passt.
  Damit ein Ausdruck auch nach dem Auftreten von Slashes suchen kann, erlaubt sed auch die Verwendung einer anderen Klammerung, in der Form `\#`, wobei für `#` jedes beliebige Zeichen stehen kann. Also ist auch die Konstruktion `\+Ausdruck\+` eine legale Angabe einer Adresse.

Wird bei sed der Adressteil weggelassen, so wird jede Zeile des Datenstroms bearbeitet, es wird also die Adresse `1,$` angenommen.

Der Befehl zum Suchen und Ersetzen für sed ist das `s`, das wie folgt angewandt werden muß:

      s/Suchbegriff/Ersetzung/[Optionen]

Die Angabe der Optionen ist optional (daher die eckigen Klammern), der abschließende Slash nach dem Ersetzungsteil ist aber zwingend, auch wenn keine Optionen angegeben werden sollen.

Als Optionen stehen zur Verfügung:

Eine Zahl n
  Eine Zahl von 1 bis 512 ersetzt nur das n-te Auftreten des Musters.
g
  Global - jedes Auftreten des Begriffes innerhalb einer Zeile wird bearbeitet. Ohne diese Option wird immer nur das erste Auftreten des Begriffs bearbeitet.
p
  Print -wenn eine Ersetzung stattgefunden hat, wird der Inhalt des Arbeitsspeichers von sed in die Standardausgabe geschrieben

Im einfachsten Fall können wir also z.B. das Auftreten des Wortes "Huber" in "Herr Huber" mit dem folgenden Befehl ersetzen:

      s/Huber/Herr Huber/g

Wie wird das nun angewandt? Entweder, wir geben diesen Befehl gleich auf der Kommandozeile ein (mit der Option `-e`, dann würde das etwa folgendermaßen aussehen:

      cat Datei.txt | sed -e "s/Huber/Herr Huber/g" > Datei2.txt

oder einfacher

      sed -e "s/Huber/Herr Huber/g" Datei.txt > Datei2.txt

Der Editorbefehl wurde hier in Anführungszeichen gesetzt, weil er ein Leerzeichen enthält. Wie schon bei grep ist es grundsätzlich zu empfehlen, diese Konstruktion in Anführungszeichen zu setzen um die Shell davon abzuhalten, Sonderzeichen zu interpretieren.

Was hat unser Befehl jetzt bewirkt? Jedes Vorkommen des Wortes "Huber" in der Datei `Datei.txt` wurde in "Herr Huber" verwandelt. Die gesamte Datei mit den Änderungen wurde in die Datei `Datei2.txt` geschrieben.

Wir hätten den Befehl aber auch in eine separate Datei (nennen wir sie mal `Befehle`) schreiben können. Das macht insbesondere dann Sinn, wenn es sich um mehrere Befehle handelt, oder wir die Ersetzung immer wieder brauchen werden. Dann hätte der sed-Aufruf einfach statt der Option `-e` ein `-f` enthalten, gefolgt vom Namen der Befehlsdatei:

      sed -f Befehle Datei.txt > Datei2.txt

Die eigentliche Stärke dieses Suchen und Ersetzen liegt aber natürlich wieder in der Angabe von regulären Ausdrücken. Es können hier beliebige Ausdrücke verwendet werden, wie sie oben beschrieben sind. Als besondere Möglichkeit sei hier noch auf die Klammerung mit der Konstruktion `\(...\)` verwiesen. Auf jedes Element, das im Suchen-Teil derart geklammert ist, kann im Ersetzen-Teil wieder zugegriffen werden. Dazu muß nur eine Nummer n mit vorgestelltem Backslash (\) angegeben werden. Das meint die n-te Klammer. Ein Beispiel:

Wir haben eine Datei `Namen.txt` mit folgendem Inhalt:

      Hans Müller
      Peter Schmidt
      Michael Huber
      Gabi Maier
      Thomas Gruber

Wir wollen jetzt die Namen in der Form Nachname, Vorname haben. Kein Problem mit sed. Wir schreiben

      sed -e "s/\(.*\) \(.*\)/\2, \1/" Namen.txt > Namen2.txt

Schon steht in der Datei `Namen2.txt`

      Müller, Hans
      Schmidt, Peter
      Huber, Michael
      Maier, Gabi
      Gruber, Thomas

Was ist passiert? Schauen wir uns den sed-Befehl einmal genauer an. Der reguläre Ausdruck `.*` steht für eine beliebige Zeichenfolge beliebiger Länge. Also können wir die zwei Namen (Vor- und Nachname) auch als `.* .*` angeben. Also zwei beliebige Zeichenfolgen, getrennt durch ein Leerzeichen. Jetzt klammern wir die beiden Konstrukte jeweils mit `\(...\)` ein. So entsteht zweimal der Ausdruck `\(.*\)`. Auf jeden dieser geklammerten Ausdrücke können wir jetzt im Ersetzen-Teil des sed-Befehls wieder zugreifen, mit `\1` für den ersten gefundenen Ausdruck und `\2` für den zweiten.

Dadurch entsteht folgende Zuordnung, am Beispiel der ersten Zeile:

`\(.*\)` | `\(.*\)`
Hans | Müller
`\1` | `\2`

Im Ersetzen-Teil schreiben wir einfach `\2, \1`, das wird dann eben ersetzt durch den Inhalt der zweiten Klammer, gefolgt von einem Komma, einem Leerzeichen und dem Inhalt der ersten Klammer. Also in unserem Beispiel durch `Müller, Hans`.