====== std.regex — Reguläre Ausdrücke ======
→ [[lyx_-_programmiersprache:units|Zurück zur Unit-Übersicht]]
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''
**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.
===== 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 [[https://github.com/SEOLizer/LyX-Compiler/issues/1564|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1563|#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 |
> **Alle drei Flags wirken.** Frühere Fassungen dieser Seite nannten sie wirkungslos; mit 1.0.21A einzeln nachgemessen:
>
>
> 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
>
>
> Die Flags werden mit ''%%|%%'' kombiniert und den ''*Ex''-Fassungen der Funktionen übergeben.
----
===== Beispiele =====
==== Muster prüfen ====
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: kontakt@example.com");
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;
}
Ausgabe:
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: kontakt@example.com' 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'
Im Lyx-Quelltext braucht jeder Backslash des Musters einen zweiten: ''"\\d{4}"'' ergibt das Muster ''\d{4}''.
==== Treffer finden, ausmessen und ersetzen ====
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, "");
PrintLn("ersetzt: ", IntToStr(anzahl));
PrintLn("Ergebnis: ", dest as pchar);
return 0;
}
Ausgabe:
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: - 200 - - 404 - - 500
Zwei Dinge zur Speicherverwaltung: ''RegexFindAll'' 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) ====
var text: pchar := "foo bar foo baz foo";
var n: int64 := RegexReplace("foo", text, "XXX");
PrintLn("Rueckgabe = ", IntToStr(n));
PrintLn("Text danach: '", text, "'");
Rueckgabe = 3
Text danach: 'foo bar foo baz foo'
Der Parameter ''replacement'' 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:
RegexMatchEx("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
''REGEX_FLAG_MULTILINE'' wurde bis [[https://github.com/SEOLizer/LyX-Compiler/issues/1564|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1563|#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.
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
Das ''r"…"''-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 =====
* [[lyx_-_programmiersprache:units:string|std.string]] — ''StrFind'', ''StrSub'', ''StrSplit'' für einfache Fälle ohne Regex
* [[lyx_-_programmiersprache:units:validate:iban|std.validate]] — fertige Prüfungen für IBAN, ISO-Codes und Ähnliches
* [[lyx_-_programmiersprache:units:text|std.text]] — Textverarbeitung auf höherer Ebene