Reguläre Ausdrücke - praktischer Kurs

Aus DebianforumWiki
Zur Navigation springen Zur Suche springen

Diskussion 183941


Baustelle.png Baustelle: Dieser Artikel ist eine Baustelle. Das heißt, jemand hat sich dieses Artikels angenommen und überarbeitet ihn gerade.


Übersicht: https://wiki.debianforum.de/Regul%C3%A4re_Ausdr%C3%BCcke

Teil 01

Motivation

Diskussion 183942

Diskussion 183974

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

  1. Welche Suchprobleme hattest du schon, die du entweder nicht lösen konntest oder für die du viel Handarbeit investieren musstest?
  2. 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?
  3. An was für Anwendungsfälle denkst du wenn du „Reguläre Ausdrücke“ hörst?

Teil 02

Metazeichen & Escaping

Diskussion 183974

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:

  1. Wir wählen ein Escapezeichen. (Man nimmt möglichst ein Zeichen, das selten im Text vorkommt, aber prinzipiell ist jedes Zeichen möglich.)
  2. 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.
  3. 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

  1. Das Escapezeichen ist der Unterstrich (_). Literale Zeichen sind die Standardzeichen. Schreibe den Ausdruck für das literale Wort: „Haus“.
  2. Das Escapezeichen ist der Unterstrich (_). Metazeichen sind die Standardzeichen. Schreibe den Ausdruck für das literale Wort: „Haus“.
  3. Das Escapezeichen ist das grosse X. Metazeichen sind die Standardzeichen. Schreibe den Ausdruck für das literale Wort: „Haus“.
  4. Das Escapezeichen ist das kleine a. Literale Zeichen sind die Standardzeichen. Schreibe den Ausdruck für das literale Wort: „Haus“.
  5. 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 ‚@‘.
  6. Schreibe den gleichen Text aus (5) aber mit Metazeichen als Standardzeichen.
  7. 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.
  8. Das Escapezeichen ist das Komma (,). Literale Zeichen sind die Standardzeichen. Beschreibe den Ausdruck (vgl. (5)): „A,BCD,E,FG,,H“
  9. Das Escapezeichen ist das Komma (,). Metazeichen sind die Standardzeichen. Beschreibe den Ausdruck: „hello, world“
  10. Das Escapezeichen ist das kleine a. Literale Zeichen sind die Standardzeichen. Beschreibe den Ausdruck: „Haus“
  11. Das Escapezeichen ist das kleine a. Literale Zeichen sind die Standardzeichen. Beschreibe den Ausdruck: „Haaus“
  12. Das Escapezeichen ist das kleine a. Metazeichen sind die Standardzeichen. Beschreibe den Ausdruck: „Haaus“
  13. 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).
  14. 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

Diskussion 184043

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

  1. Schreibe einen egrep-Ausdruck der die Zeichenkette „Reiter“ matcht. Teste ihn entweder so:
    user@debian:~$ echo "Reiter"  |  egrep 'DEIN_AUSDRUCK'
    
    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.
  2. Schreibe einen egrep-Ausdruck der die Zeichenkette „Viel Steine“ matcht.
  3. Schreibe einen egrep-Ausdruck der die Zeichenkette „heil'gen“ matcht.
  4. Schreibe einen egrep-Ausdruck der die Zeichenkette „Er sprach: "Sagt an,“ matcht.
  5. Schreibe einen egrep-Ausdruck der die Zeichenkette „eben.“ matcht. Testet mit ‚-o‘ und ohne.
  6. Schreibe einen egrep-Ausdruck der die Zeichenkette „?“ matcht.
  7. 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.)
  8. 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 Icon pastebin.gif 41651

Teil 04

Diskussion 184095

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:


man 7 regex
       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:


man 7 regex
       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

  1. 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.
  2. Finde ein inhaltlich sinnvolles Beispiel für eine zwei- oder dreifach verschachtelte Alternation. ;-)
  3. Schreibe einen egrep-Ausdruck (nur mit Alternation und Unterausdrücken), um die Schreibweisen Maier, Meier, Mayer, Meier zu matchen.
  4. Ergänze (3) um die Schreibeweise Myer.
  5. Finde alternative Ausdrücke für (3) und (4) (ohne andere RE-Operatoren zu verwenden, sondern nur indem du anders gruppierst).
  6. Versuche diese Aufgaben auch mit fgrep umzusetzen. Was sind deine Erkenntnisse?
  7. Braucht man runde Klammern um den gesamten Ausdruck wenn man eine Alternation (auf oberster Ebene) verwendet?

Teil 05

Diskussion 184152

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:

  1. 0-9 für die Ziffern
  2. A-Z für die Grossbuchstaben
  3. 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.

  1. RE für eine Hexadezimalziffer.
  2. RE für eine dreistellige Ganzzahl
  3. RE für eine dreistellige Ganzzahl mit (Pflicht-)Vorzeichen.
  4. RE für einen Euro-und-Cent-Betrag kleiner 10,-.
  5. RE für die 31 Tage des Januars (zweistelliges Format mit führender Null). Zwei Varianten.
  6. RE für die 28 Tage des Februars (zweistelliges Format mit führender Null). Zwei Varianten.
  7. RE für ein beliebiges Zeichen, das kein Buchstabe ist.
  8. RE für eine beliebige Ziffer. Drei verschiedene Varianten.
  9. RE für ein Zeichen, das kein Circumflex (^) ist.
  10. RE für eine Zeichenklasse, die nur einen Circumflex matcht.
  11. RE für ein Minus oder eine eckige Klammer.
  12. RE die alles ausser einer schliessenden eckigen Klammer oder einem Circumflex matcht.
  13. Schreibe zwei verschiedene REs, die beide einen Backslash matchen.
  14. Was matcht die Zeichenklasse ‚[A-z]‘ ausser Buchstaben sonst noch? (Zeichensatz: US-ASCII)
  15. RE, die einen Tab matcht. Versuche mehrere Varianten zu finden.
  16. Schreibe einen egrep-Ausdruck (nun auch mit Zeichenklassen), um die Schreibweisen Maier, Meier, Mayer, Meier und Myer zu matchen.
  17. 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)