使用正则表达式解析VBA常量声明

7
我正在尝试编写一个VBA解析器,为了创建一个ConstantNode,我需要能够匹配所有可能的Const声明变化。
这些工作得非常好:
  • Const foo = 123
  • Const foo$ = "123"
  • Const foo As String = "123"
  • Private Const foo = 123
  • Public Const foo As Integer = 123
  • Global Const foo% = 123
但我有两个问题:
  1. If there's a comment at the end of the declaration, I'm picking it up as part of the value:

    Const foo = 123 'this comment is included as part of the value
    
  2. If there's two or more constants declared in the same instruction, I'm failing to match the entire instruction:

    Const foo = 123, bar = 456 
    

这是我正在使用的正则表达式:

    /// <summary>
    /// Gets a regular expression pattern for matching a constant declaration.
    /// </summary>
    /// <remarks>
    /// Constants declared in class modules may only be <c>Private</c>.
    /// Constants declared at procedure scope cannot have an access modifier.
    /// </remarks>
    public static string GetConstantDeclarationSyntax()
    {
        return @"^((Private|Public|Global)\s)?Const\s(?<identifier>[a-zA-Z][a-zA-Z0-9_]*)(?<specifier>[%&@!#$])?(?<as>\sAs\s(?<reference>(((?<library>[a-zA-Z][a-zA-Z0-9_]*))\.)?(?<identifier>[a-zA-Z][a-zA-Z0-9_]*)))?\s\=\s(?<value>.*)$";
    }

显然,这两个问题都是由于(?<value>.*)$部分引起的,该部分匹配直到行尾的任何内容。我通过将整个模式括在一个捕获组中并添加一个可选逗号,使VariableNode支持一条指令中的多个声明,但由于常量具有此value组,因此这样做会导致第一个常量将所有后续声明都捕获为其值的一部分......这让我回到了问题#1。
我想知道是否可能使用正则表达式解决问题#1,考虑到值可能是包含撇号和可能包含转义(双倍)双引号的字符串。
我认为我可以在ConstantNode类本身中解决它,在Value的getter中:
/// <summary>
/// Gets the constant's value. Strings include delimiting quotes.
/// </summary>
public string Value
{
    get
    {
        return RegexMatch.Groups["value"].Value;
    }
}

我是说,我可以在这里实现一些附加逻辑,以执行正则表达式无法完成的任务。
如果问题#1可以使用正则表达式解决,那么我相信问题#2也可以...我在正确的轨道上吗?我应该放弃[相当复杂]的正则表达式模式,考虑另一种方法吗?我对贪婪子表达式、反向引用和其他更高级的正则表达式特性不太熟悉 - 这是限制我的因素,还是只是我用错了工具?
注:模式可能匹配非法语法 - 这段代码仅针对可编译的VBA代码运行。

