Rolf b: Suche eine Regex

Hallo alle,

ich suche Regex-Unterstützung für folgende Aufgabenstellung, bzw. hätte gerne einen Tipp, ob und wie man sie in eine mehrstufige Prüfung (gerne mit Regexen) aufteilt.

Ein einzeiliger String enthält 1-n Regeln. Leerstellen sind darin dort erlaubt, wo ich es ausdrücklich erlaube. Ich möchte diesen String parsen und bei Syntaxfehlern halbwegs exakt angeben können, wo der Fehler ist. Ich möchte mich nicht in Monster wie Antlr oder so einarbeiten, die für diese Aufgabe vermutlich die beste Lösung wären...

Regeln sind durch Komma getrennt. Die Kommas dürfen von Leerstellen umgeben sein.

Eine Regel besteht aus einer Source-Referenz, gefolgt von einem Vergleichsoperator, gefolgt von einem Vergleichswert.

Eine Source-Referenz ist ein einfacher Name. Er kann aus ASCII-Buchstaben und Ziffern bestehen, darf aber nicht mit einer Ziffer beginnen. Ja, ich weiß, es gibt UNICODE. Hier brauche ich aber ASCII. (?<dataRef>[a-zA-Z][a-zA-Z0-9]*)

Vergleichsoperator ist =, !=, <, >, <=, >= oder LIKE. Die Vergleichsoperatoren können von Leerstellen umgeben sein, LIKE muss mindestens eine Leerstelle vor und hinter sich haben.

Ein Vergleichswert ist ein einfacher Wert oder eine Werteliste.

Ein einfacher Wert kann eins von drei Dingen sein:

  • Eine Integerzahl
  • Eine Stringkonstante, in Hochkomma eingeschlossen. Hochkomma innnerhalb des Strings werden durch ein \ escaped. Innerhalb des Strings ist alles erlaubt (außer Zeilenumbrüchen...).
  • Eine Variablenreferenz. Das ist ein oder zwei $, gefolgt von einer Datenreferenz. ($foo, $bar.name). Eine Datenreferenz kann eins von dreien sein:
    • sie ist leer ("$" ist eine zulässige Variablenreferenz)
    • ein erweiterter Name, bestehend aus Buchstaben, Ziffern, Minuszeichen und Unterstrich
    • Eine Datenreferenz, gefolgt von einem Punkt, gefolgt von einem erweiterten Namen.

Meine Definition erlaubt "$.foo" als Variablenreferenz, das ist korrekt so. Besondere Regeln, wie sich ein Namensteil aus den zulässigen Zeichen zusammensetzt, gebe ich nicht vor.

Eine Werteliste besteht aus einer linken Klammer, einer beliebigen Menge von einfachen Werten, die durch Komma getrennt sind, und einer rechten Klammer. Die Kommas dürfen von Leerstellen umgeben sein.

Rechenoperationen oder String-Operatoren habe ich mir verkniffen. Das wäre eventuell zwar hilfreich, aber dann wird es noch wüster.

Meine bisherigen Versuche sind alle in einem Sumpf von Redundanz abgesoffen, bzw. sind daran gescheitert, bei Kommas und Klammern - vor allem innerhalb von Stringkonstanten - sauber abzugrenzen.

Einige Teilausdrücke, mit denen ich mein Glück versucht habe:

Integerzahl: -?[0-9]×
Stringkonstante: '.?(?<!\)'
Einfacher Name: [a-z][a-z0-9]

Variablenreferenz: ${1,2}[a-z0-9_-](.[a-z0-9_-])

Wie mach ich's am besten?

Rolf

--
Dosen sind silbern

