====== 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