web-dev-qa-db-fra.com

Regex pour faire correspondre les fonctions et capturer leurs arguments

Je travaille sur une calculatrice qui prend des expressions de chaîne et les évalue. J'ai une fonction qui recherche dans l'expression des fonctions mathématiques à l'aide de Regex, récupère les arguments, recherche le nom de la fonction et l'évalue. Ce qui me pose problème, c'est que je ne peux le faire que si je sais combien d'arguments il va y avoir, je ne peux pas obtenir le regex juste. Et si je divise simplement le contenu des caractères ( et ) par le caractère ,, je ne peux pas avoir d'autres appels de fonction dans cet argument.

Voici le modèle de correspondance de fonction: \b([a-z][a-z0-9_]*)\((..*)\)\b

Cela ne fonctionne qu'avec un seul argument. Puis-je créer un groupe pour chaque argument, à l'exception de ceux contenus dans des fonctions imbriquées? Par exemple, il correspondrait à: func1(2 * 7, func2(3, 5)) et créerait des groupes de capture pour: 2 * 7 et func2(3, 5).

Voici la fonction que j'utilise pour évaluer l'expression:

    /// <summary>
    /// Attempts to evaluate and store the result of the given mathematical expression.
    /// </summary>
    public static bool Evaluate(string expr, ref double result)
    {
        expr = expr.ToLower();

        try
        {
            // Matches for result identifiers, constants/variables objects, and functions.
            MatchCollection results = Calculator.PatternResult.Matches(expr);
            MatchCollection objs = Calculator.PatternObjId.Matches(expr);
            MatchCollection funcs = Calculator.PatternFunc.Matches(expr);

            // Parse the expression for functions.
            foreach (Match match in funcs)
            {
                System.Windows.Forms.MessageBox.Show("Function found. - " + match.Groups[1].Value + "(" + match.Groups[2].Value + ")");

                int argCount = 0;
                List<string> args = new List<string>();
                List<double> argVals = new List<double>();
                string funcName = match.Groups[1].Value;

                // Ensure the function exists.
                if (_Functions.ContainsKey(funcName)) {
                    argCount = _Functions[funcName].ArgCount;
                } else {
                    Error("The function '"+funcName+"' does not exist.");
                    return false;
                }

                // Create the pattern for matching arguments.
                string argPattTmp = funcName + "\\(\\s*";

                for (int i = 0; i < argCount; ++i)
                    argPattTmp += "(..*)" + ((i == argCount - 1) ? ",":"") + "\\s*";
                argPattTmp += "\\)";

                // Get all of the argument strings.
                Regex argPatt = new Regex(argPattTmp);

                // Evaluate and store all argument values.
                foreach (Group argMatch in argPatt.Matches(match.Value.Trim())[0].Groups)
                {
                    string arg = argMatch.Value.Trim();
                    System.Windows.Forms.MessageBox.Show(arg);

                    if (arg.Length > 0)
                    {
                        double argVal = 0;

                        // Check if the argument is a double or expression.
                        try {
                            argVal = Convert.ToDouble(arg);
                        } catch {
                            // Attempt to evaluate the arguments expression.
                            System.Windows.Forms.MessageBox.Show("Argument is an expression: " + arg);

                            if (!Evaluate(arg, ref argVal)) {
                                Error("Invalid arguments were passed to the function '" + funcName + "'.");
                                return false;
                            }
                        }

                        // Store the value of the argument.
                        System.Windows.Forms.MessageBox.Show("ArgVal = " + argVal.ToString());
                        argVals.Add(argVal);
                    }
                    else
                    {
                        Error("Invalid arguments were passed to the function '" + funcName + "'.");
                        return false;
                    }
                }

                // Parse the function and replace with the result.
                double funcResult = RunFunction(funcName, argVals.ToArray());
                expr = new Regex("\\b"+match.Value+"\\b").Replace(expr, funcResult.ToString());
            }

            // Final evaluation.
            result = Program.Scripting.Eval(expr);
        }
        catch (Exception ex)
        {
            Error(ex.Message);
            return false;
        }

        return true;
    }

    ////////////////////////////////// ---- PATTERNS ---- \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

    /// <summary>
    /// The pattern used for function calls.
    /// </summary>
    public static Regex PatternFunc = new Regex(@"([a-z][a-z0-9_]*)\((..*)\)");