akzeptierte Antworten

  1. Suche eine Regex

    Wo genau hakts denn? Es gibt Tools wie Regex101 (pcre, js, python, golang) oder regexstorm (.NET/C#) wo man das doch alles schön testen und zusammenzimmern kann.

    ${1,2}

    Hiezu würdest du z.B. auf regex101 schonmal sehen: The preceding token is not quantifiable. Natürlich meintest du \${1,2}

    Interessante Tutorials wären z.B. regular-expressions.info oder rexegg.

    Wenn du etwas konkreter würdest

    1. Welche Regex Implementation/Sprache?
    2. Unterschiedliche Beispiele für Input
    3. Woher kommt der Input und was ist das Ziel der Prüfung?

    könnte man wohl besser weiterhelfen.

    1. Hallo Jonny,

      es hakte an der Komplexität und an der Frage, ob ich mit einer einzelnen Regex tatsächlich auf dem richtigen Weg bin. Grundsätzlich ist mir der gemeine Tyrannosuchus Regex durchaus bekannt.

      Tools wie regex101 oder regexstorm kenne ich. ich baue eine C# Lösung, also ist regexstorm das Mittel der Wahl. Nur ist dieses Tool für das Monster, das ich da aufbauen muss, etwas unübersichtlich, und ich bin stecken geblieben.

      Den Backslash vor dem $ hatte ich in meinen Versuchen übrigens drin, den hab ich nur zu kopieren vergessen.

      Rolf

      --
      Dosen sind silbern
    2. Hallo Jonny,

      achso, zu deiner Frage 3: Ich baue ein Tool, um an unserer CTI-Lösung Konfigänderungen automatisiert einspielen zu können.

      Dafür gibt's dann eine Art einfacher Scriptsprache, und ein Befehl soll die zu ändernden Objekte selektieren können. Teilweise muss ich auch mehrstufig selektieren, d.h. erstmal ein Bezugsobjekt, von dort alle abhängigen Objekte (dafür die Variablenreferenzen im Wert).

      Die Eingabe ist also menschengemacht, von Administratoren der Anwendung, und wird aus einer Datei gelesen. Insofern ist nichts Bösartiges zu erwarten, aber Tippfehler und falsch verstandene Anwendung. Zielumgebung ist Windows Server 2012++. Ich hätte also ggf. auch eine Powershell-Extension bauen können, war mir aber nicht sicher, ob ich den Admins das zumuten kann. Admins sind eher keine Programmierer.

      Ziel ist also, die angegebenen Regeln zu verstehen und in Queries gegen den Config Server der CTI Plattform zu übersetzen.

      SELECT DN(VoipService) WHERE name=('DP_NORD', 'DP_SUED')

      wäre so ein Befehl, er würde mir VOIP Services selektieren, deren Namen DP_NORD oder DP_SUED lauten (Dial-Plans). Meine Frage hier bezog sich auf den Teil hinter dem WHERE.

      Rolf

      --
      Dosen sind silbern
    3. Hallo,

      Interessante Tutorials wären z.B. regular-expressions.info oder rexegg.

      nur zur Ergänzung, damit auch was deutsches dabei ist:

      http://regexp-evaluator.de/tutorial/

      und den dortigen Tester.

      Gruss
      Henry

  2. @@Rolf b

    Ich möchte diesen String parsen und bei Syntaxfehlern halbwegs exakt angeben können, wo der Fehler ist.

    Ich bin mir nicht sicher, ob reguläre Ausdrücke hierfür das geeignete Werkzeug sind.

    Ein einzeiliger String enthält 1-n Regeln. … Regeln sind durch Komma getrennt. Die Kommas dürfen von Leerstellen umgeben sein.

    [1] ⟨String⟩ ::= ⟨Regel⟩(\s*,\s*⟨Regel⟩)*
    

    Eine Regel besteht aus einer Source-Referenz, gefolgt von einem Vergleichsoperator, gefolgt von einem Vergleichswert.

    [2] ⟨Regel⟩ ::= ⟨SourceReferenz⟩⟨Vergleichsoperator⟩⟨Vergleichswert⟩
    

    Eine Source-Referenz ist ein einfacher Name. Er kann aus ASCII-Buchstaben und Ziffern bestehen, darf aber nicht mit einer Ziffer beginnen. Ja, ich weiß, es gibt UNICODE. Hier brauche ich aber ASCII. (?<dataRef>[a-zA-Z][a-zA-Z0-9]*)

    [3] ⟨SourceReferenz⟩ ::= [a-zA-Z][a-zA-Z0-9]*
    

    Vergleichsoperator ist =, !=, <, >, <=, >= oder LIKE. Die Vergleichsoperatoren können von Leerstellen umgeben sein, LIKE muss mindestens eine Leerstelle vor und hinter sich haben.

    [4] ⟨Vergleichsoperator⟩ ::= \s*(=|!=|<|>|<=|>=|\sLIKE\s)\s*
    

    Hier ließe sich auch zu \s*(!?=|[<>]=?|\sLIKE\s)\s* zusammenfassen, aber das macht den Ausdruck nicht besser lesbar.

    Ein Vergleichswert ist ein einfacher Wert oder eine Werteliste.

    [5] ⟨Vergleichswert⟩ ::= (⟨einfacherWert⟩|⟨Werteliste⟩)
    

    Ein einfacher Wert kann eins von drei Dingen sein:

    [6] ⟨einfacherWert⟩ ::= (⟨Integerzahl⟩|⟨Stringkonstante⟩|⟨Variablenreferenz⟩)
    
    • Eine Integerzahl
    [7] ⟨Integerzahl⟩ ::= [+-]?[0-9]+
    

    Plus ist erlaubt? Führende Nullen sind erlaubt?

    • Eine Stringkonstante, in Hochkomma eingeschlossen. Hochkomma innnerhalb des Strings werden durch ein \ escaped. Innerhalb des Strings ist alles erlaubt (außer Zeilenumbrüchen...).
    [8] ⟨Stringkonstante⟩ ::= '([^']|\\')*'
    
    • Eine Variablenreferenz. Das ist ein oder zwei $, gefolgt von einer Datenreferenz.
    [9] ⟨Variablenreferenz⟩ ::= \$\$?⟨Datenreferenz⟩
    

    Eine Datenreferenz kann eins von dreien sein:

    • sie ist leer ("$" ist eine zulässige Variablenreferenz)
    • ein erweiterter Name, bestehend aus Buchstaben, Ziffern, Minuszeichen und Unterstrich
    • Eine Datenreferenz, gefolgt von einem Punkt, gefolgt von einem erweiterten Namen.
    [10] ⟨erweiterterName⟩ ::= [a-zA-Z0-9_-]+
    [11] ⟨leerOderErweiterterName⟩ ::= [a-zA-Z0-9_-]*
    

    Ein erweiterter Name muss nicht mit einem Buchstaben beginnen?

    [12] ⟨Datenreferenz⟩ ::= ((⟨leerOderErweiterterName⟩\.)*⟨erweiterterName⟩)?
    

    Eine Werteliste besteht aus einer linken Klammer, einer beliebigen Menge von einfachen Werten, die durch Komma getrennt sind, und einer rechten Klammer. Die Kommas dürfen von Leerstellen umgeben sein.

    [13] ⟨Werteliste⟩ ::= \(⟨einfacherWert⟩(\s*,\s*⟨einfacherWert⟩)*\)
    

    Das musst du jetzt nur noch™ alles zusammensetzen:

    [10] und [11] in [12] eingesetzt ergibt:

    [14] ⟨Datenreferenz⟩ = (([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?
    

    [14] in [9]:

    [15] ⟨Variablenreferenz⟩ = \$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?
    

    [7], [8], [15] in [6]:

    [16] ⟨einfacherWert⟩ = ([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)
    

    [16] in [13]:

    [17] ⟨Werteliste⟩ = \(([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)(\s*,\s*([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?))*\)
    

    [16], [17] in [5]:

    [18] ⟨Vergleichswert⟩ = (([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)|\(([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)(\s*,\s*([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?))*\))
    

    [3], [4], [18] in [2]:

    [19] ⟨Regel⟩ = [a-zA-Z][a-zA-Z0-9]*\s*(=|!=|<|>|<=|>=|\sLIKE\s)\s*(([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)|\(([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)(\s*,\s*([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?))*\))
    

    [19] in [1]:

    [20] ⟨String⟩ = [a-zA-Z][a-zA-Z0-9]*\s*(=|!=|<|>|<=|>=|\sLIKE\s)\s*(([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)|\(([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)(\s*,\s*([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?))*\))(\s*,\s*[a-zA-Z][a-zA-Z0-9]*\s*(=|!=|<|>|<=|>=|\sLIKE\s)\s*(([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)|\(([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?)(\s*,\s*([+-]?[0-9]+|'([^']|\\')*'|\$\$?(([a-zA-Z0-9_-]*\.)*[a-zA-Z0-9_-]+)?))*\)))*
    

    Bei konkreten Implementationen müssen ggfs. noch Zeichen escapet werden und wenn Teile nicht gemerkt werden sollen, wäre (?: statt ( zu schreiben – außer natürlich bei \(.

    LLAP 🖖

    --
    “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
    1. @@Gunnar Bittersmann

      Eine Regel besteht aus einer Source-Referenz, gefolgt von einem Vergleichsoperator, gefolgt von einem Vergleichswert.

      [2] ⟨Regel⟩ ::= ⟨SourceReferenz⟩⟨Vergleichsoperator⟩⟨Vergleichswert⟩
      

      Vergleichsoperator ist =, !=, <, >, <=, >= oder LIKE. Die Vergleichsoperatoren können von Leerstellen umgeben sein, LIKE muss mindestens eine Leerstelle vor und hinter sich haben.

      [4] ⟨Vergleichsoperator⟩ ::= \s*(=|!=|<|>|<=|>=|\sLIKE\s)\s*
      

      Die Leerzeichen wären besser in [2] untergebracht anstatt in [4]:

      [2] ⟨Regel⟩ ::= ⟨SourceReferenz⟩\s*⟨Vergleichsoperator⟩\s*⟨Vergleichswert⟩
      [4] ⟨Vergleichsoperator⟩ ::= (=|!=|<|>|<=|>=|\sLIKE\s)
      

      Beim Zusammensetzen [3], [4], [18] in [2] → [19] ändert sich dadurch natürlich nichts.

      LLAP 🖖

      --
      “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
    2. Hallo Gunnar,

      2:54 Uhr? Hast Du Dir deswegen die Nacht um die Ohren gehauen? Sozusagen als Challenge?

      Das Ergebnis muss ich mir jetzt erstmal gaaanz ruhig zu Gemüte führen. Kein Wunder, dass ich ständig abgesoffen bin beim Versuch, das zusammenzubekommen.

      Allerdings gefällt mir die deutliche sichtbare Redundanz im Ergebnis nicht. Sie wird letztlich nicht vermeidbar sein, aber das war einer der Punkte, an denen ich bei meinen Versuchen immer zurückgeschreckt bin. Vielleicht müsste man die Regex - analog deinem Vorgehen im Posting - im Programm aus Strings zusammensetzen. Dürfte dem Verständnis des Lesers gut tun, und da es C# ist, passiert das auch nicht bei jedem Gebrauch auf's neue.

      Mein Problem war auch oft, dass die * zu gierig waren, es ist bei meinen Versuchen fast immer auf *? hinausgelaufen. Ich bin gespannt, wie sich das bei deiner Lösung ergibt.

      Aber ich denke, dass dein erster Satz der entscheidenste ist: Ist EINE Regex das Mittel der Wahl. Ich werde es wohl zweistufig versuchen: Erstmal in Regeln zerhacken, und die dann genauer betrachten. Das dürfte einiges an Redundanz aus den Suchausdrücken nehmen.

      Jedenfalls vielen Dank für die Mühe, das hat mir sehr geholfen.

      Rolf

      --
      Dosen sind silbern
      1. @@Rolf b

        2:54 Uhr? Hast Du Dir deswegen die Nacht um die Ohren gehauen? Sozusagen als Challenge?

        Nee, andersrum. Ich hab mir die Nacht um die Ohren gehauen, deswegen die Challenge.

        Das Ergebnis muss ich mir jetzt erstmal gaaanz ruhig zu Gemüte führen.

        Das Ergebnis [20] will man sich eigentlich gar nicht zu Gemüte führen.

        Interessant ist der Teil von [1] bis [13]. Danach folgt nur noch das Ersetzen der Variablen durch die zuvor ermittelten rechten Seiten aus anderen Regeln mittels copy and paste – und hoffen, dass sich dabei kein Fehler einschleicht.

        Kein Wunder, dass ich ständig abgesoffen bin beim Versuch, das zusammenzubekommen.

        Ich würde mir auch nicht das Ergebnis [20] einfach so aus dem Hut zaubern können. Der Trick hier ist, das gar nicht erst zu versuchen, sondern die einfachen Regeln [1] bis [13] zu erstellen und dann zu ersetzen.

        Sollte das Ergebnis nicht das tun, was es soll, würde ich mir auch nicht die [20] vornehmen (Viel Spaß bei der Fehlersuche!), sondern [1] bis [13] überprüfen und nochmals copy and paste.

        Aber ich denke, dass dein erster Satz der entscheidenste ist: Ist EINE Regex das Mittel der Wahl. Ich werde es wohl zweistufig versuchen: Erstmal in Regeln zerhacken, und die dann genauer betrachten.

        Das hört sich nach einem Plan an.

        LLAP 🖖

        --
        “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
  3. Hallo Rolf,

    Ich habe zwar keine Ahnung was Du machen willst, weil du keine Beispiele gibst, aber wenn ich komplexe reguläre Ausdrücke zusammenschustere, verwende ich regex – das hat die gleiche API wie Pythons Standard re modul plus Erweiterungen – und als Editor verwende ich den Code Browser, da kann ich mir dann die einzelnen Teile des Regexes schön zusammenfalten.

    Deine Aversion gegen ANTLR kann ich sehr gut verstehen, ich finde dieses Framework auch monströs. Meine Lieblingsparser sind TatSu, pyPEG, und PEG.js, bei letzterem kanst Du ohne Installation im Browser testen. Für C# kenne ich allerdings da nichts. Ist mir zu kompliziert.

    Gruß, Nils

  4. Wäre ein passender Minimalparser, dem du deine Regeln mittels ABNF beibringst, nicht besser geeignet?

    Für C# sollte das gehen (nicht getestet): http://www.parse2.com/manual-cs.shtml

    1. Hallo andaris,

      ok, danke für den Hinweis. Den guck ich mir an.

      Rolf

      --
      Dosen sind silbern
      1. Tjaaaa.

        Es kam, wie es musste. Ich bin bei ANTLR hängen geblieben. Weil - wenn man schon mit einem Parser anfängt, macht man es ja auch ordentlich und parsed damit nicht nur diesen einen String, sondern auch alles drumherum. Und da waren alle anderen, die für mich in Frage kamen und die ich mir angeguckt habe (parse2, SableCC, Grammatica) entweder zu schwach auf der Brust oder wurden nicht mehr gepflegt.

        Nachdem ich über den ersten Schmerz weg bin, und weil das Brett durch das Prüfen der anderen Parser schon etwas angebohrt war, gefällt mit ANTLR jetzt eigentlich ganz gut...

        Rolf

        --
        Dosen sind silbern