【问题标题】:Parsing VBA Const declarations... with regex使用正则表达式解析 VBA 常量声明...
【发布时间】:2014-11-07 06:43:50
【问题描述】:

我正在尝试编写 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. 如果声明末尾有注释,我会将其作为价值的一部分:

    Const foo = 123 'this comment is included as part of the value
    
  2. 如果在同一条指令中声明了两个或多个常量,我将无法匹配整个指令:

    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>.*)$";
    }

显然这两个问题都是由 (?&lt;value&gt;.*)$ 部分引起的,它匹配 anything 直到行尾。我得到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 也可以解决......还是我在正确的轨道上?我应该放弃[相当复杂的]正则表达式模式并考虑另一种方式吗?我对 greedy subexpressionsbackreferences 和其他更高级的正则表达式功能不太熟悉 - 这是否限制了我,或者只是我使用了错误的锤子这个钉子?

注意:模式可能与非法语法匹配并不重要 - 此代码只会针对可编译的 VBA 代码运行。

【问题讨论】:

  • 你没有用错钉子的锤子.....你没有钉子,你有螺丝;-)
  • @rolfl 所以...那我搞砸了
  • 我要在这里冒个险,说解析可能应该用解析器来完成;) 解析 VBA 是一个有趣的想法……您可以添加对宏等预处理的支持或添加语法扩展以支持更简单的错误处理,甚至在 VBA 之上创建更简单的语言(就像咖啡脚本之于 javascript)!
  • @Blackhawk 我非常愿意使用经过测试且已经有效的解决方案!您是否知道任何易于使用、免费/开源的 VBA 解析器,可以生成 C# 代码可以使用的语法树?
  • @retailcoder 我没有 :(

标签: c# regex parsing


【解决方案1】:

让我继续添加免责声明。这绝对不是一个好主意(但这是一个有趣的挑战)。我将要介绍的正则表达式将解析问题中的测试用例,但它们显然不是防弹的。使用解析器将在以后为您省去很多麻烦。我确实尝试为 VBA 找到解析器,但空手而归(我假设其他人也有)。

正则表达式

要使其正常工作,您需要对传入的 VBA 代码进行一些控制。如果您无法做到这一点,那么您确实需要考虑编写解析器而不是使用正则表达式。但是,从你已经说过的情况来看,你可能有一点控制力。所以也许这会有所帮助。

为此,我不得不将正则表达式拆分为两个不同的正则表达式。原因是 .Net 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>.+))?$

Regex Demo

这是解析变量的正则表达式

(?<identifier>[a-zA-Z][a-zA-Z0-9_]*)(?<specifier>[%&@!#$])?(?:\sAs)?\s(?:(?<reference>[a-zA-Z][a-zA-Z0-9_]*)\s)?=\s(?<value>[^',]+(?:(?:(?!").)+")?),?

Regex Demo

这里有一些 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("");
}

c# 代码正是我用来测试我的理论的,所以我为它的疯狂道歉。

为了完整起见,这里是输出的一个小样本。如果你运行代码你会得到更多的输出,但这直接表明它可以处理你所询问的情况。

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

它处理什么

以下是我能想到的您可能感兴趣的主要案例。它应该仍然可以处理您之前拥有的所有内容,因为我刚刚添加到您提供的正则表达式中。

  • 评论
  • 在一行中声明多个变量
  • 字符串值中的撇号(注释字符)。 ie foo = "她太棒了"
  • 如果该行以注释开头,则应忽略该行

它不能处理什么

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

你不会对它的格式有太大的宽容,但在使用正则表达式时你可以做的不多。


希望这对您有所帮助,如果您需要更多关于其中任何工作原理的解释,请告诉我。 知道这是个坏主意。您将遇到正则表达式无法处理的情况。如果我处于你的位置,我会考虑编写一个简单的解析器,从长远来看会给你更大的灵活性。祝你好运。

【讨论】:

  • 哇,这是一些很棒的正则表达式!间距不是什么大问题,因为 VBA IDE 会自动添加/标准化它们,所以逗号后总会有一个空格。我目前有一个混合正则表达式和“正常代码”的解决方案来解决这个问题,但我肯定会尝试这个正则表达式,并希望在此过程中学到一些东西。如果可以的话,只是一个[愚蠢的]问题,是否可以使用正则表达式实现解析器的[部分]?它们似乎适合我,用于解析任何常规语法,不是吗?
  • @retailcoder:是的,您可以在解析器中使用正则表达式。正则表达式无法独立存在。您可以解析语言的流畅部分,然后使用正则表达式来完成语言的结构化组件。在使用这种方法时,显然有一些事情需要考虑,例如可维护性或代码优化,但我认为这是一种值得关注的方法。小心不要让正则表达式做太多事情,这很容易上当。
  • 谢谢!仅供参考,当我让它做我想做的事情时,我会把解析器放在Code Review上;)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-08
  • 2012-08-23
  • 2014-06-08
相关资源
最近更新 更多