Comme vous pouvez le constater, il y a une très mauvaise tentative de construire un regex qui corresponde aux arguments. Ça ne marche pas.

Tout ce que j'essaie de faire est d'extraire 2 * 7 et func2(3, 5) de l'expression func1(2 * 7, func2(3, 5)), mais cela doit également fonctionner pour des fonctions avec des nombres d'arguments différents. S'il existe un moyen de le faire sans utiliser Regex, c'est également bien.

14
Brandon Miller

Il existe à la fois une solution simple et une solution plus avancée (ajoutée après edit ) pour gérer des fonctions plus complexes. 

Pour réaliser l'exemple que vous avez posté, je suggère de le faire en deux étapes. La première étape consiste à extraire les paramètres (les expressions rationnelles sont expliquées à la fin): 

\b[^()]+\((.*)\)$

Maintenant, pour analyser les paramètres. 

Solution simple

Extrayez les paramètres en utilisant: 

([^,]+\(.+?\))|([^,]+)

Voici quelques exemples de code C # (tous les assertions passent): 

string extractFuncRegex = @"\b[^()]+\((.*)\)$";
string extractArgsRegex = @"([^,]+\(.+?\))|([^,]+)";

//Your test string
string test = @"func1(2 * 7, func2(3, 5))";

var match = Regex.Match( test, extractFuncRegex );
string innerArgs = match.Groups[1].Value;
Assert.AreEqual( innerArgs, @"2 * 7, func2(3, 5)" );
var matches = Regex.Matches( innerArgs, extractArgsRegex );            
Assert.AreEqual( matches[0].Value, "2 * 7" );
Assert.AreEqual( matches[1].Value.Trim(), "func2(3, 5)" );

Explication de regexes. L'extraction des arguments en une seule chaîne: 

\b[^()]+\((.*)\)$

où: 

  • [^()]+ caractères qui ne sont pas des crochets ouvrants ou fermants. 
  • \((.*)\) tout entre les crochets

L'extraction des args:

([^,]+\(.+?\))|([^,]+)

où:

  • ([^,]+\(.+?\)) caractère qui ne sont pas des virgules suivies de caractères entre parenthèses. Ceci reprend les arguments de func. Notez le +? de sorte que le match est paresseux et s'arrête au premier) il se rencontre. 
  • |([^,]+) Si le précédent ne correspond pas, faites correspondre les caractères consécutifs qui ne sont pas des virgules. Ces matchs vont dans les groupes.

Solution plus avancée

À présent, cette approche présente des limitations évidentes, par exemple, elle correspond au premier crochet de fermeture, de sorte qu'elle ne gère pas très bien les fonctions imbriquées. Pour une solution plus complète (si vous en avez besoin), nous devons utiliser définitions du groupe d'équilibrage (comme je l'ai mentionné précédemment dans cette modification). Pour nos besoins, les définitions de groupe d'équilibrage nous permettent de suivre les instances des crochets ouverts et de soustraire les instances de crochets fermants. En substance, les supports d’ouverture et de fermeture s’annulent mutuellement dans la partie d’équilibrage de la recherche jusqu’à ce que le support de fermeture final soit trouvé. C'est-à-dire que le match continuera jusqu'à ce que les supports soient en équilibre et que le dernier support soit trouvé.

Donc, l'expression rationnelle pour extraire les paramètres est maintenant (l'extraction func peut rester la même): 

(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*\)))*)+

Voici quelques cas de test pour le montrer en action: 

string extractFuncRegex = @"\b[^()]+\((.*)\)$";
string extractArgsRegex = @"(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*\)))*)+";

//Your test string
string test = @"func1(2 * 7, func2(3, 5))";

var match = Regex.Match( test, extractFuncRegex );
string innerArgs = match.Groups[1].Value;
Assert.AreEqual( innerArgs, @"2 * 7, func2(3, 5)" );
var matches = Regex.Matches( innerArgs, extractArgsRegex );
Assert.AreEqual( matches[0].Value, "2 * 7" );
Assert.AreEqual( matches[1].Value.Trim(), "func2(3, 5)" );

