Perl 6

Die Zukunft des Programmierens

Regexes

Regexes sind Muster, die Text beschreiben. Wenn man sie auf einen String "anwendet", überprüft man, ob der String diesem Muster folgt.

if "Mein Name ist Hase" ~~ m/Hase/ {
    say "In dem Text kam 'Hase' vor";
}

In diesem Beispiel wird ein Text darauf überprüft, ob der String "Hase" darin vorkommt. Dabei ist "Hase" die regex, und wird mit m/.../ begrenzt.

Die Regex wird mit dem Smart Match Operator ~~ auf den String angewandt, oder gematched wie man sagt.

Aufbau von Regexes

Im einfachsten Fall bestehen Regexes einfach auch Text, ein Buchstabe nach dem anderen.

Dabei sollte man beachten, dass Leerzeichen keinerlei Bedeutung haben, d.h. m/Hase/ und m/Ha se/ sind genau die gleiche Regex.

Zusätlich gibt es Zeichen, die eine Sonderfunktion haben, zum Beispiel der Punkt . steht für ein beliebiges Zeichen:

my $str = "Hosenträger";
if $str ~~ m/H.se/ {
    # passt auch.
}
if "Haase" ~~ m/H.se/ {
    # passt nicht, der Punkt steht für genau ein Zeichen
}

Es gibt auch Zeichenkombinationen, die etwas besonderes bedeuten, z.B. \d steht für eine Ziffer (Englisch digit = Ziffer).

my $zeit = "6H30";
if $zeit ~~ m/\d H \d\d/ {
    say "gültige Weckzeit";
} else {
    say "Damit kann ich nichts anfangen";
}

Die folgende Tabelle fasst die wichtigsten dieser Zeichenkombinationen zusammen. Dabei gilt, dass jeweils genau ein Zeichen gematched wird.

Die Negation ist die Verneinung, d.h. wenn \d auf eine Ziffer passt, passt \D auf ein beliebiges Zeichen, das keine Ziffer ist.

Zeichen Bedeutung Beispiel Herkunft Negation
. beliebiges Zeichen / ...
\d Ziffer 7 digit = Ziffer \D
\t Tabulator tab = Tabulator \T
\n Zeilenumbruch newline = neue Zeile \N
\w Buchstabe, Ziffer oder Unterstrich H word = Wort \W
\s (Unicode)-Leerzeichen space = Leerzeichen \S

Alternativen

Alternativen sind oder-Verknüpfungen, das heißt die Regex matcht, wenn eine von mehreren Alternativen passt:

if $str ~~ m/der|die|das/ {
    say "Bestimmten Artikel gefunden";
}

Wenn sich die Alternative nicht über die gesamte Regex erstrecken soll, kann man ihren Gültigkeitsbereich mit eckigen Klammern einschränken:

m/ [ der | die | das ] \s Artikel /
# passt auf "der Artikel", "die Artikel" oder "das Artikel"

Nur zur Erinnerung: Leerzeichen in Regexes werden ignoriert, wenn man Leerzeichen matchen will, muss man das zum Beispiel mit \s angeben.

Quantoren, oder: "Wie oft darf's denn sein?"

Mit sogenannten Quantoren (Englisch quantifier) gibt man an, wie häufig ein bestimmtes Zeichen oder Teilmuster vorkommen muss.

Dabei steht ein Fragezeichen für "0 mal oder einmal":

m/abc?d/        # passt auf abcd oder abd
m/a[bc]?d/      # passt auf abcd oder ad

Ein + steht für "Ein mal oder beliebig oft":

m/abc+d/        # passt auf abcd, abccd, abcccd, ...
m/a[bc]+d/      # passt auf abcd, abcbcd, abcbcbcd, ...

if $str ~~ m/\d+/ {
    say "in $str kommt eine Zahl vor";
}

Ein * passt auf keine, eine oder beliebig viele Wiederholungen:

m/abc*d/        # passt auf abd, abcd, abccd, abcccd, ...
m/a[bc]*d/      # passt auf ab, abcd, abcbcd, abcbcbcd, ...

Wenn man z.B. eine Wiederholung genau vier mal haben will, kann man das mit der **4-Notation angeben:

m/a b**4 c/     # passt auf abbbbc
m/a b**2..4 c/  # passt auf abbc oder abbbc oder abbbbc
m/a b**2..* c/  # passt auf abbc, abbbc, abbbbc, abbbbbc, ...

Regexes sinnvoll einsetzen

Bisher haben Sie gesehen, wie man mit Regexes überprüft, ob ein Muster in einem String vorkommt oder nicht.

Natürlich kann man auch abfragen, welcher Teil eines Strings auf eine Regex passt:

my $str = "Du schuldest mir 122 Euro";

if $str ~~ m/\d+/ {
    say "$/ Euro, verstanden?";
}

Das Ergebnis des letzten Matches wird in der speziellen Variable $/ gespeichert, und im String-Kontext wird daraus automatisch der Teil des Strings, auf den die Regex gepasst hat.

Man kann das Ergebnis auch explizit einer Variable zuweisen, und auf weitere Eigenschaften zugreifen:

my $match = "foobar" ~~ m/o+/;
say "Anfangsposition des Matches: ",    $match.from;
say "Endposition des Matches: ",        $match.to;

Mit den Funktionen comb und split kann man Strings in Arrays zerlegen.

comb liefert dabei alle Treffer einer Regex zurück:

my $text = "20 mal 2 ist 40";

my @zahlen = $text.comb(m/\d+/);
# Ergebnis: @zahlen = 20, 2, 40;

(Der Name kommt von to comb, was auch "etwas durchkämmen" im Sinne von "systematisch Durchsuchen" heißt.)

split trennt einen String mithilfe einer Regex, wobei der String überall dort getrennt wird, wo die Regex passt. Der passende Teil wird nicht zurückgeliefert:

my $text = "20 mal 2 ist 40";

my @worte = $text.split(m/\s+/);
# Ergebnis: @worte = "20", "mal", "2", "ist", "40";

Wenn man in einer Regex einen Schrägstrich / benutzt, muss man ihn mit einem Rückstrich escapen: \/. Eine andere Möglichkeit ist, ein anderes Zeichen als Begrenzung der Regex zu verwenden:

my $cmd = "/usr/bin/perl -w";
if $cmd ~~ m{^(/.*/)perl} {
    say "Perl liegt im Verzeichnis $/[0]";
}

Man kann fast beliebige Sonderzeichen zur Begrenzung verwenden, wenn man Klammern verwendet wird die Regex mit der entsprechenden schliessenden Klammer beendet.

Gierige und bescheidene Quantoren

Angenommen, man lässt die Regex m/a.*b/ auf den String afffbffb los. Die meisten Anfänger erwarten, dass die Regex auf "afffb" passt, aber wenn man sich $/ ausgeben lässt, erlebt man eine Überrasschung: Der ganze String passt auf die Regex.

Der Grund dafür ist, dass die Quantoren +, *, ? und **{...} gierig, englisch greedy sind, d.h. sie passen immer auf einen möglichst langen String.

Wenn der Programmteil von Perl, der die Regexes verarbeitet, auf eine Konstruktion wie m/a.*c/ trifft, sucht er nach dem ersten "a", geht dann ans Ende des Strings, und sucht dann rückwärts nach dem ersten "c", wenn er das gefunden hat, ist er fertig.

Um das zu verhindern, und dafür zu sorgen, dass ein möglichst kurzer String gefunden wird, kann man hinter jeden Quantoren ein Fragezeichen ? stellen:

$m = "affbeeb" ~~ m/a .* b/;
say $m;             # Ausgabe: affbeeb
$m = "affbeeb" ~~ m/a .*? b/;
say $m;             # Ausgabe: affb

Genauso kann man aus + mit der Schreibweise +? "einen oder mehr, aber möglichst wenig davon" machen, aus **{2...6} mit **{2..6}? "zwei bis sechs Wiederholungen, aber möglichst wenig davon" usw.

Man nennt diese Quantoren dann non-greedy, also nicht gierig.

Anker

Die bisherigen Regexes haben immer nur überprüft, ob ein bestimmtes Must in einem String vorkommt, aber nicht, ob der gesamte String zu dem Muster passt.

Das geht, indem man die Regex verankert. Dazu gibt es die Sonderzeichen ^ und $, die auf Anfang bzw. Ende des Strings passen. Wenn man also überprüfen will, ob ein String mit einer Ziffer anfängt, geht das m/^\d/, und wenn man überprüfen will, ob ein String eine positive, ganze Zahl ist, geht das mit m/^\d+$.