5
你并没有使用错误的锤子敲钉子......因为你没有钉子,你有一个螺丝钉;-) - rolfl
@rolfl 那么...我现在就完蛋了? - Mathieu Guindon
我要冒昧地说,解析应该使用解析器来完成 ;) 解析VBA是一个有趣的想法...你可以添加支持宏预处理的功能,或者添加扩展语法以支持更简单的错误处理,甚至在VBA之上创建一种更简单的语言(就像coffeescript对javascript一样)! - Blackhawk
@Blackhawk 我非常愿意使用一个经过测试和已经工作的解决方案!你知道有没有现成可用的、免费/开源的 VBA 解析器,可以输出语法树供 C# 代码使用吗? - Mathieu Guindon
@retailcoder 我不知道 :( - Blackhawk
1个回答

3
让我先在这里加上免责声明。这绝对不是一个好主意(但它是一个有趣的挑战)。我即将呈现的正则表达式可以解析问题中的测试用例,但显然它们并不是万无一失的。使用解析器将会在以后节省很多麻烦。我尝试找到 VBA 的解析器,但没有找到(我假设其他人也没有)。
正则表达式
为了使其正常工作,您需要对进入的 VBA 代码具有一定的控制权。如果您不能这样做,那么您确实需要考虑编写解析器而不是使用正则表达式。但是,根据您已经说的话,您可能有一点控制权。所以也许这会有所帮助。
因此,我必须将正则表达式拆分为两个不同的正则表达式。原因是 .Net 正则表达式库无法处理重复组内的捕获组。
捕获行并开始解析,这将把变量(带值)放入单个组中,但第二个正则表达式将解析它们。只是提醒一下,这些正则表达式使用了负回顾后发断言。
^(?:(?<Accessibility>Private|Public|Global)\s)?Const\s(?<variable>[a-zA-Z][a-zA-Z0-9_]*(?:[%&@!#$])?(?:\sAs)?\s(?:(?:[a-zA-Z][a-zA-Z0-9_]*)\s)?=\s[^',]+(?:(?:(?!"").)+"")?(?:,\s)?){1,}(?:'(?<comment>.+))?$

这是解析变量的正则表达式,请查看正则表达式演示
(?<identifier>[a-zA-Z][a-zA-Z0-9_]*)(?<specifier>[%&@!#$])?(?:\sAs)?\s(?:(?<reference>[a-zA-Z][a-zA-Z0-9_]*)\s)?=\s(?<value>[^',]+(?:(?:(?!").)+")?),?

正则表达式演示

以下是一些C#代码,您可以将其插入并测试所有内容。这应该可以轻松测试任何边缘情况。

static void Main(string[] args)
{
    List<String> test = new List<string> {
        "Const foo = 123",
        "Const foo$ = \"123\"",
        "Const foo As String = \"1'2'3\"",
        "Const foo As String = \"123\"",
        "Private Const foo = 123",
        "Public Const foo As Integer = 123",
        "Global Const foo% = 123",
        "Const foo = 123 'this comment is included as part of the value",
        "Const foo = 123, bar = 456",
        "'Const foo As String = \"123\"",
    };


    foreach (var str in test)
        Parse(str);

    Console.Read();
}

private static Regex parse = new Regex(@"^(?:(?<Accessibility>Private|Public|Global)\s)?Const\s(?<variable>[a-zA-Z][a-zA-Z0-9_]*(?:[%&@!#$])?(?:\sAs)?\s(?:(?:[a-zA-Z][a-zA-Z0-9_]*)\s)?=\s[^',]+(?:(?:(?!"").)+"")?(?:,\s)?){1,}(?:'(?<comment>.+))?$", RegexOptions.Compiled | RegexOptions.Singleline, new TimeSpan(0, 0, 20));
private static Regex variableRegex = new Regex(@"(?<identifier>[a-zA-Z][a-zA-Z0-9_]*)(?<specifier>[%&@!#$])?(?:\sAs)?\s(?:(?<reference>[a-zA-Z][a-zA-Z0-9_]*)\s)?=\s(?<value>[^',]+(?:(?:(?!"").)+"")?),?", RegexOptions.Compiled | RegexOptions.Singleline, new TimeSpan(0, 0, 20));

public static void Parse(String str)
{
    Console.WriteLine(String.Format("Parsing: {0}", str));

    var match = parse.Match(str);

    if (match.Success)
    {
        //Private/Public/Global
        var accessibility = match.Groups["Accessibility"].Value;
        //Since we defined this with atleast one capture, there should always be something here.
        foreach (Capture variable in match.Groups["variable"].Captures)
        {
            //Console.WriteLine(variable);
            var variableMatch = variableRegex.Match(variable.Value);
            if (variableMatch.Success) 
            {
                Console.WriteLine(String.Format("Identifier: {0}", variableMatch.Groups["identifier"].Value));

                if (variableMatch.Groups["specifier"].Success)
                    Console.WriteLine(String.Format("specifier: {0}", variableMatch.Groups["specifier"].Value));

                if (variableMatch.Groups["reference"].Success)
                    Console.WriteLine(String.Format("reference: {0}", variableMatch.Groups["reference"].Value));

                Console.WriteLine(String.Format("value: {0}", variableMatch.Groups["value"].Value));

                Console.WriteLine("");
            }
            else
            {
                Console.WriteLine(String.Format("FAILED VARIABLE: {0}", variable.Value));
            }

        }

        if (match.Groups["comment"].Success)
        {
            Console.WriteLine(String.Format("Comment: {0}", match.Groups["comment"].Value));
        }
    }
    else
    {
        Console.WriteLine(String.Format("FAILED: {0}", str));
    }

    Console.WriteLine("+++++++++++++++++++++++++++++++++++++++++++++");
    Console.WriteLine("");
}
<为了完整起见,这里是一小部分输出样例。如果运行代码,您会得到更多输出,但这直接显示它可以处理您所问的情况。>
Parsing: Const foo = 123 'this comment is included as part of the value
Identifier: foo
value: 123
Comment: this comment is included as part of the value


Parsing: Const foo = 123, bar = 456
Identifier: foo
value: 123

Identifier: bar
value: 456

它可以处理什么

以下是我能想到的您可能感兴趣的主要情况。它应该仍然处理您先前提供的所有内容,因为我只是在提供的正则表达式中添加了一些内容。

  • 注释
  • 单行上的多个变量声明
  • 字符串值内的省略号(注释字符)。即 foo = "She's awesome"
  • 如果行以注释开头,则应忽略该行

它无法处理的内容

我没有真正处理的一件事是间距,但如果需要可以自己添加。因此,例如,如果声明多个变量,则逗号后必须有一个空格。即 (有效: foo = 123, foobar = 124) (无效: foo = 123,foobar = 124)

它不会对格式给予很多宽容度,但在使用正则表达式时不太能做到这一点。


希望这可以帮助到您,如果您需要更多关于如何运作其中任何内容的解释,请告诉我。 只是要知道这是个坏主意。您将遇到正则表达式无法处理的情况。如果我处于您的位置,我将考虑编写一个简单的解析器,这将在长期内为您提供更大的灵活性。祝好运。


哇,这是一些很棒的正则表达式技巧!空格不是什么问题,因为对于 VBA IDE 自动添加/标准化它们,所以逗号后总会有一个空格。我目前有一个混合正则表达式和“普通代码”解决方案来解决这个问题,但我肯定会尝试一下那个正则表达式,并希望在此过程中学到一些东西。只有一个[愚蠢]问题,如果可以的话,不是可能使用正则表达式实现[部分]解析器吗?它们似乎适用于解析任何常规语法,不是吗? - Mathieu Guindon
1
@retailcoder:是的,你可以在解析器中使用正则表达式。但正则表达式不能独立存在。你可以解析语言的流动部分,然后使用正则表达式完成结构化组件。当采用这种方法时,显然有一些需要考虑的问题,例如可维护性或代码优化,但我认为这是值得考虑的方法。只要小心不要让正则表达式做太多的事情,这是一个容易陷入的陷阱。 - Nathan
谢谢!FYI,当我让解析器按照我的意图工作时,我会将其放在[codereview.se]上。 - Mathieu Guindon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接