//A more advanced test string
test = @"someFunc(a,b,func1(a,b+c),func2(a*b,func3(a+b,c)),func4(e)+func5(f),func6(func7(g,h)+func8(i,(a)=>a+2)),g+2)";
match = Regex.Match( test, extractFuncRegex );
innerArgs = match.Groups[1].Value;
Assert.AreEqual( innerArgs, @"a,b,func1(a,b+c),func2(a*b,func3(a+b,c)),func4(e)+func5(f),func6(func7(g,h)+func8(i,(a)=>a+2)),g+2" );
matches = Regex.Matches( innerArgs, extractArgsRegex );
Assert.AreEqual( matches[0].Value, "a" );
Assert.AreEqual( matches[1].Value.Trim(), "b" );            
Assert.AreEqual( matches[2].Value.Trim(), "func1(a,b+c)" );
Assert.AreEqual( matches[3].Value.Trim(), "func2(a*b,func3(a+b,c))" );
Assert.AreEqual( matches[4].Value.Trim(), "func4(e)+func5(f)" );
Assert.AreEqual( matches[5].Value.Trim(), "func6(func7(g,h)+func8(i,(a)=>a+2))" );
Assert.AreEqual( matches[6].Value.Trim(), "g+2" );

Notez en particulier que la méthode est maintenant assez avancée:

someFunc(a,b,func1(a,b+c),func2(a*b,func3(a+b,c)),func4(e)+func5(f),func6(func7(g,h)+func8(i,(a)=>a+2)),g+2)

Alors, regardons encore la regex: 

(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*\)))*)+

En résumé, il commence par des caractères qui ne sont ni des virgules ni des crochets. Ensuite, s'il y a des crochets dans l'argument, il correspond et soustrait les crochets jusqu'à ce qu'ils s'équilibrent. Il essaie ensuite de répéter cette correspondance s'il existe d'autres fonctions dans l'argument. Il passe ensuite à l'argument suivant (après la virgule). En détail:

  • [^,()]+ correspond à tout ce qui n'est pas ', ()' 
  • ?: signifie un groupe sans capture, c’est-à-dire ne stocke pas les correspondances entre parenthèses dans un groupe.
  • \( signifie commencer à un crochet ouvert. 
  • ?> signifie groupement atomique - essentiellement, cela signifie qu'il ne se souvient pas des positions en arrière. Cela contribue également à améliorer les performances car il y a moins de recul pour essayer différentes combinaisons. 
  • [^()]+| signifie tout sauf une parenthèse ouvrante ou fermante. Ceci est suivi de | (ou)
  • \((?<open>)| Ceci est la bonne chose et dit match '(' ou ' 
  • (?<-open>) C'est le meilleur moyen de faire correspondre un ')' et d'équilibrer le '('. Cela signifie que cette partie de la correspondance (tout ce qui suit le premier crochet) continuera jusqu'à ce que tous les crochets internes correspondent. Sans les expressions d'équilibrage, le match s'achèverait sur la première tranche de fermeture. Le noeud du problème est que le moteur ne correspond pas à ce ')' contre la finale ')', à la place, il est soustrait de la correspondance '('. Quand il n'y a plus rien en suspens '(' , -open échoue pour que la finale ')' puisse être mise en correspondance.
  • Le reste de la regex contient les parenthèses fermantes du groupe et les répétitions (, et +) qui sont respectivement: répétez la correspondance de parenthèse interne 0 fois ou plus, répétez la recherche de parenthèse complète 0 fois ou plus ( 0 autorise les arguments sans crochets) et répète la correspondance complète 1 fois ou plus (permet à foo (1) + foo (2))

Une dernière embellissement: 

Si vous ajoutez (?(open)(?!)) à l'expression régulière:

(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*(?(open)(?!))\)))*)+

(?!) Échouera toujours si open a capturé quelque chose (qui n’a pas été soustrait), c’est-à-dire qu’il échouera toujours s’il existe un crochet d’ouverture sans crochet de fermeture. C'est un moyen utile de vérifier si l'équilibrage a échoué. 