Dabei sollte man beachten, dass ^ und $ keine Zeichen "aufbrauchen", d.h. ^a$ passt genau auf den String "a".

Wenn man sich nicht für Anfang und Ende eines Strings interessiert, sondern für Zeilenanfang und -Ende, so kann man ^^ und $$ als Anker verwenden.

my $s = "He shouted\nRUN\nAnd I ran";

if $s ~~ m/ ^^ \w+ \n $$ / {
    # passt auf eine Zeile, die aus nur einem Wort
    # und einem newline besteht, matched also
    say $/;
    # Ausgabe: RUN
}

if $s ~~ m/ ^ \w+ $ / {
    # passt nicht, da der String nicht nur
    # aus einem Wort besteht.
}

Dabei gelten Anfang und Ende eines Strings auch immer als logischer Zeilenanfang bzw. -Ende.

Captures

Mit den bisher vorgestellten Elementen von Regexes ist es nicht möglich, nach einem Muster wie Ein Wort, gefolgt von einem Leerzeichen, gefolgt von dem selben Wort zu suchen.

Das ändert sich mit sogenannten captures (auf Deutsch auch manchmal fangende Klammern genannt), die mit runden Klammern eingerichtet werden:

m/ (\w+) \s $0 /

Dabei wird der Match des in den runden Klammern stehenden Teils der Regex in der Variable $0 gespeichert.

Der String "foo foo" würde z.B. zu der Regex passen, nicht aber "foo bar".

Wenn es mehrere Capturing Groups gibt, wird das Ergebnis der ersten in $0 abgespeichert, der zweiten in $1 usw.

Außerdem kann man das Match-Objekt, das beim Anwenden einer Regex entsteht, als Array verwenden, das als Elemente die verschiedenen Capturing Groups hat.

my @a = "der Artikel, das Adverb",
        "das Schiff, das Monster",
        "der Berg, das Schaf";

for @a -> $s {
    if $s ~~ m/(\w+) \s (\w+), \s  $0 \s (\w+)/ {
        say "$/[1] und $/[2] haben den gleichen Artikel: $/[0]";
    }
}
# Ausgabe:
# Schiff und Monster haben den gleichen Artikel: das

Dieses Muster sucht nach einem Wort \w+ und speichert das Ergebnis in der Variablen $0, sucht weiter nach einem Leerzeichen \s, nach noch einem Wort, das in $1 gespeichert wird, einem Komma, einem Leerzeichen, dem zuerst gefunden Wort, und dann noch einem Wort, das in $2 gespeichert wird.

Außerhalb der Regex kann auf $0 mit $/[0] oder mit $0 zugegriffen werden, auf $1 auch mit $/[1] usw.

Ab dem 11. Capture mit Nummer 10 kann nur noch mit der $/[$index]-Notation darauf zugegriffen werden.

Wenn man z.B. eine Datei einliest, kann man mit der folgenden Regex nach Strings suchen, die entweder mit " oder ' begrenzt sind:

m/ ('|") .*? $0 /

Das sucht nach einem einfach oder doppelten Anführungszeichen, dann beliebig viele (aber möglichst wenige) Zeichen, und dann noch einmal dem Anfangszeichen.

Modifier

Mit sogenannten Modifiern kann man das Verhalten von Regexes auf verschiedene Arten verändern.

Groß- und Kleinschreibung ignorieren - :ignorecase

Mit dem :i oder :ignorecase-Modifier wird Groß- und Kleinschreibung ignoriert. Das gilt nicht nur für latinische Zeichen, sondern für beliebige Schriften, die das Konzept von Groß- und Kleinschreibung kennen.

my $str = "Ab";
$str ~~ m:i/ab/;        # matched
regex foo {
    :ignorecase         # kann auch innerhalb einer Regex stehen
    ab                  # passt auf ab, Ab, aB und AB
}

Akzente ignorieren - :basechar

Mit :basechar oder :b werden alle Arten von Akzente von der Regex ignoriert.

m:b/a/ matched also nicht nur "a", sondern auch "ä", "à", "á", "â", "ã", "ä" oder "å".

Alle Treffer finden - :global

Mit dem :g oder :global-Modifier kann man nach allen, nichtüberlappenden Treffern einer Regex suchen:

my $str = "3 = (4 - 2) + 1";
my @zahlen = $str ~~m:g/\d+/;
say @zahlen.join(", ");         # 3, 4, 2, 1

Überlappende Treffer finden - :overlap

Mit dem :overlap oder :ov-Modifier findet man auch überlappende Treffer, aber pro Anfangsposition nur eines:

my @treffer = 'abababa' ~~ m:ov/a.+a/;
# Ergebnis: 'abababa', 'ababa', 'aba'

Wirklich alle Treffer finden - :exhaustive

Mit dem :exhaustive oder :ex-Modifier kann man jedem möglichen Treffer suchen lassen:

$str = "abracadabra";

for $str ~~ m:exhaustive/ a (.*?) a / -> $match {
    say $match[0];  # br brac bracad bracadabr c cad cadabr d dabr br
}

:sigspace

Mit dem :sigspace oder :s-Modifier werden alle Leerzeichen so interpreteirt, als stünde an ihrere Stelle die Regex <.ws>.

Die matcht, wenn sie zwischen zwei Wörtern steht, \s+, und sonst \s*.

(Der Punkt am Anfang bewirkt, dass keine Named Capture erstellt wird.)

m:sigspace/ \d+ [\+ | \*] \d+ /
# matcht: '2+3', '2 + 3', ' 2 + 3 ', ...

Backtracking verhindern - :ratchet

Bisher haben wir so getan, also ob die Regex-Engine auf magische Weise wüßte, ob eine Regex zu einem String passt oder nicht.

Das ist natürlich nicht der Fall, sie muss in dem String suchen.

Dabei gibt es häufig anfangs mehrere Möglichkeiten, wie die Regex-Engine einen String matchen könnte.

Ein Beispiel: Wenn die Regex \w+\d+ in dem String "ab123" das erste Mal auf eine Ziffer trifft, könnte das immer noch zu \w+ gehören, oder aber schon zu \d+. Da die Engine das noch nicht entscheiden kann, ohne den Rest des Strings zu kennen, merkt sie sich, dass es an dieser Stelle noch eine andere Möglichkeit gegeben hätte, und fährt damit fort, \w+ zu matchen.

Bei der nächsten Ziffer, 2 passiert genau das Gleiche, ebenso bei der dritten.

Dann ist die Engine am Ende des Strings angelangt, die Regex ist aber noch nicht zuende. Also geht die Regex Engine zu dem letzten Punkt zurück, an dem es mehrere Möglichkeiten gab, das war beim letzten Zeichen, der 3.

Dort wird versucht, die verbleibende Möglichkeit, \d+, zu matchen. Das ist erfolgreich, also matcht die ganze Regex.

Dieser Vorgang des Speicherns von mehrdeutigen Stellen und wieder darauf zurückzugehen, wenn auf keine andere Art gematcht werden kann, nennt sich Backtracking.

Manchmal möchte man dieses Verhalten jedoch abschalten, meistens um das Matchen einer Regex zu beschleunigen.

Das geht mit dem :ratchet-Modifier.

Die Idee dahinter ist, dass man häufig genauer als die Regex-Engine weiß, auf was eine Regex matchen soll und auf was nicht.

Wenn man z.B. eine Datei der Form

name1 = wert1
name2 = wert2

parsen will, nimmt eine Regex der Form ^^(\w+) \s* \= \s* (\N+)\n$$ - und man weiss genau, dass wenn das = nicht matcht, muss die Engine gar nicht erst versuchen, den vorherigen String zu kürzen (der vorher gematchte String waren null oder mehr Leerzeichen, das kann niemals ein = sein).

Also sagt man der Engine, dass sie es gar nicht erst versuchen soll:

m:ratchet:global/ ^^(\w+) \s* \= \s* (\N+)\n$$/

Regexes können beschreibbar sein

my $str = "12+3";
token op { \+ };
$str ~~ m:rw/(\d+) <op> (\+d)/;
$<op> = '(irgendwas)';    # $str ist jetzt "12(irgendwas)3"

Mit dem :rw-Modifier kann man die Captures einer Regex beschreibbar machen, wie oben demonstriert.

Dabei ändern sich automatisch auch die anderen Captures, insbesondere .from und .to der Captures:

my $str = "ab";
$str ~~ m:rw/(a)(b)/;
say $/[1].from;        # 1
$/[0] = 'aa';
say $/[1].from;        # 2
say ~$/;        # aab

Eigene Zeichenklassen, Grammatiken

Wenn einem die vorgefertigten Zeichenklassen wie \d und \w nicht ausreichen, kann man sich selbst welche erzeugen, allerdings mit anderer Notation:

m/ <[a..yB..J_]> /

Damit werden Zeichen aus dem Bereich "a" bis "y" oder "B" bis "J" oder der Unterstrich "_" gefunden.

Man kann auch Zeichenklassen mit einem Minuszeichen negieren:

m/<-[a..z]>/;        # alle Zeichen ausser Kleinbuchstaben
m/<[a..z]-[ij]>/;    # alle Kleinbuchstaben außer i und j

Man kann Zeichklassen und Regexes auch Namen geben:

token meine_zeichen     { <[a..zA-U]> };
regex identifier        { <meine_zeichen> \w* };

Dabei werden Regexes wie Funktionen definiert, und werden mit dem Schlüsselwort token, rule oder regex eingeleitet.

Innerhalb anderer Regexes kann man dann mit der Notation <name> auf die Regex zugreifen (dabei sind keine Leerzeichen vor oder nach dem Namen erlaubt).

Dabei ist token ein regex mit einem impliziten :ratchet-Modifier (d.h. es findet kein Backtracking innerhalb des Tokens statt), eine rule ist eine regex mit implizitem :ratchet :sigspace.

Named Captures

Einer Regex einen Namen zu geben und in einer andere Regex zu verwenden hat noch einen weiteren Effekt:

token integer { \d+ };
my $str = "zahl: 25";
my $match =  $str ~~ m/ ^zahl: \s <integer> /;
if $match {
    say "Die Zahl war: $match<integer>";
}
# Ausgabe: Die Zahl war 25

Da die subrule, hier integer, einen Namen hat, wird eine sogenannte named capture angelegt. Wenn man das Match-Objekt als Hash verwendet, kann man unter dem Namen der subrule auf den Teil des Strings zugreifen, auf den die subrule gepasst hat.

Der Inhalt einer named capture ist ebenfalls ein Match-Objekt:

token integer { \d+ };
my $str = "zahl: 25";
my $match =  $str ~~ m/ ^zahl: \s <integer> /;
if $match {
    my $z = $match{"zahl"};
    say $z.from, " bis ", $z.to;
    say $z.WHAT;
}
# Ausgabe:
# 7 bis 8
# Match

Wenn eine subrule mehrfach verwendet, wird unter dem Namen ein Array von Matches erstellt:

token integer { \d+ };
token add_op  { \+ | - };

if my $match = "23+42" ~~ m/ <integer> <add_op> <integer>/ {
    say $match<integer>.elems;     # Ausgabe: 2
    say $match<integer>[0];        # Ausgabe: 23
}

Wenn man kein Freund von Arrays ist, kann man auch einen anderen Namen für den Match angeben:

token integer { \d+ };
token add_op  { \+ | - };

if my $match = "23+42" ~~ m/ $<lhs>:=<integer> <add_op> $<rhs>:=<integer>/ {
    say $match<lhs>;        # Ausgabe: 23
    say $match<rhs>;        # Ausgabe: 42
}

Named Captures auslassen

Angenommen, man möchte obige Regex so erweitern, dass auch Leerzeichen zugelassen werden (aber optional sind), könnte man das so machen:

token integer { \d+ };
token add_op  { \+ | - };

if "3 + b" ~~ m/<integer> <ws> <add_op> <ws> <integer>/ {
    ...
}

<ws> ist eine "builtin" regex, und steht für ein (kontextsensitiv) optionales Leerzeichen (oder mehrere davon).

Dabei wird im Match-Objekt eine Liste von Leerzeichen angelegt - wenn man aber nur an dem verarbeiteten mathematischen Ausdruck interessiert ist, interessieren die Leerzeichen aber nicht. Wenn man sie nicht im Match-Objekt haben möchte, kann man vor den Namen ein Fragezeichen stellen:

token integer { \d+ };
token add_op  { \+ | - };

if "3 + b" ~~ m/<integer> <.ws> <add_op> <.ws> <integer>/ {
    # $/ hat hier keinen <ws>-Eintrag
}

Grammatiken

Durch das verschachteln von regexes und tokens kann man ganze Grammatiken aufstellen und wie eine regex benutzen. In diesem Fall spricht man von rules, also Regeln:

token integer       { \d+ };
token add_op        { \+ | - };
token mul_op        { \* | \/ };
rule factor         { <integer> [ <mul_op> <integer> ]* }
rule expression     { <factor>  [ <add_op> <factor>  ]* }

Damit kann man Mathematische Ausdrücke wie 2 + 3*5 - 4/23 erkennen (und entsprechend verarbeiten), während z.B. 2 3 (also zwei Zahlen, die nicht von einem Operator getrennt werden) korrekterweise nicht erkannt werden.

Weitere Elemente von Regexes

Konjunktionen

Regexes unter Perl 6 können Konjunktionen enthalten, d.h. zwei (oder mehr) Regexes, die gemeinsam mit dem selben Start- und Endpunkt matchen müssen:

if "1 + 3 * 4" ~~ m/^[<expression> & ^<-[4]>+4 ]$/ {
    ...
}

Dieses Beispiel passt auf mathematische Ausdrücke, die mit 1 anfangen, auf 4 enden und dazwischen keine weitere 4 haben.

Code in Regexes ausführen

Mit der Konstruktion <{...}> kann man Perl-code ausführen, dessen Rückgabewert als Regex ausgewertet wird.

Zum Beispiel kann man damit eine ganze Zahl, gefolgt von ihrer Wurzel, abgerundet auf die nächste ganze Zahl, gematched werden:

m/ (\d+) <ws> <{ return $0.sqrt.int; }> /

Lookahead- und Lookbehind-Assertions

Mit <before ...> und <after ...> kann man vor und hinter eine Regex schauen, der Rest in den spitzen Klammern wird als Regex interpretiert.

m/ <before foo> \d+ <after bar> /
# ist fast das gleiche wie
m /foo \d+ bar/

Der Unterschied ist, dass bei der zweiten Regex foo und bar zum Match gehören, bei der ersten nicht.

Man kann alle Konstruktionen in spitzen Klammern auch mit einem Ausrufezeichen negieren:

m/ \d+ <!after <sp> Euro>/
# passt auf "3 Dollar", aber nicht auf "3 Euro"

Mit Rules parsen

Mit Rules kann man sehr leicht Parser schreiben, also Programme, die einen Text in eine interne Struktur verarbeiten.

Als Beispiel sollen einfache Konfigurationsdateien gelesen werden:

# Das ist ein Kommentar

# Strings werden mit doppelten Anführungszeichen
ip = "127.0.0.1";

timeout = 120; # Zahlen können einfach so darstehen

# Variablen können ohne Anführungszeichen zugewiesen werden:
other_timeout = timeout;

Das ist zwar noch leicht ohne Rules zu parsen, aber des pädagogischen Wertes wegen soll es mit Rules geparsed werden.

Ein erster naiver, aber gar nicht so schlechter Ansatz sieht so aus:

grammar config_file {
    rule config        { <config_line>* };
    # Kommentare müssen nicht gespeichert werden:
    rule confg_line    { <?comment> | <assignment> };
    token comment      { '#' \N* \n };
    rule assigment     { <ident> '=' <expression> ';' };
    rule expression    { <number> | <quoted_string> | <ident> };
    rule quoted_string { \" <-["]>* \" };
}

Das parsed eine Konfigurationsdatei, hat aber einen großen Nachteil: Wenn die Datei einen Syntaxfehler hat, dann matched die Regex nicht - aber es gibt keine Fehlermeldung, was schief gelaufen ist.

Alternative schafft z.B. folgende Konstruktion, hier nur an zwei Beispielen gezeigt:

    rule config_line {
            | <?comment>
            | <assignment>
            | <fail: "Expected comment or assignment">
    };
    rule assigment     {
        <ident>
        [ '=' |  <fail: "Expected '='"> ]
        <expression>
        [ ';' | <fail: "Expected ';'"> ]
    };