Regular Expressions: Alternative Syntax und Builder

Regular Expressions - oder zu Deutsch Reguläre Ausdrücke - sind gerade in der Softwareentwicklung ein mächtiges Hilfsmittel, wenn es um Filtern, Validieren, Suchen und Ersetzen geht. Leider sind sie eines ganz bestimmt nicht: Einfach zu lesen und zu schreiben. Eine simple Regular Expression zum Validieren einer E-Mail-Adresse könnte zum Beispiel so aussehen:

/^([A-Z0-9._%+-])+@[A-Z0-9.-]+\.[A-Z]{2,}$/i

Wer noch nie versucht hat, Regular Expressions zu verstehen, oder diese schlichtweg nicht kennt, schaltet nach einer solchen Zeile definitiv auf Durchzug. Sie ist kryptisch, unverständlich, hässlich und durcheinander. Nur, wer sich gut mit der Materie auskennt und genau weiß, wofür die einzelnen Zeichen stehen, kann - sofern er sich konzentriert in die Sache hineindenkt - verstehen, worum es geht. Aber warum so kompliziert? Es gibt in der heutigen Zeit viele einfachere Wege, einen regulären Ausdruck zu definieren. Letztendlich lässt sich ein solches Statement ja doch recht einfach in Worte fassen:

Beginne mit einem oder mehreren Zeichen zwischen A-Z, 0-9 oder ., _, %, + oder -. Dann erwarte ein @. Dann...

Und so weiter. Wer einen Schritt weiter denkt, kennt schon jetzt eine Sprache, die so aufgebaut ist: SQL. Die Sprache zum Kommunizieren mit den relationalen Datenbanken dieser Welt ist logisch und verständlich. Warum also kein SQL für Regular Expressions? Warum kein S"R"L?

SRL - Simple Regex Language

Das habe ich mich auch gefragt. Und es einfach gemacht. Darf ich vorstellen: SRL - Simple* Regex Language! Ein Beispiel gefällig? Ihr erinnert euch an das Beispiel mit der E-Mail-Validierung? In PHP würde das Ganze aktuell so aussehen:

$regex ='/^([A-Z0-9._%+-])+@[A-Z0-9.-]+\.[A-Z]{2,}$/i';
if (preg_match($regex, '[email protected]') === 1) { }

Hiermit lässt sich prüfen, ob die gegebene E-Mail-Adresse gültig ist. Wesentlich schöner geht es aber so:

$regex = new SRL('BEGIN WITH EITHER OF (NUMBER, LETTER, ONE OF "._%+-") ONCE OR MORE,
LITERALLY "@",
EITHER OF (NUMBER, LETTER, ONE OF ".-") ONCE OR MORE,
LITERALLY ".",
LETTER AT LEAST 2,
MUST END, CASE INSENSITIVE');
if ($regex->isMatching('[email protected]')) { }

Ohne Zweifel ist das SRL-Beispiel wesentlich länger, aber sind wir mal ehrlich... wer keine Ahnung von Regular Expressions hat, kann sich hier denken, was passiert. Besonders, wenn die Person es nicht geschrieben hat und es einen Bug geben sollte, kann dieser einfach behoben werden, da klar ist, worum es geht.

SRL Query Builder - LINQ für Regular Expressions

Doch das ist noch lange nicht alles. Wer Query Builder für MySQL und Co. kennt, wie zum Beispiel LINQ oder Laravels Query Builder, dürfte den SRL Query Builder gut verstehen. Dasselbe Beispiel wie oben, lässt sich hiermit wie folgt in PHP umsetzen:

$query = SRL::startsWith()
    ->eitherOf(function (Builder $query) {
        $query->number()
            ->letter()
            ->oneOf('._%+-');
    })->onceOrMore()
    ->literally('@')
    ->eitherOf(function (Builder $query) {
        $query->number()
            ->letter()
            ->oneOf('.-');
    })->onceOrMore()
    ->literally('.')
    ->letter()->atLeast(2)
    ->mustEnd()->caseInsensitive();

Wie ihr seht, ist die Syntax der SRL Syntax sehr ähnlich. Nur lässt sich damit relativ einfach eine dynamische Regular Expression aufbauen, wenn die Bedingungen erst zur Laufzeit bekannt werden. Am Ende purzelt bei beiden Versionen (SRL und Query Builder) ein Objekt heraus, welches viele tolle Features bietet, wie zum Beispiel das schon gezeigte "isMatching".

Capture Groups und Lookarounds

Wer sich mit Regular Expressions auskennt, der weiß, dass noch ein paar wichtige Features in SRL bisher nicht erwähnt wurden. Darunter Capture Groups, welche es ermöglichen, Resultate zu filtern, und Lookarounds, welche Bedingungen vor oder nach dem Filter erfüllen müssen. Doch selbstverständlich ist auch dies mit ein paar einfachen Worten zu definieren:

$regex = new SRL('CAPTURE (LITERALLY "foo", OPTIONAL "bi") AS "sample" IF FOLLOWED BY (LITERALLY "bar")');

Das CAPTURE-Keyword definiert eine Capture Group unter dem Namen "sample", welche auf die Sub-Expression in den Klammern matcht. Dies passiert jedoch nur, wenn auf diese Gruppe direkt die kommende Sub-Expression, nämlich "bar" folgt. Die folgenden Beispiele würden daraus also resultieren:

$regex->isMatching('foobar'); // true, da auf "foo" direkt "bar" folgt.
$regex->isMatching('foobiboo'); // false, da auf "foobi" kein "bar" folgt.

Da "foobibar" ebenfalls matchen würde, können wir uns mit der "getMatches"-Methode nun ein Array aller Matches ausgeben lassen. Unsere Capture-Group hat den Namen "sample" bekommen, weswegen wir unseren ersten Match nun über diesen Namen abrufen können:

$regex->getMatches('foobibar')[0]->get('sample'); // "foobi"

Auch Dinge wie Replace lassen sich ganz einfach abbilden:

$regex->replace('wunder', 'foobar'); // "wunderbar"

Neben Replace gibt es dann auch noch "filter" und "split", welche ähnlich funktionieren. Und wem das noch nicht genug ist, der darf sich gerne auch die im Hintergrund erstellte Regular Expression ausgeben lassen und damit alles anstellen, was mit normalen Regular Expressions auch möglich ist.

Viel Spaß

Wer das Ganze einmal selbst ausprobieren möchte, darf gerne das SRL GitHub-Projekt klonen, forken, kopieren, löffeln, runterladen, anpassen, mergen oder was euch sonst noch so alles einfällt. Natürlich sind auch Implementierungen in anderen Sprachen oder Contributions und Bugfixes sehr gerne gesehen.   * Jaja, das S in SQL steht für Structured, aber um ehrlich zu sein finde ich trifft es Simple im Falle der Regular Expressions einfach besser.