Quelques notes:

  • \ b ne correspondra pas lorsque le dernier caractère est un ')' car il ne s'agit pas d'un caractère Word et\b teste les limites des caractères Word afin que votre expression régulière ne corresponde pas.
  • Bien que regex soit puissant, à moins que vous ne soyez un gourou parmi les gourous, il est préférable de garder les expressions simples, car sinon elles sont difficiles à maintenir et à comprendre pour les autres. C'est pourquoi il est parfois préférable de diviser le problème en sous-problèmes et en expressions plus simples et de laisser le langage effectuer certaines des opérations de recherche/correspondance autres que celles pour lesquelles il est bon. Donc, vous voudrez peut-être mélanger des expressions rationnelles simples avec un code plus complexe ou inversement, selon votre confort.
  • Cela correspond à des fonctions très complexes, mais ce n'est pas un analyseur lexical pour les fonctions.
  • Si vous pouvez avoir des chaînes dans les arguments et si les chaînes elles-mêmes peuvent contenir des crochets, par exemple. "go (..." alors vous devrez modifier la regex pour extraire les chaînes de la comparaison. Idem avec les commentaires.
  • Quelques liens pour l’équilibrage des définitions des groupes: ici , ici , ici et ici

J'espère que cela pourra aider. 

31
acarlon

Je suis désolé de faire éclater la bulle RegEx, mais c’est une de ces choses que vous ne pouvez pas faire efficacement avec des expressions régulières uniquement.

Ce que vous implémentez est fondamentalement un analyseur Operator-Precedence Parser qui prend en charge les sous-expressions et les listes d’arguments. L'instruction est traitée comme un flux de jetons, éventuellement à l'aide d'expressions régulières, avec des sous-expressions traitées comme opérations de haute priorité.

Avec le bon code, vous pouvez le faire sous forme d'itération sur le flux de jetons complet, mais les analyseurs récursifs sont également courants. Quoi qu'il en soit, vous devez être en mesure de pousser efficacement l'état et de relancer l'analyse à chacun des points d'entrée de la sous-expression - un jeton (, , ou <function_name>( - et de pousser le résultat dans la chaîne d'analyse à partir des points de sortie de la sous-expression - ) ou , jeton.

4
Corey

Cette regex fait ce que vous voulez:

^(?<FunctionName>\w+)\((?>(?(param),)(?<param>(?>(?>[^\(\),"]|(?<p>\()|(?<-p>\))|(?(p)[^\(\)]|(?!))|(?(g)(?:""|[^"]|(?<-g>"))|(?!))|(?<g>")))*))+\)$

N'oubliez pas d'échapper aux barres obliques inverses et aux guillemets lorsque vous les collez dans votre code.

Il fera correspondre correctement les arguments entre guillemets, fonctions internes et nombres comme celui-ci: 
f1 (123, "df" "j" ", dhf", abc12, func2 (), func (123, a> 2))

La pile de paramètres contiendra
123
"df" "j" ", dhf"
abc12
func2 ()
func (123, a> 2)

1
alexandrecote99

Il existe de nouvelles améliorations (= relativement très nouvelles) spécifiques à la langue de regex qui permettent de faire correspondre les langues sans contexte avec "regex", mais vous trouverez plus de ressources et plus d'aide lorsque vous utilisez les outils plus souvent utilisé pour ce genre de tâche:

Il serait préférable d'utiliser un générateur d'analyseur tel que ANTLR, Lex + YACC, FLEX + BISON ou tout autre générateur de parseurs couramment utilisé . La plupart d'entre eux sont accompagnés d'exemples complets expliquant comment créer des calculatrices simples prenant en charge les appels de groupe et de fonction.

0
Mike Clark

Les expressions régulières ne vont pas vous dissiper complètement des ennuis avec ça ...

Puisque vous avez des parenthèses imbriquées, vous devez modifier votre code pour compter ( contre ). Lorsque vous rencontrez un (, vous devez prendre note de la position puis regarder en avant, incrémenter un compteur pour chaque extra ( que vous trouvez et le décrémenter pour chaque ) que vous trouvez. Lorsque votre compteur est à 0 et que vous trouvez un ), c'est la fin de votre bloc de paramètres de fonction. Vous pouvez ensuite analyser le texte entre les parenthèses. Vous pouvez également scinder le texte sur , lorsque le compteur est 0 pour obtenir les paramètres de la fonction.

Si vous rencontrez la fin de la chaîne alors que le compteur est à 0, vous avez une erreur "(" without ")".

Vous prenez ensuite le ou les blocs de texte entre les parenthèses ouvrante et fermante et les virgules, puis répétez la procédure ci-dessus pour chaque paramètre.

0
Monty Wild