Mit regulären Ausdrücken quälen angeblich Informatikprofessoren ihre Studenten. Tatsächlich bilden Regexe im Linux-Universum ein überaus mächtiges Werkzeug.
Wer programmiert, stolpert irgendwann zwangsläufig über sie: reguläre Ausdrücke. In Programmiersprachen sind sie praktisch allgegenwärtig, es gibt sie für PHP, C, Python und Java. Außerdem tauchen sie unweigerlich im Kontext von Unix-Urgesteinen wie Grep, Sed und Awk auf. Dabei dienen reguläre Ausdrücke stets demselben Zweck, dem Erkennen von Mustern. Immer dann, wenn dafür die simple Suchen-Ersetzen-Funktion im Editor nicht mehr genügt, gelten Regexe als das Mittel der Wahl.
Das Schreckgespenst verliert viel von seinem Schrecken, sobald Sie verstanden haben, dass im Rahmen von regulären Ausdrücken eine Reihe von Zeichen eine völlig neue Bedeutung bekommen. Darüber hinaus sollten Sie begreifen, dass reguläre Ausdrücke auf Zeichenfolgen operieren. Das heißt, dass Sie damit anhand von Zeichenmengen beziehungsweise Zeichenklassen angeben, wie oft welche Zeichen vorkommen sollen. Die Komplexität kennt hier kaum Grenzen.
Zeichenklassen
Möchte ich beispielsweise die Menge der Vokale der deutschen Sprache als Ausdruck definieren, formuliere ich sie als [AEIOUaeiou]. Die eckigen Klammern kennzeichnen eine Menge, die Buchstaben stehen für die Vokale. Geht es um die Menge der Diphthonge, also der Doppellaute au, äu, ei, eu und so weiter, gestaltet sich die Sache schon schwieriger. Man könnte auch Doppellaute über die Zeichenklassen abbilden, etwa als [aäe][eiu].
Wie Sie sehen, liefert die Aneinanderreihung von Zeichenklassen genau die gewünschte Funktion. Allerdings hat die Sache einen Haken: Der Ausdruck passt nicht nur auf die gewünschten Werte au, äu, ei, eu, sondern ebenso auf ae, äe, äi oder ee. Bevor wir uns der Lösung dieses Problems widmen, beleuchten wir zunächst den Sonderfall der Negation.
Für einen Ausdruck, der Vokale explizit ausschließt, muss man die Zeichenklasse negieren: [^AEIOUaeiou]. Das Caret- oder Zirkumflex-Zeichen dreht die Logik innerhalb der eckigen Klammern um. Statt nur auf Vokale trifft der Ausdruck nun auf alles außer Vokale zu. Alternativ zur Notation mit eckigen Klammern definiert die Spezifikation für wichtige Zeichenklassen Kurzschreibweisen. Dementsprechend lassen sich Dezimalzahlen durch \d, alphanumerische Zeichen per \w und Weißraum per \s ausdrücken [1]. In den Links zum Artikel finden Sie die dazugehörigen Definitionen [2].
Quantifizieren
Mithilfe von Zeichenklassen können Sie eine Menge von Zeichen definieren, die vorhanden sein müssen. Allerdings können Sie auf diese Weise nicht angeben, ob Sie genau einen Vokal möchten oder vielleicht doch mindestens zwei. Dazu benötigen Sie sogenannte Quantoren. Sie ermöglichen, die Anzahl der angeforderten Zeichen festzulegen.
Wollen Sie zum Beispiel eine deutsche Postleitzahl erkennen, müssen Sie einen Ausdruck schreiben, der auf Wörter aus genau fünf Dezimalziffern passt. Eine Zeichenklasse ist schnell bestimmt: [0-9]. Daraufhin könnten Sie durch Aneinanderreihung den entsprechenden Ausdruck [0-9][0-9][0-9][0-9][0-9] festlegen. Das mag im Einzelfall Sinn ergeben, erweist sich aber für große Längen als unpraktikabel und bei variablen Längen als völlig ungeeignet.
Auch dafür bieten reguläre Ausdrücke überaus nützliche Hilfsmittel. Um auszudrücken, dass ein Zeichen höchstens einmal vorkommen darf, verwenden Sie das Fragezeichen, also im PLZ-Beispiel [0-9]?. Hier passt keine oder genau eine Ziffer. Selbstverständlich klappt das genauso mit ganz konkreten Zeichen: E? definiert, dass entweder kein oder genau ein E vorkommen darf. Möchten Sie angeben, dass mindestens ein E gefordert ist, kommt der Quantor + zum Einsatz, also E+.
Ein anderes Beispiel: Sie wollen die Altersangabe innerhalb eines Formulars bereinigen. Wer sein Alter angibt, darf ausschließlich Dezimalziffern verwenden, also [0-9]* oder \d*. Das Sternchen definiert, dass der Ausdruck entweder auf das leere Wort passt oder aber auf beliebig viele Dezimalziffern. Eine solche Altersprüfung würde auch Senioren buchstäblich biblischen Alters akzeptieren, denn \d* erkennt auch 1300.
Offensichtlich fehlt hier eine obere Schranke. Nachdem reguläre Ausdrücke auf Wörtern aus Alphabeten operieren, also keine Zahlensemantik kennen, können Sie nicht mit den vertrauten Kleiner- und Größer-Operatoren < und > arbeiten. Was Sie aber sagen können: Menschen mit einhundert oder mehr Jahren Alter gibt es, Tausendjährige dagegen nicht. Dazu ziehen Sie eine Verkettung von Zeichenklassen heran: [0-9][0-9]?[0-9]?. Die Fragezeichen deklarieren die beiden hinteren Zeichenklassen als optional, was zum gewünschten Ergebnis führt.
Für definierte Wortlängen, etwa von 7 bis 23 Ziffern, gibt es eine eigene Regex-Syntax: [0-9]{7,23}. In geschweiften Klammern legen Sie eine Unter- und Obergrenze fest, wobei Sie auf die Obergrenze verzichten dürfen. Somit sind \d? und [0-9]{0,1} sowie [a-z]+ und [a-z]{1, } äquivalente Ausdrücke.
An dieser Stelle verfügen Sie über das notwendige Instrumentarium, um die ersten sinnvollen Daten auf der Kommandozeile zu ermitteln. Dass Grep Dateien nach Zeichenketten durchsuchen kann, dürfte bekannt sein. Dass das Tool die erweiterte Regex-Spezifikation und sogar teilweise Perl Compatible Regular Expressions (PCRE) beherrscht, ist manchem vielleicht neu. Mit diesem Wissen kann man zum Beispiel über den Befehl aus Listing 1 die Release-Nummern von Debian-Maschinen in Erfahrung bringen (Abbildung 1).
Listing 1
Distro-Version ermitteln
$ grep -E '[0-9]{2,}' /etc/os-release grep -P '\d{2,}' /etc/os-release
Verankern
Wenn niemand festlegt, wo relevante Daten anfangen oder enden, erweist es sich als schwierig, sinnvoll mit diesen Daten umzugehen. Ein prosaischer Text, in dem sich Zahlen und Zusammenhänge verbergen (besser bekannt als Textaufgabe), eignet sich somit logischerweise kaum als Spielfeld für Regexe. Tabellarische Daten dagegen taugen hier ausgesprochen gut.
Reguläre Ausdrücke benötigen oft eine Art Verankerung. In nahezu allen Fällen kommen Sie problemlos zurecht, wenn Sie als Ankerpunkte den Zeilenanfang oder das Zeilenende auswählen. Das gilt vor allem für Dateien in Tabellenform. Dazu gehören Log-Dateien wie /var/log/syslog. Für das Durchsuchen von Protokolldateien nach Fehlern eignet sich Grep bestens. Um die Log-Messages eines bestimmten Tags zu erhalten, benutzen Sie das Werkzeug mit einem Ausdruck, der es anweist, Zeilen auszugeben, die mit dem entsprechenden Datum beginnen: grep '^2023-09-24'. Der vorangestellte Zirkumflex ^ steht für den Zeilenanfang (Abbildung 2).
Abbildung 2: Mithilfe von Grep und einem Regex lassen Sie sich gezielt Zeilen ausgeben, die mit einer bestimmten Datumsangabe anfangen.
Geht es stattdessen um die Fehlerangabe am Zeilenende, lautet der Aufruf grep '/var/log/[bw]tmp$'. Diese Anweisung kombiniert zwei Features, die Sie bereits kennen. Zunächst fasst der Befehl wtmp und btmp über eine Zeichenklasse [wb] zusammen. Abschließend legt er fest, dass hinter [bw]tmp nur noch das Zeilenende kommt: Das erledigt das Dollar-Zeichen $.
Grüppchenbildung
Mit den bisher vorgestellten Funktionen lässt sich schon eine ganze Menge anfangen. Trotzdem fällt es nach wie vor schwer, aus Log-Dateien etwa sämtliche Einträge mit den Schweregraden error und warning zu ermitteln. Ein regulärer Ausdruck wie [ew][ra]r[on][ri]n?g? passt nicht nur auf error und warning, sondern genauso auf earoin. Für solche Situationen benötigen Sie ein Konstrukt, das entweder auf error oder auf warning passt: Gruppen. Die recht intuitive Syntax dazu lautet (error|warning). Die runden Klammern legen fest, dass es sich um eine Gruppe handelt, das Pipe-Zeichen | trennt die Optionen. Ein Regex, der den drei Artikeln des Deutschen entspricht, lautet somit d(er|ie|as).
Auch für Gruppen gelten Quantoren und Anker. Das heißt, Sie können damit auch Wortwiederholungen identifizieren. Der Regex (d(er|ie|as) ?)+ deckt beispielsweise der, die, das, der der und derdiedas ab. Die innere Gruppe erkennt die Artikel, gefolgt von einem optionalen Leerzeichen (?). Die äußere Gruppe gestattet eine Wiederholung der inneren Gruppe (+).
Mithilfe von Gruppen lassen Sie sich außerdem die richtigen Zeilen aus dem Syslog per Grep ausgeben (Listing 2). Das angegebene Suchmuster definiert wieder per Anker das Datum am Zeilenanfang. Anschließend gestattet es per .* beliebige Zeichen (.) in beliebiger Menge (*). Damit lassen sich die Uhrzeitangaben überspringen, um im Anschluss die Anwesenheit von entweder error oder warning zu fordern (Abbildung 3).
Listing 2
Syslog auswerten
$ grep -E '^2023-09-24.*(error|warning)' /var/log/syslog

