std.regex — Reguläre Ausdrücke
Reguläre Ausdrücke mit einer rekursiven Backtracking-Engine: Zeichenklassen, Quantoren, Anker, Alternation und verschachtelte Gruppen. Sucht in einem pchar, liefert Trefferpositionen und kann Treffer in einen Zielpuffer ersetzen.
Einsatzbereiche: Eingabeprüfung, Log-Auswertung, Textextraktion, Protokollanalyse.
Autor: Andreas Röne
Copyright: 2024–2025 Andreas Röne
Quelle: std/regex.lyx
<WRAP info> Seit lyxc 1.0.20F arbeitet die Unit vollständig (#1466, #1467, #1468, #1378). Die drei früher hier verzeichneten Mängel sind behoben — nachgemessen:
RegexReplaceAlloc("[0-9]", "a1b2c3", "#") → a#b#c#
RegexReplace(...) → 3 (zaehlt die Treffer)
RegexMatch("^a.c$", "abc") → 1
RegexMatchEx("^ABC$", "abc", 1) → 1 (ignore-case wirkt)
RegexMatchEx("^ABC$", "abc", 0) → 0
Zu beachten bleibt die Arbeitsteilung der Namen: RegexReplaceAlloc liefert die ersetzte Zeichenkette, RegexReplace die Anzahl der Ersetzungen. Wer den Text braucht, nimmt die Alloc-Fassung oder RegexReplaceInto mit eigenem Zielpuffer.
</WRAP>
Import
import std.regex;
Unterstützte Syntax
Alle Angaben mit lyxc 1.0.21A nachgeprüft.
| Konstrukt | Bedeutung | Beispiel |
|---|---|---|
. | ein beliebiges Zeichen | a.c passt auf abc |
* | null- oder mehrmals | ab*c passt auf ac und abbbc |
+ | ein- oder mehrmals | ab+c passt nicht auf ac |
? | null- oder einmal | ab?c passt auf ac |
{n} | genau n-mal | a{2} passt nicht auf a |
{n,} | mindestens n-mal | a{3,} braucht drei a |
{n,m} | n- bis m-mal | [a-z]{2,3} |
[…] | Zeichenklasse, Bereiche mit - | [a-z0-9] |
[^…] | negierte Klasse | [^0-9] |
^ | Textanfang | ^GET |
$ | Textende | \.txt$ |
| | Alternation | foo|bar |
(…) | Gruppe, auch verschachtelt und mit Quantor | (ab)+c, (a(b|c))d |
\d \w \s | Ziffer, Wortzeichen, Leerraum | \d{4}-\d{2}-\d{2} |
\. \$ … | Sonderzeichen maskieren | a\.c passt nur auf a.c |
Ein leeres Muster passt an Position 0. Ein Muster mit doppeltem Quantor (a) findet nichts, statt einen Fehler zu melden.
Nicht unterstützt: Rückwärtsreferenzen (\1), Lookahead/Lookbehind, nicht-gierige Quantoren (*?), benannte Gruppen, Unicode-Klassen. Zeichenklassen arbeiten byteweise — bei UTF-8 passt . auf ein Byte, nicht auf ein Zeichen.
—-
===== Funktionen =====
==== Verwendbare API ====
^ Signatur ^ Beschreibung ^
| RegexSearch(pattern, text: pchar): int64 | Position des ersten Treffers, oder -1 |
| RegexMatch(pattern, text: pchar): bool | Ob es irgendwo einen Treffer gibt — kein Vollstring-Vergleich |
| RegexSearchSpan(pattern, text: pchar, startOut, endOut: int64): bool | Schreibt Start und Ende des Treffers per poke64 in zwei Puffer |
| RegexFindAll(pattern, text: pchar, out: int64, maxMatches: int64): int64 | Alle nicht überlappenden Treffer; out nimmt je 8 Byte einen pchar-Zeiger auf |
| RegexReplaceInto(dest, pattern, text, replacement: pchar): int64 | Schreibt den ersetzten Text nach dest, liefert die Zahl der Ersetzungen |
Match, Search und Replace sind Kurznamen derselben Funktionen; _RegexMatch, _RegexSearch und _RegexReplace die internen Fassungen dahinter.
RegexMatch ist kein Test auf vollständige Übereinstimmung: RegexMatch(„abc“, „xxabcxx“) liefert true. Wer den ganzen Text prüfen will, verankert das Muster selbst mit ^…$.
==== Hilfsfunktionen für Zeichenklassen ====
^ Signatur ^ Beschreibung ^
| isAlpha(c: int64): int64 | Buchstabe |
| isDigit(c: int64): int64 | Ziffer |
| isAlnum(c: int64): int64 | Buchstabe oder Ziffer |
| isWhitespace(c: int64): int64 | Leerraum |
| isWordChar(c: int64): int64 | Wortzeichen (Buchstabe, Ziffer, Unterstrich) |
Alle liefern 1 oder 0 als int64, nicht bool.
==== Interna ====
atomLen, atomMatches, matchHere und regexSearchFrom sind der Kern der Engine. Sie sind öffentlich, gehören aber nicht zur beabsichtigten Schnittstelle — ihre Signaturen können sich ändern.
==== Zustand der früher defekten Funktionen ====
^ Signatur ^ Zustand (1.1.2E nachgemessen) ^
| RegexReplace/Replace/RegexReplaceEx | ersetzen; RegexReplace liefert die Anzahl, RegexReplaceAlloc/Into den Text (#1466) |
| RegexMatchEx/RegexSearchEx/ReplaceEx | flags wirken: IGNORE_CASE → 1, ANCHORED → −1 (#1467), MULTILINE seit #1564 |
| RegexCaptureCount/Start/End/Text | liefern echte Werte: (a)(b) auf zab ergibt CaptureCount = 3, Gruppe 1 von 1 bis 2 (#1468) |
| RegexSearchCompiled/MatchCompiled/ReplaceCompiled und die *Ex-Varianten | benutzbar, seit RegexCompile den Bytecode erzeugt (#1563) |
==== Konstanten ====
^ Name ^ Wert ^ Bedeutung ^
| REGEX_FLAG_IGNORE_CASE | 1 | Groß-/Kleinschreibung ignorieren |
| REGEX_FLAG_ANCHORED | 2 | nur am Textanfang suchen |
| REGEX_FLAG_MULTILINE | 4 | Anker an Zeilengrenzen |
| REGEX_MAX_CAPTURE_SLOTS | 32 | Obergrenze für Capture-Gruppen |
| REGEX_STACK_MAX | 256 | Tiefe des Backtracking-Stapels |
<box tip>
> Alle drei Flags wirken. Frühere Fassungen dieser Seite nannten sie wirkungslos; mit 1.0.21A einzeln nachgemessen:
>
> |
>
> Die Flags werden mit > RegexMatchEx("^ABC$", "abc", 0) → false
> RegexMatchEx("^ABC$", "abc", REGEX_FLAG_IGNORE_CASE) → true
>
> RegexSearchEx("b", "ab", 0) → 1 (gefunden)
> RegexSearchEx("b", "ab", REGEX_FLAG_ANCHORED) → -1 (nur am Anfang)
>
> RegexSearchEx("^zweite", "erste\nzweite", 0) → -1
> RegexSearchEx("^zweite", "erste\nzweite", REGEX_FLAG_MULTILINE) → 6
> kombiniert und den *Ex-Fassungen der Funktionen übergeben.
</box>
—-
===== Beispiele =====
==== Muster prüfen ====
„\\d{4}“
Ausgabe:
import std.regex;
fn pruefe(pat: pchar, txt: pchar): void {
var pos: int64 := RegexSearch(pat, txt);
if (pos >= 0) { PrintLn(" '", pat, "' passt auf '", txt, "' ab Position ", IntToStr(pos)); }
else { PrintLn(" '", pat, "' passt nicht auf '", txt, "'"); }
}
fn main(): int64 {
PrintLn("Zeichenklassen und Quantoren:");
pruefe("[0-9]+", "Bestellung 4711");
pruefe("\\d{4}-\\d{2}-\\d{2}", "am 2026-08-13 geliefert");
pruefe("[a-z]+@[a-z]+\\.[a-z]{2,3}", "mail: [email protected]");
PrintLn("Anker:");
pruefe("^GET ", "GET /index.html");
pruefe("^GET ", "POST /form");
pruefe("\\.txt$", "bericht.txt");
PrintLn("Alternation und Gruppen:");
pruefe("(GET|POST|PUT) /", "PUT /api/v1");
pruefe("(ab)+c", "xxababc");
PrintLn("Escapes:");
pruefe("a\\.c", "a.c");
pruefe("a\\.c", "abc");
return 0;
}
Im Lyx-Quelltext braucht jeder Backslash des Musters einen zweiten: Zeichenklassen und Quantoren:
'[0-9]+' passt auf 'Bestellung 4711' ab Position 11
'\d{4}-\d{2}-\d{2}' passt auf 'am 2026-08-13 geliefert' ab Position 3
'[a-z]+@[a-z]+\.[a-z]{2,3}' passt auf 'mail: [email protected]' ab Position 6
Anker:
'^GET ' passt auf 'GET /index.html' ab Position 0
'^GET ' passt nicht auf 'POST /form'
'\.txt$' passt auf 'bericht.txt' ab Position 7
Alternation und Gruppen:
'(GET|POST|PUT) /' passt auf 'PUT /api/v1' ab Position 0
'(ab)+c' passt auf 'xxababc' ab Position 2
Escapes:
'a\.c' passt auf 'a.c' ab Position 0
'a\.c' passt nicht auf 'abc'
ergibt das Muster \d{4}.
==== Treffer finden, ausmessen und ersetzen ====
RegexFindAll
Ausgabe:
import std.regex;
import std.alloc;
fn main(): int64 {
var log: pchar := "10.0.0.1 - 200 - 10.0.0.7 - 404 - 192.168.1.9 - 500";
// Alle Treffer auf einmal
var out: int64 := alloc(16 * 8);
var n: int64 := RegexFindAll("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+", log, out, 16);
PrintLn("gefundene IP-Adressen: ", IntToStr(n));
var i: int64 := 0;
while (i < n) {
PrintLn(" ", peek64(out + i * 8) as pchar);
i := i + 1;
}
// Position und Ende eines einzelnen Treffers
var sb: int64 := alloc(8);
var eb: int64 := alloc(8);
if (RegexSearchSpan("[0-9]{3}", log, sb, eb)) {
PrintLn("erster Statuscode: Zeichen ", IntToStr(peek64(sb)), " bis ", IntToStr(peek64(eb)));
}
// Ersetzen — nur RegexReplaceInto ersetzt tatsaechlich
var dest: int64 := alloc(256);
var anzahl: int64 := RegexReplaceInto(dest as pchar, "[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+", log, "<IP>");
PrintLn("ersetzt: ", IntToStr(anzahl));
PrintLn("Ergebnis: ", dest as pchar);
return 0;
}
Zwei Dinge zur Speicherverwaltung: gefundene IP-Adressen: 3
10.0.0.1
10.0.0.7
192.168.1.9
erster Statuscode: Zeichen 11 bis 14
ersetzt: 3
Ergebnis: <IP> - 200 - <IP> - 404 - <IP> - 500
legt jeden Treffer als eigenen Puffer an — der Aufrufer gibt sie mit free(zeiger, laenge + 1) einzeln frei. RegexReplaceInto prüft die Größe von dest nicht; der Puffer muss groß genug für den Ergebnistext sein.
—-
===== Was nicht funktioniert =====
Geprüft mit lyxc 1.0.21A.
==== RegexReplace ersetzt nicht (#1466) ====
replacementvar text: pchar := "foo bar foo baz foo";
var n: int64 := RegexReplace("foo", text, "XXX");
PrintLn("Rueckgabe = ", IntToStr(n));
PrintLn("Text danach: '", text, "'");
Der Parameter Rueckgabe = 3
Text danach: 'foo bar foo baz foo'
wird im Rumpf nie verwendet; die Funktion zählt lediglich die Treffer. Als Zähler ist sie brauchbar — als Ersetzung nicht. Für tatsächliche Ersetzungen RegexReplaceInto mit Zielpuffer verwenden.
==== Flags und Capture-Gruppen ====
Beides arbeitet. Mit 1.1.2E nachgemessen:
REGEX_FLAG_MULTILINERegexMatchEx("ABC", "abc", REGEX_FLAG_IGNORE_CASE) = 1
RegexSearchEx("abc", "xxabc", REGEX_FLAG_ANCHORED) = -1
RegexSearchEx("^zweite", "erste\nzweite", REGEX_FLAG_MULTILINE) = 6
RegexSearch("(a)(b)", "zab") = 1
RegexCaptureCount() = 3 (Gruppe 0 ist der Gesamttreffer)
RegexCaptureStart(1)/End(1) = 1 / 2
wurde bis #1564 entgegengenommen, aber nicht ausgewertet. Jetzt gilt $ auch unmittelbar vor einem \n, und ^ bekommt jede Stelle hinter einem \n als Ansatzpunkt — die Verankerung bleibt dabei erhalten (^[a-z]+$ findet weiterhin nicht allo in Hallo).
==== Die kompilierte API ist benutzbar (#1563) ====
Der fehlende Erzeuger für das Bytecode-Programm ist da: RegexCompile(pattern: pchar, progOut: pchar, progMax: int64): int64 übersetzt ein Muster und liefert die Länge des Programms — 0 bei unübersetzbarem Muster oder zu kleinem Puffer. Unterstützt wird derselbe Sprachumfang wie in der Muster-API.
r„…“
Das var prog: int64 := alloc(4096);
var n: int64 := RegexCompile("[0-9]+"c, prog as pchar, 4096);
PrintLn(IntToStr(n)); // 54
PrintLn(IntToStr(RegexSearchCompiled(prog as pchar, n, "abc 4711"c))); // 4
-Literal ist dafür weiterhin nicht geeignet: es liefert den Rohtext des Musters, keinen Bytecode. Der Bytecode kommt ausschließlich aus RegexCompile.
==== Weitere Grenzen ====
* Byteweise Verarbeitung: . und Zeichenklassen arbeiten auf Bytes. Bei UTF-8 zählt . ein Byte eines mehrbyteigen Zeichens; [ä-ö] funktioniert nicht.
* Backtracking ohne Schutz: Muster wie (a+)+b auf einer langen Kette von a können sehr lange laufen. REGEX_STACK_MAX begrenzt die Stapeltiefe, nicht die Laufzeit — Muster aus fremder Quelle sind damit ein Risiko.
* Muster einmal übersetzen: Die Muster-API wertet das Muster bei jedem Aufruf neu aus. In heißen Schleifen lohnt RegexCompile einmal vorab und danach RegexSearchCompiled.
—-
===== Verwandte Units =====
* std.string — StrFind, StrSub, StrSplit'' für einfache Fälle ohne Regex
* std.validate — fertige Prüfungen für IBAN, ISO-Codes und Ähnliches
* std.text — Textverarbeitung auf höherer Ebene
