Reguläre Ausdrücke - praktischer Kurs
Übersicht: Reguläre Ausdrücke
Teil 01
Diskussion 183942 Diskussion 183974
Motivation
Wozu braucht man überhaupt REs?
Einfach gesagt kann man mit REs Text suchen. Was ihr alle kennt ist eine Stringsuche, d.h. eine ganz normale Textsuchfunktion: Man gibt ein Wort ein und sucht dieses im Text. Leider ist diese Suchmöglichkeit etwas begrenzt in ihrer Fähigkeit.
Durchsuchen wir doch mal Text nach ‚Samstag‘. Anschliessend sollten wir wohl eine zweite Suche starten, um den gleichen Text auch nach ‚Sonnabend‘ zu durchsuchen. Es wäre schöner wenn wir beides mit einer Suche erledigen könnten. REs können das. In dem Fall ist es nur eine kleine Arbeitsersparnis.
Wie war das nochmal mit dem Namen Maier? Schreibt man den mit ‚ai‘ oder ‚ei‘ oder mit ‚ay‘ oder ‚ey‘ oder nur mit ‚y‘? Hier sind es schon fünf verschiedene Suchen, die wir ausführen müssen. Mit REs nur eine.
Nun suchen wir nach dem Ortsnamen ‚Au‘. Das ist zwar nur eine Suche, aber sie liefert uns leider auch ‚Auge‘, ‚Auto‘, usw. Wir wollen aber nur Treffer bei denen ‚Au‘ ein eigenständiges Wort ist. Mit REs ist das einfach. Mit Stringsuchen geht es nur falls das jeweilige Programm es zufällig anbietet.
In einem Logfile interessieren uns alle Zeilen, die mit ‚incoming‘ beginnen. Wenn ‚incoming‘ irgendwo sonst innerhalb der Zeile vorkommt interessiert es uns nicht. Eine solche Unterscheidung ist mit einer Stringsuche nicht möglich.
Bis hierhin kann man mit Stringsuchen die Suchprobleme auch lösen, wenngleich mitunter mehrere Suchen nötig sind oder das Suchresultat auch unerwünschte Treffer enthält.
Die folgenden Suchprobleme können mit Stringsuchen nicht gelöst werden, mit REs aber sehr wohl.
- Negativ-Suchen: Alle Zeilen in denen das Wort ‚debug‘ nicht enthalten ist. Stringsuchen können nur positiv suchen, also all die Stellen finden, wo das Suchwort steht, aber sie können nicht die Stellen finden wo es nicht steht.
- Zahlen: Es sollen alle numerischen Werte gesucht werden ... oder noch genauer: alle dreistelligen Zahlenwerte -- unmöglich mit einer Stringsuche.
- Komplexe Formate, wie Datumsformate und überhaupt alles was variable Anteile hat.
REs können all diese Suchprobleme mit Leichtigkeit lösen.
Aufgaben
- Welche Suchprobleme hattest du schon, die du entweder nicht lösen konntest oder für die du viel Handarbeit investieren musstest?
- Kennst du Programme, deren Stringsuche (ohne REs) mehr kann als nur stupide nach einem Suchwort zu suchen? Oft kann man z.B. die Unterscheidung von Gross-/Kleinschreibung an- und abschalten. Was ist dir da schon begegnet?
- An was für Anwendungsfälle denkst du wenn du „Reguläre Ausdrücke“ hörst?
Teil 02
Metazeichen & Escaping
Diese Einheit ist die konzeptionelle Basis zum Lesen von REs.
Eine herkömmliche Stringsuche ist genau genommen eine Substringsuche, d.h. wir suchen den Substring (= Suchwort) im Gesamtstring des Textes. Sowohl der Gesamttext als auch das Suchwort können dabei jedes beliebige Zeichen enthalten. Das Suchwort besteht zudem aus nichts anderem als einer Reihe von Zeichen, die in der Reihenfolge im Text gesucht werden sollen.
Reguläre Ausdrücke bieten darüber hinaus weitere Möglichkeiten. Man kann in ihnen Zusatzinformationen hinterlegen, wie dass es entweder das eine oder das andere Wort ist (Samstag/Sonnabend) oder dass es der eine oder der andere Buchstabe ist (Maier/Meier/Mayer/...). Oder dass ein Wort am Zeilenanfang stehen muss oder dass die Buchstaben ein abgeschlossenes Wort bilden müssen.
Die Herausforderung ist nun, dass ein Regulärer Ausdruck sowohl die gesuchten Buchstaben als auch diese Zusatzinformationen enthält.
Das ist wichtig zu verstehen! Während bei einer Stringsuche das Suchwort nur gesuchte Zeichen enthält, enthält eine RE sowohl gesuchte Zeichen als auch Zusatzinformationen zur Suche.
Begrifflich nennt man die gesuchten Zeichen „literale Zeichen“; sie sollen genau so im Text gesucht werden. Die Zusatzinformationen nennt man „Metazeichen“; sie tragen Informationen bei, die so nicht wörtlich im Text zu finden sind.
Falls man bei einer Stringsuche die Gross-/Kleinschreibungsunterscheidung aktivieren und deaktivieren kann, so ist diese Option auch eine Metainformation. Jedoch wird diese nicht innerhalb des Suchwortes umgesetzt, sondern als Checkbox oder Schalter ausserhalb des Suchfelds. In diesem Falle ist die Trennung zwischen literalen Zeichen und Metainformation einfach: sie ist strukturell im User Interface gegeben.
Bei REs dagegen sind beide Informationen in einem einzigen Ausdruck (also einem Wort, einer Zeichenfolge) vermischt. Einerseits macht es das beqüm, da man nur einen einzigen RE-String eingeben muss, der schon alles enthält, und nicht in jedem Programm weitere User-Interface-Elemente zur Angabe der weiteren Optionen braucht, andererseits macht gerade diese Vermischung REs so schwer verständlich.
Unsere Hauptaufgabe beim Lesen von REs wird folglich sein, im Ausdruck Literale und Metazeichen zu unterscheiden.
Wie kann man zwei Arten von Zeichen in einem Ausdruck unterscheiden?
Im einfachsten geht das wenn die zwei Arten von Zeichen disjunkt sind, d.h. ohne überschneidung. In dieser Weise nutzen Mathematiker und Physiker gerne griechische Buchstaben für ihre Variablen. Es ist dann sofort klar, ob etwas eine Variable oder normaler Text ist.
Eine andere Art der Unterscheidung kann durch das Formatieren der Zeichen erreicht werden. Wenn man Fett- und Kursivschrift oder Farben zur Verfügung hat, kann man damit die sonst gleichen Zeichen unterscheidbar machen.
Bei Regulären Ausdrücken haben wir leider keine dieser beiden Möglichkeiten zur Verfügung. Zum einen können alle Zeichen im Text und damit literal im Regulären Ausdruck vorkommen, so dass wir keine weitere Zeichenmenge für die Metazeichen übrig haben, zum anderen sind die REs selbst reiner Text und können demnach nicht typographisch formatiert werden. Wir brauchen folglich einen anderen Mechanismus, um die beiden Zeichenarten zu trennen.
Der in solchen Fällen genutzte Mechanismus heisst Escapen.
Escapen funktioniert folgendermassen:
- Wir wählen ein Escapezeichen. (Man nimmt möglichst ein Zeichen, das selten im Text vorkommt, aber prinzipiell ist jedes Zeichen möglich.)
- Entweder die Literale oder die Metazeichen sind die Standardzeichen. Die andere Zeichenart muss escapet werden. D.h. vor jedes Zeichen der anderen Zeichenart muss ein Escapezeichen kommen.
- Um das Escapezeichen literal zu erhalten muss es (mit sich selbst) escapet werden. (Zwei aufeinander folgende Escapezeichen stehen immer für ein literales Escapezeichen.)
Nun haben wir einen Mechanismus mit dem wir jedes Zeichen in zwei Arten interpretieren können: In der ersten Art wenn es nach einem Escapezeichen kommt und in der zweiten Art wenn es nicht nach deinem Escapezeichen kommt.
Wir können das Escapezeichen dabei verstehen als einen Hinweis: Achtung, das nächste Zeichen gehört zur anderen Zeichenart. (Wikipedia: „an escape character is a character that invokes an alternative interpretation on the following character“.)
Die Unterscheidung und Identifizierung von Literalen und Metazeichen in einem Regulären Ausdruck ist von zentraler Bedeutung. Sie ist die Basis des Verständnisses von REs. Dazu muss das Konzept des Escapens gut verstanden sein.
Hier folgen nun verschiedene Übungen, um mit diesem Konzept warm zu werden. Am besten, ihr arbeitet mit Bleistift, Papier und Farben. Bei den letzten Aufgaben könnt ihr dann auch selber programmieren (wenn ihr das könnt) und eure vorigen Lösungen damit nochmal abgleichen.
Aufgaben
- Das Escapezeichen ist der Unterstrich (_). Literale Zeichen sind die Standardzeichen. Schreibe den Ausdruck für das literale Wort: „Haus“.
- Das Escapezeichen ist der Unterstrich (_). Metazeichen sind die Standardzeichen. Schreibe den Ausdruck für das literale Wort: „Haus“.
- Das Escapezeichen ist das grosse X. Metazeichen sind die Standardzeichen. Schreibe den Ausdruck für das literale Wort: „Haus“.
- Das Escapezeichen ist das kleine a. Literale Zeichen sind die Standardzeichen. Schreibe den Ausdruck für das literale Wort: „Haus“.
- Suche dir ein Escapezeichen aus. Literale Zeichen sind die Standardzeichen. Schreibe die Zeichenfolge: Literales ‚D‘, literales ‚F‘, literales ‚D‘, literales ‚E‘, Metazeichen ‚J‘, literales ‚R‘, literales ‚E‘, literales ‚s‘, literales ‚!‘, Metazeichen ‚J‘, Metazeichen ‚@‘.
- Schreibe den gleichen Text aus (5) aber mit Metazeichen als Standardzeichen.
- Das Escapezeichen ist die öffnende runde Klammer (‚(‘). Suche dir aus, welche Zeichenart die Standardzeichen sind. Schreibe einen Ausdruck mit nur öffnenden und schliessenden runden Klammern und erkläre ihn anschliessend.
- Das Escapezeichen ist das Komma (,). Literale Zeichen sind die Standardzeichen. Beschreibe den Ausdruck (vgl. (5)): „A,BCD,E,FG,,H“
- Das Escapezeichen ist das Komma (,). Metazeichen sind die Standardzeichen. Beschreibe den Ausdruck: „hello, world“
- Das Escapezeichen ist das kleine a. Literale Zeichen sind die Standardzeichen. Beschreibe den Ausdruck: „Haus“
- Das Escapezeichen ist das kleine a. Literale Zeichen sind die Standardzeichen. Beschreibe den Ausdruck: „Haaus“
- Das Escapezeichen ist das kleine a. Metazeichen sind die Standardzeichen. Beschreibe den Ausdruck: „Haaus“
- Schreibe ein Programm, das literalen Text passend escapet. Das Escapezeichen und die Entscheidung, welche Zeichenart die Standardzeichen sind, sollen variabel sein (z.B. CLI-Argumente). Prüfe damit deine Lösungen für die Aufgaben (1) bis (7).
- Auf die vorigen Aufgabe aufbauend: Schreibe ein Programm, das den escapeten Text wieder einliest und eine sprachliche Beschreibung davon ausgibt, ähnlich wie in (5). Prüfe damit deine Lösungen für die Aufgaben (8) bis (12).
Teil 03
egrep
Nun geht es an die Praxis. Wir verwenden hierfür nun egrep, das auf POSIX Extended Regular Expressions (EREs) basiert.
Das Escapezeichen ist bei allen üblichen RE-Varianten der Backslash (\).
Was die Literale und Metazeichen angeht, ist es leider nicht so schön klar wie wir es in der vorigen Einheit betrachtet haben. Dort waren jeweils die unescapeten Zeichen Literale und die escapeten Zeichen Metazeichen, bzw. umgekehrt. In der Praxis sind diese zwei Zeichenarten nicht so klar getrennt, sondern vermischt. Es ist weiterhin so, dass das Escapen die Zeichenart ändert, aber sowohl unescapete Zeichen als auch escapete Zeichen können sowohl Literale als auch Metazeichen sein. Das macht die Sache etwas schwieriger. Bei den unescapeten Zeichen sind also manche Literale und manche Metazeichen; welche was sind muss man auswendig lernen. Immerhin sind bei EREs alle escapeten Zeichen Literale. (Das ist bei BREs nicht so.) Allerdings kann man auch wiederum nicht einfach ein beliebiges Zeichen escapen, um es zu einem Literal zu machen. Escapet man beispielsweise einen Buchstaben, so ist das Verhalten undefiniert. Und da POSIX nur eine nachträgliche Zusammenfassung des grössten gemeinsamen Nenners der vielfältigen Unix-Realität ist, verhalten sich die konkreten Programme oft nochmal ein bisschen anders als es in POSIX beschrieben ist.
Konkret heisst das, dass man für jede RE-Variante und letztlich für jedes Programm separat auswendig lernen muss, welche Zeichen Sonderbedeutungen haben!
Das ist wohl die grösste Schwierigkeit bei der Verwendung von REs. Wir gehen vorerst von EREs aus (z.B. in egrep, awk), die in der Hinsicht etwas einfacher sind als BREs (z.B. in grep, sed).
In EREs steht jedes Zeichen für sich selbst, ausser es ist ein Zeichen mit Sonderbedeutung. Die Zeichen mit Sonderbedeutung sind:
\.[]()*+?{}|^$
... wobei sie leider auch nicht an jeder Stelle in einem Ausdruck eine Sonderbedeutung haben. Und Zeichen wie - und , haben nur an bestimmten Stellen eines Ausdrucks eine Sonderbedeutung. Dies ist nunmal so und ich kann es euch leider nicht einfacher machen. :-(
Kurzum können wir uns aber merken, dass alle Buchstaben, Zahlen und z.B. Unterstrich, Apostroph und Anführungszeichen immer literal für sich selbst stehen.
Obige Sonderzeichen sind an den meisten Stellen Metazeichen. Wenn wir sie escapen, werden sie literal interprätiert.
Aufgaben
- Schreibe einen egrep-Ausdruck der die Zeichenkette „Reiter“ matcht. Teste ihn entweder so: Wenn das Wort „Reiter“ ausgegeben wird, hat der Ausdruck gematcht. Oder wende den egrep-Befehl auf den Text schwäbische-kunde.txt (s.u.) an. Dort werden dann alle Zeilen ausgegeben, die „Reiter“ enthalten. Wenn du `egrep -o' verwendest, dann werden nur die gematchten Worte selbst ausgegeben.
user@debian:~$ echo "Reiter" | egrep 'DEIN_AUSDRUCK'
- Schreibe einen egrep-Ausdruck der die Zeichenkette „Viel Steine“ matcht.
- Schreibe einen egrep-Ausdruck der die Zeichenkette „heil'gen“ matcht.
- Schreibe einen egrep-Ausdruck der die Zeichenkette „Er sprach: "Sagt an,“ matcht.
- Schreibe einen egrep-Ausdruck der die Zeichenkette „eben.“ matcht. Testet mit ‚-o‘ und ohne.
- Schreibe einen egrep-Ausdruck der die Zeichenkette „?“ matcht.
- Vergleiche diese egrep-Ausdrücke mit der Verwendung von fgrep, welches eine simple Stringsuche umsetzt. Bei fgrep werden alle Zeichen literal interprätiert. Wo liegen die Unterschiede? (Bis zu diesem Zeitpunkt wird euch egrep noch nicht besser als fgrep vorkommen. Das aendert sich bald.)
- Verwende egrep für ein Stück Quellcode, der Sonderzeichen enthält. Schreibe dafür egrep-Ausdrücke, die Sterne, Klammern und Punkte matchen.
Inputtext: schwaebische-kunde.txt 41651
Teil 04
Nachdem ihr nun mit egrep und dem Escapen der dort geltenden Zeichen mit Sonderbedeutung vertraut seid, kommen wir nun zu den ersten Operatoren. Diese machen egrep mächtiger als fgrep.
Konkatenation
Der einfachste Operator in REs ist die Konkatenation. Auf Deutsch: die Aneinandereihung. Der Operator ist deshalb einfach, weil er implizit ist. Wenn wir zwei Ausdrücke hintereinander schreiben, dann bilden sie damit einen grösseren Ausdruck. Es gibt kein Operator(meta)zeichen, das wir verwenden müssten.
Beispielsweise ist „Schwaben“ ein Ausdruck, den wir mit egrep in der Datei schwaebische-kunde.txt matchen können. Gleichermassen ist „streiche“ ein solcher Ausdruck. Diese beiden können wir auch einfach hintereinanderstellen, um einen konkatenierten Ausdruck zu bilden: „Schwabenstreiche“.
Das mag trivial klingen, trotzdem ist es ganz gut, sich das zu vergegenwärtigen. Im kleinsten Fall ist ein Ausdruck ein einziges Zeichen. So ist der Ausdruck „Schwaben“ ja auch schon eine Konkatenation von acht einzelnen Ausdrücken mit je einem literalen Zeichen.
Eine Stringsuche ist auch nur eine Konkatenation literaler Zeichen. Derart waren all die Aufgaben der vorigen Einheit. Die Möglichkeiten der Stringsuche enden dort; bei uns geht es jetzt erst richtig los! ;-)
Alternation
Der zweite (und erste wirklich interessante) Operator ist die Alternation, die zwei Ausdrücke in eine Oder-Beziehung setzt. Wenn ich also mit einem Ausdruck sowohl „Schwabe“ als auch „Held“ suchen will, dann kann ich die beiden Ausdrücke per Alternation verbinden.
Die Alternation wird durch den Operator Pipe (|) angegeben. Das Pipe-Zeichen ist also ein Sonderzeichen. Tritt es in der RE auf, dann wird es als Operator interpretiert. Will man es literal matchen, dann muss man es escapen. (Achtung: Je nach RE-Variante kann das auch umgekehrt sein.)
Der Reguläre Ausdruck für die erwähnte Suche wäre damit:
Schwabe|Held
Dies ist auch mehrfach möglich:
Spiess|Säbel|Pfeil|Schwerdt
(Hier nochmal der Text vom vorigen Teil, um die Beispiele anzuwenden: TODO Inputtext aus 3)
Unterausdrücke
Um nicht nur eine Ebene von Alternation zu haben, sondern diese verschachteln zu können, brauchen wir eine Möglichkeit, einen komplexen Ausdruck als Einheit zu betrachten. Das geht indem wir ihn in runde Klammern einfassen.
Runde Klammern ( und ) sind also Sonderzeichen. Wenn sie in der RE auftreten, dann werden sie als Metazeichen interpretiert. Will man sie literal matchen, dann muss man sie escapen. (Achtung: Je nach RE-Variante kann das auch umgekehrt sein.)
Wir könnten die Ausdrücke zwar immer auch ausschreiben, aber es ist weniger Schreibarbeit wenn wir gleiche Teile aus der Klammer rausziehen können.
Nehmen wir beispielsweise alle Kombinationen von einem zu- und abnehmenden Mond bzw. Wind. Ausgeschrieben könnte man das folgendermassen zu einem Regulären Ausdruck zusammenbauen:
zunehmender Mond|abnehmender Mond|zunehmender Wind|abnehmender Wind
Das ist eine ganze Menge Text, in der sich viel wiederholt. Mit Unterausdrücken geht es auch kompakter:
(zu|ab)nehmender (Wind|Mond)
Hier gilt die Alternation jeweils nur auf die geklammerten Teile.
Man kann Alternationen auch Schachteln:
Erd(apfel|birne)|Kartoffel
Generell kann man jeden vollständigen Regulären Ausdruck in runde Klammern fassen und dann als Unterausdruck in einem anderen Ausdruck verwenden.
Rekapitulation und eine Bemerkung
Hier ein etwas angepasster und mit Anmerkungen versehenen Ausschnitt aus der Manpage regex(7), der unseren Wissensstand bis zum aktuellen Punkt zusammenfasst:
A Regular Expression is one or more branches, separated by '|'. It matches anything that matches one of the branches. [= Alternation]
A branch is one or more pieces, concatenated. It matches a match for the first, followed by a match for the second, etc. [= Konkatenation]
A piece is an atom [...].
An atom is a regular expression enclosed in "()" (matching a match for the regular expression) [= Unterausdruck], [...], a '\' followed by one of the characters "^.[$()|*+?{\" (matching that character taken as an ordinary character) [= escapetes Metazeichen das dann literal interpretiert wird], [...], or a single character with no other significance (matching that character) [= Literal]. [...]
(Die Lücken in dieser Beschreibung werden wir in den nächsten Einheiten nach und nach füllen.)
In der gleichen Manpage steht auch folgender Abschnitt, der zwar auf die regex-Lib von Henry Spencer zutrifft aber leider nicht generell auf POSIX oder sonstige RE-Varianten:
An atom is [...]
a '\' followed by one of the characters "^.[$()|*+?{\" (matching that character taken as an ordinary character),
a '\' followed by any other character (matching that character taken as an ordinary character, as if the '\' had not been present)
Der erste Satz gilt generell für alle EREs. Der zweite leider nicht. Wäre es
anders, dann hätte ich anfangs die Dinge nicht so kompliziert machen müssen,
denn dann hätte ich sagen koennen:
- Das Escapezeichen ist der Backslash.
- Metazeichen sind die Standardzeichen.
- Jedes escapete Zeichen steht literal für sich selbst.
Das wäre klar und konsistent gewesen. Damit wären die beiden Zeichenklassen (Literale und Metazeichen) klar getrennt gewesen.
Der Bequemlichkeit halber kann man sich mit folgender Zusatzregel dann noch Tipparbeit sparen:
- Metazeichen, die keine Sonderbedeutung haben, stehen automatisch als Fallback auch literal für sich selbst. (‚a‘ und ‚\a‘ sind also beide ein literales ‚a‘.)
Leider gilt das aber eben nur für Programme, die Henry Spencers regex-Lib verwenden. Für andere RE-Implementierungen ist die Situation etwas komplizierter, wenn man sie konzeptionell beschreiben will, da es implementierungsabhängig ist, was ‚\a‘ bedeutet. In der Bedienpraxis macht das aber selten einen relevanten Unterschied.
Aufgaben
- Schreibe einen egrep-Ausdruck, der sowohl eine Alternation enthält als auch das Pipezeichen literal matcht und wende ihn auf einen dazu passenden Eingabetext an.
- Finde ein inhaltlich sinnvolles Beispiel für eine zwei- oder dreifach verschachtelte Alternation. ;-)
- Schreibe einen egrep-Ausdruck (nur mit Alternation und Unterausdrücken), um die Schreibweisen Maier, Meier, Mayer, Meier zu matchen.
- Ergänze (3) um die Schreibeweise Myer.
- Finde alternative Ausdrücke für (3) und (4) (ohne andere RE-Operatoren zu verwenden, sondern nur indem du anders gruppierst).
- Versuche diese Aufgaben auch mit fgrep umzusetzen. Was sind deine Erkenntnisse?
- Braucht man runde Klammern um den gesamten Ausdruck wenn man eine Alternation (auf oberster Ebene) verwendet?
Teil 05
Zeichenklassen
Zeichenklassen kann man sich so ähnlich wie Alternationen vorstellen, bloss für einzelne Zeichen statt für ganze Ausdrücke. Von der Ausdrucksmöglichkeit sind sie theoretisch nicht unbedingt nötig wenn man Alternation und Unterausdrücke hat, aber in der Praxis sind sie ungeheuer praktisch. Ausserdem sind sie ein Standardfeature, das man in in allen RE-Varianten identisch wiederfindet.
Wenn wir nach „Samstag“ oder „Sonnabend“ suchen wollen, dann brauchen wir dafür eine Alternation: ‚Samstag|Sonnabend‘.
Wenn wir nach „Maier“ oder „Meier“ suchen wollten, dann können wir das ebenfalls mit einer Alternation machen: ‚Maier|Meier‘. Hier ist die Abweichung der zwei Varianten jedoch nur gering. Den gemeinsamen Teil können wir darum auch nur einmal schreiben und einen Unterausdruck nutzen: ‚M(a|e)ier‘.
Wenn nun alle Teilausdrücke der Alternation nur genau ein Zeichen sind, dann können wir stattdessen auch eine Zeichenklasse verwenden:
M[ae]ier
Eine Zeichenklasse steht immer für genau ein einziges Zeichen. Dieses kann ein beliebiges der aufgelisteten Zeichen sein.
Eine Zeichenklasse wird mit eckigen Klammern geschrieben, zwischen denen alle Zeichen aufgeführt werden für die die Zeichenklasse stehen kann. In diesem Beispiel kann die Zeichenklasse also entweder für ‚a‘ oder für ‚e‘ stehen.
Metazeichen
Innerhalb von Zeichenklassen werden fast alle Zeichen, also auch die normalen Metazeichen (wie Pipe und Klammern und auch das Escapezeichen Backslash!), literal interpretiert.
Speziell sind in Zeichenklassen nur wenige Zeichen, auf die ich nun nach und nach komme.
Die schliessende eckige Klammer (]). Diese markiert das Ende der Zeichenklasse. Wenn die schliessende eckige Klammer von der Zeichenklasse literal gematcht werden soll, dann muss sie als erstes Zeichen verwendet werden:
[]abc]
Diese Zeichenklasse steht für eines der vier Zeichen: schliessende eckige Klammer, ‚a‘, ‚b‘ oder ‚c‘.
Die schliessende eckige Klammer hat also an jeder Stelle einer Zeichenklasse eine Metabedeutung (nämlich die Zeichenklasse zu beenden), ausser als erstes Zeichen innerhalb der Zeichenklasse.
Negation
Zeichenklassen können eines was Alternationen nicht können, nämlich den Match invertieren. Eine Zeichenklasse kann man ausdrücken: Jedes Zeichen *ausser* den angegebenen. Dazu muss als erstes Zeichen in der Zeichenklasse ein Circumflex (Caret) stehen. Dieser negiert die Zeichenklasse. Die Zeichenklasse matcht dann alle Zeichen ausser die auf den Circumflex folgenden.
Eine Zeichenklasse, die alle Zeichen, ausser den Satzzeichen Punkt, Ausrufezeichen und Fragezeichen matcht sieht folgendermassen aus:
[^.!?]
Der Circumflex ist also auch ein Zeichen, das innerhalb einer Zeichenklasse ein Metazeichen ist, allerdings nur wenn er als erstes Zeichen in der Zeichenklasse kommt. An jeder anderen Stelle steht er literal für sich selbst.
Diese Zeichenklasse matcht auf die Satzzeichen und den Circumflex:
[.!?^]
Diese ebenfalls:
[.^!?]
Bereiche
Eine Zeichenklasse, die eine beliebige Ziffer matcht kann man noch einigermassen hinschreiben:
[0123456789]
Aber bei einer Zeichenklasse, die einen beliebigen Buchstaben matcht, wird das sehr unpraktisch. Darum gibt es hierfür die Möglichkeit, Bereiche mittels eines Minuszeichens anzugeben:
[a-z]
Da die Reihenfolge der Zeichen vom Zeichensatz abhängt, sollte man dies nur auf US-ASCII anwenden ... genauer gesagt, sind eigentlich nur drei Bereiche (und Teile davon) einigermassen verlässlich:
- 0-9 für die Ziffern
- A-Z für die Grossbuchstaben
- a-z für die Kleinbuchstaben
Wobei bei den Buchstaben nicht unbedingt eindeutig ist, ob Umlaute enthalten sein werden oder nicht. Solange man sich nur im Rahmen von US-ASCII bewegt, klappt das alles gut, darüber hinaus sollte man nicht zu genaue Erwartungen haben. Diese Ungenauigkeiten kann man in der Praxis meist problemlos in Kauf nehmen, angesichts der praktischen Bequemlichkeit von Bereichen in Zeichenklassen.
Will man sowohl Gross- als auch Kleinbuchstaben matchen, so muss man beide Bereiche separat angeben:
[A-Za-z]
Dies steht für ein Zeichen, das ein beliebiger Gross- oder Kleinbuchstabe ist.
Ein Minuszeichen bildet immer dann einen Bereich, wenn es in einer Zeichenklasse zwischen zwei literalen Zeichen steht. In diesen Fällen ist es ein Metazeichen. Steht das Minuszeichen ganz am Anfang oder ganz am Ende, so kann es keinen Bereich aufspannen und steht dann literal für sich selbst.
Vordefinierte Bereiche
Da die Internationalisierung Zeichenklassenbereiche erschwert hat und weil man immer wieder bestimmte Bereiche braucht, die man ggf. auch nicht schnell mal hinschreiben kann (wie alle druckbaren Zeichen, beispielsweise), hat POSIX einige Zeichenklassenausdrücke eingeführt:
[:alnum:] | alphanumerische Zeichen | |
[:alpha:] | Buchstaben | |
[:blank:] | Leerzeichen und Tab | |
[:cntrl:] | Steuerzeichen | |
[:digit:] | Ziffern | |
[:graph:] | druckbare Zeichen ausser Whitespace | |
[:lower:] | Kleinbuchstaben | |
[:print:] | alle druckbaren Zeichen | |
[:punct:] | Punktuationszeichen, Klammern, etc. | |
[:space:] | Whitespace | |
[:upper:] | Grossbuchstaben | |
[:xdigit:] | Hexadezimalziffern |
TODO schöner formatieren
Jeder davon steht für eine Liste von Zeichen bzw. einen Bereich. So steht, vereinfacht gesagt, ‚[:lower:]‘ für ‚a-z‘. Man achte hier auf die eckigen Klammern! Eine Zeichenklasse, die die Kleinbuchstaben matcht kann so aussehen:
[a-z]
oder so:
[[:lower:]]
Eine Zeichenklasse, die sowohl Kleinbuchstaben als auch Ziffern matcht kann eine der folgenden Formen haben:
[a-z0-9] [[:lower:][:digit:]] [[:lower:]0-9] [01234[:lower:]5-9] usw.
Zeichenklassen sind Mengen, d.h. die Reihenfolge ihrer Elemente ist egal.
POSIX beschreibt noch weitere Spezialangaben für innerhalb von Zeichenklassen ([= =] und [. .]), aber die werden seltenst gebraucht und hier darum ausgelassen.
Reihenfolge
Bei der Reihenfolge innerhalb der Zeichenklasse sind die drei Zeichen mit Sonderbedeutung ^ - ] relevant. Der Circumflex hat nur an wirklich allererster Stelle seine Sonderbedeutung. Die schliessende eckige Klammer verliert ihre Sonderbedeutung als erstes Zeichen auch noch hinter dem Circumflex. Wenn also beides auftritt, dann muss der Circumflex zur Negation zuerst kommen. Das Minus verliert seine Sonderbedeutung als letztes Zeichen (wo es auch mit keinem anderen Sonderzeichen kollidieren kann). Oder, falls keine schliessende eckige Klammer vorhanden ist, als erstes Zeichen (ggf. nach einem Negations-Circumflex).
Aufgaben
Schreibe Reguläre Ausdrücke, wenn möglich, dann gerne verschiedene Varianten. Setzte sie mit egrep um und teste sie gegen selbst ausgedachten Input.
- RE für eine Hexadezimalziffer.
- RE für eine dreistellige Ganzzahl
- RE für eine dreistellige Ganzzahl mit (Pflicht-)Vorzeichen.
- RE für einen Euro-und-Cent-Betrag kleiner 10,-.
- RE für die 31 Tage des Januars (zweistelliges Format mit führender Null). Zwei Varianten.
- RE für die 28 Tage des Februars (zweistelliges Format mit führender Null). Zwei Varianten.
- RE für ein beliebiges Zeichen, das kein Buchstabe ist.
- RE für eine beliebige Ziffer. Drei verschiedene Varianten.
- RE für ein Zeichen, das kein Circumflex (^) ist.
- RE für eine Zeichenklasse, die nur einen Circumflex matcht.
- RE für ein Minus oder eine eckige Klammer.
- RE die alles ausser einer schliessenden eckigen Klammer oder einem Circumflex matcht.
- Schreibe zwei verschiedene REs, die beide einen Backslash matchen.
- Was matcht die Zeichenklasse ‚[A-z]‘ ausser Buchstaben sonst noch? (Zeichensatz: US-ASCII)
- RE, die einen Tab matcht. Versuche mehrere Varianten zu finden.
- Schreibe einen egrep-Ausdruck (nun auch mit Zeichenklassen), um die Schreibweisen Maier, Meier, Mayer, Meier und Myer zu matchen.
- Schreibe eine RE, die den String „8)“ matcht. ;-) (Das ist eine ernst gemeinte Aufgabe und ist ist mit den bisher gelernten RE-Möglichkeiten umsetzbar. Anmerkung: im Thread hat diese Aufgabe die Nummer 18)
Teil 06
Im Falle von EREs haben wir nun alle Möglichkeiten kennengelernt mit denen man literale Zeichen matchen kann ... mit einer Ausnahmen, die nun folgt:
Punkt
Eigentlich hätte das Metazeichen Punkt thematisch besser zu den Zeichenklassen gepasst, da er nichts anderem entspricht als einer Zeichenklasse, die alle Zeichen enthält. Oder anders gesprochen: der Punkt matcht ein beliebiges Zeichen.
Statt dieser Zeichenklasse (die alle Zeichen enthält):
[[:print:][:cntrl:]]
kann man also auch einfach
.
schreiben. Das ist ungemein praktisch, gerade da REs in der Praxis meist nicht vollkommen exakt sein müssen und der Punkt einem dann eine Menge Tipparbeit spart.
Der vorige Teil mit den Zeichenklassen war allerdings schon sehr lang und der Punkt passt von den Anwendungsszenarien auch gut zu den hier nun vorgestellten Quantoren, da man ihn dort am öftesten antrifft.
Aber rekapitulieren wir erst einmal kurz:
- Wir können literale Zeichen direkt matchen und sie ggf. escapen, falls es Metazeichen sind.
- Wir können mit Zeichenklassen eines von mehreren Zeichen bzw. nun mit dem Punkt ein beliebiges Zeichen matchen. Der Punkt und die vordefinierten Zeichenklassenbereiche erlauben es uns auch und unbekannte Zeichen zu matchen.
- Wir können Alternationen nutzen, um eine von mehreren REs zu matchen. Mittels Klammern geht das auch für Teil-REs.
Was wir noch nicht können ist eine Variabilität in der *Länge* des gematchten Textes. Bislang können wir anhand der RE schon im Voraus sagen wie viele Zeichen die Matches umfassen werden. Durch Alternationen können das verschiedene Längen sein, aber wir kennen sie alle.
Wollen wir aber beispielsweise „beliebig viel Whitespace“ matchen, dann ist uns das bislang noch nicht möglich. Diese Möglichkeit lernen wir nun kennen.
Quantoren
Mit Quantoren kann man angeben wie oft die davor stehende Teil-RE wiederholt vorkommen darf bzw. muss. Es gibt vier Arten von Quantoren in EREs. Sie alle beziehen sich auf die direkt davor stehende RE-Komponente, das kann ein literales Zeichen sein oder eine Zeichenklasse oder ein geklammerter Unterausdruck. Sie beziehen sich also auf die kleinstmögliche Sinneinheit, die vor ihnen steht.
Stern
Der Stern ist der einzige Quantor, der bei allen RE-Varianten verfügbar ist. Er ist stets ein Metazeichen und wird von allen Implementierungen unterstützt.
Ein Stern bedeutet, dass die davorstehende Sinneinheit beliebig oft (null bis unendlich) wiederholt werden kann.
Die RE:
Aa*h!
matcht damit die Worte:
Ah! Aah! Aaah! Aaaah! Aaaaah! Aaaaaah! Aaaaaaah! Aaaaaaaah! Aaaaaaaaah! usw.
Der Stern bezieht sich hier nur auf das ‚a‘, da das die kleinste Sinneinheit ist.
Plus
Oft passender aber nicht in allen RE-Implementierungen verfügbar (oder es muss escapet werden) ist das Plus. Dieses ähnelt dem Stern, steht aber für mindestens ein Vorkommen bis unendlich viele.
Die RE:
A[ua]+!
matcht damit auf:
Au! Aa! Auu! Aua! Aau! Aaa! Aaua! Auau! Auuu! Aaaa! Aaau! Aauuauauuauauuauauuauuuuauuuauaaaauuuuuu! usw.
Sie matcht aber nicht auf:
A!
Fragezeichen
Auch das Fragezeichen ist nicht in allen RE-Implementierungen verfügbar (oder muss escapet werden). Es steht für eine „Wiederholung“ von null- oder einmal und damit eigentlich nicht für eine Wiederholung sondern macht die vorhergehende Teil-RE optional.
So können mit folgender RE:
colou?r
die die britische als auch die amerikanische Schreibweise gemacht werden:
colour color
Das Fragezeichen kann stets durch eine Alternation mit einem leeren Teil (was jedoch nicht vollständig portabel ist) ersetzt werden:
colo(u|)r
Intervalle
Zuletzt die allgemeinste Form eines Quantors, der alle drei obigen Quantoren ersetzen kann. Intervalle werden mit geschweiften Klammern notiert. (Von wenigen Ausnahmen abgesehen sind Intervalle überall verfügbar. Mit ihnen kann man also auch Plus und Fragezeichen ersetzen, falls es diese in einer RE-Variante nicht gibt. Die geschweiften Klammern müssen in manchen RE-Varianten escapet werden.)
Die geschweiften Klammern können:
- ... genau eine Zahl enthalten. Das bedeutet, dass der davorstehende Teil-Ausdruck genau so oft wiederholt werden muss. Diese Form ist nur Syntactic Sugar, der Schreibarbeit spart, denn den Ausdruck:
[0-9]{3}
kann man natürlich jederzeit auch so schreiben:[0-9][0-9][0-9]
Hier ein Beispiel mit Unterausdruck für eine Zeile des langen Schlussteils eines bekannten Liedes:Na(, na na){4}, hey, Jude
- ... mit einem Komma getrennt, zwei Zahlen enthalten. Diese legen dann die Mindest- und Maximalzahl an Wiederholungen fest:
(Na(, na na){4}, Hey, Jude){8,24}
- ... eine Zahl und ein Komma enthalten. Das bedeutet, dass es keine Obergrenze gibt.
Wenn die Band also so richtig im Flow und das Publikum gut ist, dann geht auch mit offenem Ende:(Na(, na na){4}, Hey, Jude){8,}
:-D
Gierigkeit
Bei Quantoren (aber auch bei der Alternation) hat die RE-Engine manchmal die Wahl, mehr oder weniger zu matchen. Es ist festgelegt, dass REs immer so viel wie möglich matchen. POSIX nennt das den frühesten längsten Treffer.
Die RE:
a*
wird folglich so viele ‚a‘ wie möglich matchen. Wenn es keine ‚a‘ hat, dann wird sie einen leeren String matchen, aber wenn sie ‚a‘ findet, dann beginnt sie beim ersten, das sie findet, und nimmt ab da so viele wie sie kann.
Aufgaben
- Matche mit einer RE allen Input.
- Extrahiere mit alle Zahlen aus einem Text.
user@debian:~$ egrep -o
- Matche in einem XML-Input mit einer RE (ohne Alternation) die Beginn- und Ende-Tags
<h1>
und</h1>
- Schreibe die entsprechenden Intervallausdrücke für die drei anderen Quantoren: * + ?
- Erkläre die RE: ‚**‘. Auf was wird sie matchen?
- „Bananen?“ -- Dies ist keine Frage, sondern?
- Matche alles bis „ENDE“ (inklusive).
- Matche den Inhalt eines Single-Quoted Strings (dieser kann das Single-Quote nicht enthalten).
- Matche einen Satz, d.h. von einem Grossbuchstaben bis zum nächsten Satzendezeichen. (Zweimal drüber nachdenken und testen. Der erste Ansatz ist nicht unbedingt ausreichend!)
- Matche grosse Geldwerte mit Tausendertrenner.
- Finde ein sinnhaftes Praxisbeispiel für eine RE mit Punkt aber ohne Quantoren.
Teil 07
Anker
Kommen wir nun zu den letzten zwei Metazeichen von EREs: Anker. Mit ihnen kann man einen Ausdruck am Zeilenanfang bzw. Zeilenende verankern. Anker sind reine Metainformation, sie matchen auf keine Zeichen, sondern sagen nur etwas darüber aus, wo auf der Zeile der Ausdruck gematcht werden kann.
Es gibt zwei Anker in EREs:
Der Zeilenanfangsanker wir mit dem Circumflex (^) notiert. Die RE:
^foo
matcht „foo“ nur wenn es am Zeilenanfang steht.
Der Zeilenendeanker wird mit dem Dollarzeichen ($) notiert. Die RE:
bar$
matcht „bar“ nur wenn es am Zeilenende steht.
Im Falle von EREs ist es erlaubt, diese Anker auch in die Mitte einer RE einzubaün, auch wenn sie dort niemals matchen können. (Das ist bei anderen RE-Varianten anders.)
Damit haben wir nun alle Funktionen von POSIX EREs kennengelernt, lediglich Collation Symbols
([. .])
und Equivalence Class Expressions
([= =])
innerhalb von Zeichenklassen haben wir ausgelassen. Diese habe ich bislang auch noch nie in der Praxis gesehen.
POSIX-Beschreibung von EREs: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_04
Präzedenz
Von hoch bis niedrig
- Collation-related bracket symbols innerhalb von Zeichenklassen
[==] [::] [..]
- Escapete Metazeichen
\
- Zeichenklassen
[]
- Unterausdrücke
()
- Quantoren
* + ? {m,n}
- Verkettung
(ohne Operator) - Anker
^ $
- Alternation
|
Mit Unterausdrücken (runden Klammern) kann man die Präzedenz anpassen.
Aufgaben
- Schreibe eine RE die immer matcht.
- Schreibe eine RE die niemals matcht.
- Schreibe eine RE die leere Zeilen matcht.
- Filtere alle leere Zeilen weg.
- Wie kann der Circumflex den Zeilenanfang matchen, wenn er nicht als erstes Zeichen in der RE steht?
Zusammenfassung
Zum Schluss möchte ich nochmal Henry Spencers RE-Beschreibung aus der Manpage regex(7) hervorholen. Anmerkungen blau in eckingen Klammern.
Regular expressions ("RE"s), as defined in POSIX.2, come in two forms: modern REs (roughly those of egrep; POSIX.2 calls these "extended" REs) and [...BREs...].
POSIX.2 leaves some aspects of RE syntax and semantics open; "(!)" marks decisions on these aspects that may not be fully portable to other POSIX.2 implementations.
A (modern) RE is one(!) or more nonempty(!) branches, separated by '|'. It matches anything that matches one of the branches. [= Alternation]
A branch is one(!) or more pieces, concatenated. It matches a match for the first, followed by a match for the second, etc. [= Verkettung]
A piece is an atom possibly followed by a single(!) '*', '+', '?', or bound. An atom followed by '*' matches a sequence of 0 or more matches of the atom. An atom fol‐ lowed by '+' matches a sequence of 1 or more matches of the atom. An atom followed by '?' matches a sequence of 0 or 1 matches of the atom. [= Quantoren]
A bound is '{' followed by an unsigned decimal integer, possibly followed by ',' possibly followed by another unsigned decimal integer, always followed by '}'. The integers must lie between 0 and RE_DUP_MAX (255(!)) inclusive, and if there are two of them, the first may not exceed the second. An atom followed by a bound con‐ taining one integer i and no comma matches a sequence of exactly i matches of the atom. An atom followed by a bound containing one integer i and a comma matches a sequence of i or more matches of the atom. An atom fol‐ lowed by a bound containing two integers i and j matches a sequence of i through j (inclusive) matches of the atom. [= Quantoren]
An atom is a regular expression enclosed in "()" (match‐ ing a match for the regular expression), an empty set of "()" (matching the null string)(!), [= Unterausdruck] a bracket expression (see below), [= Zeichenklasse] '.' (matching any single character), [= Punkt] '^' (matching the null string at the beginning of a line), '$' (matching the null string at the end of a line), [= Anker] a '\' followed by one of the characters "^.[$()|*+?{\" (matching that character taken as an ordinary character), a '\' followed by any other character(!) (matching that character taken as an ordinary character, as if the '\' had not been present(!)), [= escapetes Metazeichen] or a single character with no other significance (matching that character). [= literales Zeichen] A '{' fol‐ lowed by a character other than a digit is an ordinary character, not the beginning of a bound(!). It is ille‐ gal to end an RE with '\'.
A bracket expression is a list of characters enclosed in "[]". It normally matches any single character from the list (but see below). If the list begins with '^', it matches any single character (but see below) not from the rest of the list. If two characters in the list are sep‐ arated by '-', this is shorthand for the full range of characters between those two (inclusive) in the collating sequence, for example, "[0-9]" in ASCII matches any deci‐ mal digit. It is illegal(!) for two ranges to share an endpoint, for example, "a-c-e". Ranges are very collat‐ ing-sequence-dependent, and portable programs should avoid relying on them. [= Zeichenklasse]
To include a literal ']' in the list, make it the first character (following a possible '^'). To include a lit‐ eral '-', make it the first or last character, or the second endpoint of a range. To use a literal '-' as the first endpoint of a range, enclose it in "[." and ".]" to make it a collating element (see below). With the exception of these and some combinations using '[' (see next paragraphs), all other special characters, including '\', lose their special significance within a bracket expression. [= Zeichenklasse]
Within a bracket expression, a collating element (a char‐ acter, a multicharacter sequence that collates as if it were a single character, or a collating-sequence name for either) enclosed in "[." and ".]" stands for the sequence of characters of that collating element. The sequence is a single element of the bracket expression's list. A bracket expression containing a multicharacter collating element can thus match more than one character, for exam‐ ple, if the collating sequence includes a "ch" collating element, then the RE "[[.ch.]]*c" matches the first five characters of "chchcc". [= Collating Symbol (weggelassen)]
Within a bracket expression, a collating element enclosed in "[=" and "=]" is an equivalence class, standing for the sequences of characters of all collating elements equivalent to that one, including itself. (If there are no other equivalent collating elements, the treatment is as if the enclosing delimiters were "[." and ".]".) For example, if o and ^ are the members of an equivalence class, then "[[=o=]]", "[[=^=]]", and "[o^]" are all syn‐ onymous. An equivalence class may not(!) be an endpoint of a range. [= Equivalence Class Expression (weggelassen)]
Within a bracket expression, the name of a character class enclosed in "[:" and ":]" stands for the list of all characters belonging to that class. Standard charac‐ ter class names are:
alnum digit punct alpha graph space blank lower upper cntrl print xdigit
These stand for the character classes defined in wctype(3). A locale may provide others. A character class may not be used as an endpoint of a range. [= Zeichenklassenausdrücke]
In the event that an RE could match more than one sub‐ string of a given string, the RE matches the one starting earliest in the string. If the RE could match more than one substring starting at that point, it matches the longest. Subexpressions also match the longest possible substrings, subject to the constraint that the whole match be as long as possible, with subexpressions start‐ ing earlier in the RE taking priority over ones starting later. Note that higher-level subexpressions thus take priority over their lower-level component subexpressions. [= Gierigkeit; Der längste der frühesten Treffer]
Match lengths are measured in characters, not collating elements. A null string is considered longer than no match at all. For example, "bb*" matches the three mid‐ dle characters of "abbbc", "(wee|week)(knights|nights)" matches all ten characters of "weeknights", when "(.*).*" is matched against "abc" the parenthesized subexpression matches all three characters, and when "(a*)*" is matched against "bc" both the whole RE and the parenthesized sub‐ expression match the null string. [= Gierigkeit; Der längste der frühesten Treffer]
Möglicherweise weitere erwartete Features (wie beispielsweise die Wortgrenzenanker ‚\< \>‘) sind nicht Teil von EREs (was schon daran ersichtlich ist, dass bei EREs alle Metazeichen unescapet sind ;-) ). Dass sie dennoch bei egrep funktionieren, liegt daran, dass egrep nicht strikt POSIX entspricht, sondern verschiedene Erweiterungen und Abweichungen hat. Auf solche und weitere Features werden wir in späteren Kapiteln dieses Kurses noch eingehen. Das Kapitel EREs wird hiermit erstmal geschlossen.