Abbildung 3: Gruppen erlauben beispielsweise, sich von Grep die richtigen Zeilen aus dem Syslog heraussuchen zu lassen.
Gier
Wie erwähnt deckt der Asterisk beliebig viele Zeichen ab: a* passt auf das leere Wort (das Sternchen erlaubt ja auch keinerlei Vorkommen), auf a, auf aa und so weiter. Wollen Sie das leere Wort ausschließen, bestimmen Sie über den Quantor +, dass das Zeichen mindestens einmal vorkommen muss. So trifft der Ausdruck a+ nicht auf leere Strings zu, jedoch sehr wohl auf a, aa, aaa und so weiter. Obendrein können Sie sich der Angabe von Bereichen bedienen und über {m,n} fordern, dass ein Zeichen, eine Zeichenklasse oder eine Gruppe mindestens m Mal vorkommen muss und höchstens n Mal auftreten darf.
Was aber, wenn es keine Information über die Länge gibt? Was, wenn Sie den Inhalt einer Zelle aus einer CSV-Datei auslesen wollen? Hier liegt im Unklaren, welche Zeichen in den Zellen vorhanden sein können. Deswegen kommt prinzipiell ausschließlich der Punkt . als Muster infrage – er passt ja auf alle Zeichen. Bekannt ist zudem, dass Strichpunkte ; die einzelnen Zellen und Newlines \n die Zeilen trennen (Listing 3).
Listing 3
CSV-Datei
Name;Vorname;E-Mail; Eberhofer;Franz;franz.eberhofer@niederkaltenkirchen.bayern; Flötzinger;Ignaz;info@rohrfrei-floetz.business; Simmerl;Max;metzger@simmerl.shop;
Um hier sämtliche Spalten als Capturing Groups zu ermitteln, verwenden Sie den Ausdruck ((.*);). Das Muster erkennt beliebige Zeichen (.), in beliebiger Anzahl (*). Die Klammern bewirken, dass der gewünschte Text zur Weiterverarbeitung als Gruppe geliefert wird. Als Trennzeichen tritt das Semikolon auf.
Um reguläre Ausdrücke in Python verwenden zu können (Listing 4), importieren Sie zunächst in der ersten Zeile die nötige Bibliothek. Das Kommando aus Zeile 2 erweitert den eben vorgestellten Ausdruck um ein vorangestelltes ?:. Das bewirkt, dass ein Klammernpaar nicht als Capturing Group interpretiert wird, sondern ausschließlich der Zusammenfassung dient. Zeile 3 wendet danach diesen Ausdruck auf die Adresstabelle aus Listing 3 an. In Zeile 4 passiert etwas unerwünschtes: Da das Muster “greedy” formuliert ist, findet Python genau eine Übereinstimmung, und zwar die gesamte Zeile exklusive des abschließenden Strichpunkts.
Listing 4
Non-greedy Regex
>>> import re >>> r = re.compile(r"(?:(.*);)") >>> r.findall(r"Eberhofer;Franz;franz.eberhofer@niederkaltenkirchen.bayern;") ['Eberhofer;Franz;franz.eberhofer@niederkaltenkirchen.bayern'] >>> r = re.compile(r"(?:(.*?);)") >>> r.findall(r"Eberhofer;Franz;franz.eberhofer@niederkaltenkirchen.bayern;") ['Eberhofer', 'Franz', 'franz.eberhofer@niederkaltenkirchen.bayern']
Das entspricht nicht dem, was Sie eigentlich möchten. Um die Inhalte der einzelnen Zellen der Tabelle als Gruppenelemente zu erhalten, müssen Sie wie in Zeile 5 den Ausdruck als non-greedy deklarieren: (?:(.*?);). Hier kommt es auf das Fragezeichen hinter dem Sternchen an. Ein erneuter Aufruf der Funktion findall() liefert das Wunschergebnis, eine Liste (zu erkennen an den eckigen Klammern) mit allen gefundenen Übereinstimmungen.
Fazit
Mit diesem Artikel sind Sie schon ein gutes Stück in der Welt der regulären Ausdrücke herumgekommen und fähig, in Dateien nach recht komplizierten Mustern zu suchen. Doch damit ist die Macht der Regexe und vor allem der damit ausgestatteten Tools längst nicht ausgeschöpft. Im zweiten Teil dieses Artikels werde ich auf fortgeschrittene Features eingehen und dabei vor allem zeigen, wie Sie damit nicht nur Inhalte finden, sondern sie auch für umfassende Änderungen einsetzen können. (csi)
Infos
-
Regex-Zeichenklassen in Grep: https://www.gnu.org/software/grep/manual/html_node/Character-Classes-and-Bracket-Expressions.html
-
Shorthand Character Classes: https://www.regular-expressions.info/shorthand.html






