在 .NET 中 使用 ANTLR4構(gòu)建語(yǔ)法分析器的方法
前言
本文將介紹如何在 .NET 中使用 ANTLR4 構(gòu)建語(yǔ)法分析器。由于篇幅限制,本文不會(huì)深入講解 ANTLR4 的語(yǔ)法規(guī)則,相關(guān)內(nèi)容可參考 ANTLR4 的官方文檔或其他資料。本文將涵蓋以下內(nèi)容:ANTLR4 的開發(fā)環(huán)境搭建、語(yǔ)法規(guī)則編寫、語(yǔ)法分析器生成以及語(yǔ)法分析器的使用。
本文中的例子相對(duì)簡(jiǎn)單,且未經(jīng)過詳細(xì)測(cè)試,旨在演示 ANTLR4 的基本用法。
實(shí)際開發(fā)的過程中,建議先去官方的這個(gè) repo 查看是否已經(jīng)有現(xiàn)成的 grammar 文件可以使用:https://github.com/antlr/grammars-v4
文中的代碼示例已上傳到 GitHub:
https://github.com/eventhorizon-cli/Antlr4Demo
ANTLR4 簡(jiǎn)介
ANTLR(Another Tool for Language Recognition)是一個(gè)強(qiáng)大的語(yǔ)法分析器生成器,屬于編譯技術(shù)中的前端工具。它可以用來(lái)構(gòu)建語(yǔ)法分析器,并借此開發(fā)編譯器、解釋器和翻譯器等。
ANTLR4 是 ANTLR 的最新版本,它支持多種編程語(yǔ)言的語(yǔ)法分析器生成,包括 Java、C#、Python、JavaScript 等。ANTLR4 的語(yǔ)法規(guī)則使用一種類似于正則表達(dá)式的語(yǔ)法來(lái)定義,可以很方便地描述復(fù)雜的語(yǔ)法結(jié)構(gòu)。
ANTLR4 的工作流程如下:
- 編寫語(yǔ)法規(guī)則:通常使用 ANTLR4 的語(yǔ)法規(guī)則文件(.g4 文件)來(lái)定義語(yǔ)法規(guī)則。
- 生成語(yǔ)法分析器:使用 ANTLR4 工具來(lái)生成目標(biāo)語(yǔ)言的語(yǔ)法分析器。
- 使用語(yǔ)法分析器進(jìn)行語(yǔ)法分析:編寫代碼來(lái)使用生成的語(yǔ)法分析器進(jìn)行語(yǔ)法分析。分析的結(jié)果通常是一個(gè)抽象語(yǔ)法樹(AST)。
- 訪問 AST:可以使用訪問者模式(Visitor Pattern)或者監(jiān)聽器模式(Listener Pattern)來(lái)訪問 AST,進(jìn)行后續(xù)的處理,例如解釋執(zhí)行、編譯等。
語(yǔ)法分析基本概念
語(yǔ)法分析的過程分為兩個(gè)階段:詞法分析(Lexical Analysis)和語(yǔ)法分析(Syntax Analysis)。
詞法分析:將字符聚集為單詞或者符號(hào)(token),例如將
1 + 2
分解為1
、+
、2
三個(gè) token。語(yǔ)法分析:輸入的 token 被組織成一個(gè)樹形結(jié)構(gòu),稱為抽象語(yǔ)法樹(Abstract Syntax Tree,AST),它表示了輸入的語(yǔ)法結(jié)構(gòu)。樹的每個(gè)節(jié)點(diǎn)表示一個(gè)語(yǔ)法單元,這個(gè)單元的構(gòu)成規(guī)則就叫做語(yǔ)法規(guī)則。每個(gè)節(jié)點(diǎn)還可以有子節(jié)點(diǎn)。
例如,表達(dá)式 1 + 2 * 3
的抽象語(yǔ)法樹如下:
+ / \ 1 * / \ 2 3
如何使用 ANTLR4
1. 安裝 Antlr4.Runtime.Standard 包
我們以加減乘除四則運(yùn)算為例來(lái)介紹如何使用 ANTLR4 來(lái)構(gòu)建語(yǔ)法分析器。
新建一個(gè)C#項(xiàng)目,在項(xiàng)目中添加 Antlr4.Runtime.Standard
包。
dotnet add package Antlr4.Runtime.Standard
2. 編寫 ANTLR4 的語(yǔ)法規(guī)則文件
接著我們需要編寫一個(gè) ANTLR4 的語(yǔ)法規(guī)則文件,文件的后綴名為 .g4,例如 Arithmetic.g4,文件的內(nèi)容如下:
grammar Arithmetic; // grammar name 需要和文件名一致 // 語(yǔ)法規(guī)則 // op=('*'|'/') 表示 op 將 ‘*' 或者 ‘/' 標(biāo)記為一個(gè)操作符號(hào) // # MulDiv 將這個(gè)規(guī)則命名為 MulDiv,訪問 AST 時(shí)會(huì)用到 expr: expr op=('*'|'/') expr # MulDiv | expr op=('+'|'-') expr # AddSub | INT # Int | '(' expr ')' # Parens ; // 詞法規(guī)則 INT : [0-9]+ ; WS : [ \t\r\n]+ -> skip ; // 表示忽略空格
g4 文件的內(nèi)容分為兩部分:詞法規(guī)則(Lexer Rules) 和 語(yǔ)法規(guī)則(Parser Rules)。
詞法規(guī)則是用來(lái)定義詞法單元的,例如數(shù)字、運(yùn)算符、括號(hào)等。詞法規(guī)則通常以大寫字母開頭。
語(yǔ)法規(guī)則是用來(lái)定義語(yǔ)法結(jié)構(gòu)的,例如表達(dá)式、語(yǔ)句等。語(yǔ)法規(guī)則通常以小寫字母開頭。
在上面的例子中,我們定義了一個(gè)簡(jiǎn)單的四則運(yùn)算語(yǔ)法規(guī)則,支持加減乘除和括號(hào)運(yùn)算。我們還定義了一個(gè)整數(shù)類型的詞法規(guī)則 INT
,表示一個(gè)或多個(gè)數(shù)字。
expr 規(guī)則表示一個(gè)表達(dá)式,用 | 分隔的部分表示或的關(guān)系,例如 expr op=('*'|'/') expr | expr op=('+'|'-') expr
表示一個(gè)表達(dá)式可以是乘法或除法,也可以是加法或減法。
而加減乘除的優(yōu)先級(jí)通過定義的順序來(lái)決定,乘除法的規(guī)則在加減法之前,所以乘除法的優(yōu)先級(jí)高于加減法。
在語(yǔ)法規(guī)則中,我們還可以使用 #
來(lái)為規(guī)則命名,例如 # MulDiv
,表示這個(gè)規(guī)則的名字是 MulDiv
。這個(gè)名字在訪問 AST 時(shí)會(huì)用到。
規(guī)則支持遞歸定義,例如 expr: expr op=('*'|'/') expr
。
這邊因?yàn)榕e的例子比較簡(jiǎn)單,可以直接在一個(gè) g4 文件中同時(shí)定義語(yǔ)法規(guī)則和詞法規(guī)則。對(duì)于復(fù)雜的語(yǔ)法規(guī)則,可以將語(yǔ)法規(guī)則和詞法規(guī)則分開定義。
在 Rider 或 VS Code 中安裝 ANTLR4 的插件,可以檢查語(yǔ)法規(guī)則的正確性。
在 Rider 中安裝 ANTLR4 的插件后,可以在 g4 文件選中 expr 規(guī)則,右鍵選擇 Test Rule expr
來(lái)測(cè)試語(yǔ)法規(guī)則是否正確。
左側(cè)的輸入框中輸入要測(cè)試的表達(dá)式,右側(cè)的輸出框中會(huì)以樹形結(jié)構(gòu)的方式顯示語(yǔ)法分析的結(jié)果。
3. 生成語(yǔ)法分析器
ANTLR4 是基于 Java 開發(fā)的,所以我們需要安裝 Java 運(yùn)行環(huán)境才能使用 ANTLR4 工具來(lái)生成語(yǔ)法分析器。
我們有兩種方式來(lái)使用 ANTLR4 生成語(yǔ)法分析器,優(yōu)先推薦使用 Antlr4BuildTasks
項(xiàng)目來(lái)自動(dòng)生成語(yǔ)法分析器。
直接使用 ANTLR4 官方提供的工具來(lái)生成語(yǔ)法分析器。
首先,我們需要下載 ANTLR4 工具,可以從 ANTLR4 的官方網(wǎng)站下載:https://www.antlr.org/download.html
寫本文時(shí),最新的版本是 4.13.2,下載地址為:
https://www.antlr.org/download/antlr-4.13.2-complete.jar
本文為方便演示,將 antlr-4.13.2-complete.jar 下載到 g4 文件所在的目錄下。
接著就可以使用 Java 運(yùn)行 ANTLR4 工具來(lái)生成語(yǔ)法分析器。
java -jar antlr-4.13.2-complete.jar -Dlanguage=CSharp Arithmetic.g4
其中,Arithmetic.g4 是我們編寫的語(yǔ)法規(guī)則文件,-Dlanguage=CSharp 表示生成 C# 語(yǔ)言的語(yǔ)法分析器。
執(zhí)行上面的命令后,會(huì)生成一些文件,其中包括 ArithmeticLexer.cs
、ArithmeticParser.cs
。
后面我們就可以使用生成的語(yǔ)法分析器來(lái)進(jìn)行語(yǔ)法分析了。
借助 Antlr4BuildTasks 項(xiàng)目自動(dòng)生成語(yǔ)法分析器。
上面的方式需要手動(dòng)下載 ANTLR4 工具,然后使用 Java 運(yùn)行 ANTLR4 工具來(lái)生成語(yǔ)法分析器,還會(huì)生成一些必須需要添加到項(xiàng)目中的文件。這樣的方式比較繁瑣,我們可以使用 Antlr4BuildTasks
項(xiàng)目來(lái)自動(dòng)生成語(yǔ)法分析器。
Antlr4BuildTasks
的 GitHub 地址為:
https://github.com/kaby76/Antlr4BuildTasks
Antlr4BuildTasks
是一個(gè) MSBuild 任務(wù),它可以自動(dòng)下載 ANTLR4 工具,然后使用 ANTLR4 工具來(lái)生成語(yǔ)法分析器,最后將生成的語(yǔ)法分析器添加到項(xiàng)目中。它也會(huì)嘗試下載 java 運(yùn)行環(huán)境,如果 build 過程中出現(xiàn)錯(cuò)誤,可以嘗試手動(dòng)安裝全局的 java 運(yùn)行環(huán)境。
除了安裝 Antlr4BuildTasks 的包之外,我們還需要在項(xiàng)目文件(.csproj)中添加一些配置,完整 .csproj 文件如下:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1"/> <PackageReference Include="Antlr4BuildTasks" Version="12.8.0" PrivateAssets="all"/> </ItemGroup> <ItemGroup> <Antlr4 Include="**\*.g4"/> </ItemGroup> </Project>
<Antlr4 Include="**\*.g4"/>
表示將項(xiàng)目中所有的 .g4 文件都添加到 Antlr4 任務(wù)中。當(dāng)然也可以指定具體的 .g4 文件路徑。
在 build 項(xiàng)目時(shí),Antlr4BuildTasks 會(huì)將 .g4 文件編譯成的文件放在 obj 文件夾下,我們可以在 obj 文件夾下找到生成的語(yǔ)法分析器。
obj 文件夾下的文件是臨時(shí)文件,會(huì)在每次 build 時(shí)重新生成,我們不需要將 obj 文件夾下的文件添加到項(xiàng)目中。
4. 編寫代碼來(lái)使用語(yǔ)法分析器
接下來(lái)我們就可以編寫代碼來(lái)使用生成的語(yǔ)法分析器了。
訪問 AST 的方式有兩種:Visitor和 Listener。我們可以選擇其中一種方式來(lái)訪問 AST。
ANTLR4 會(huì)為我們生成一個(gè) Parser,Parser 在遍歷 AST 時(shí)會(huì)調(diào)用 Visitor 的 VisitXXX
方法,或者 Listener 的 EnterXXX
和 ExitXXX
方法。
使用 Visitor 實(shí)現(xiàn)
下面我們以訪問者模式為例,編寫一個(gè)簡(jiǎn)單的 C# 程序來(lái)使用語(yǔ)法分析器。
ANTLR4 會(huì)為我們生成一個(gè) ArithmeticBaseVisitor
類,我們可以繼承這個(gè)類來(lái)完成對(duì) AST 的訪問。
在前面的 g4 文件中,我們?yōu)槊總€(gè) AST 節(jié)點(diǎn)定義了一個(gè)名字, MulDiv
、AddSub
、Int
、Parens
這些,對(duì)應(yīng) ArithmeticBaseVisitor
中的 VisitMulDiv
、VisitAddSub
、VisitInt
、VisitParens
方法。
我們可以通過重寫這些方法來(lái)實(shí)現(xiàn)對(duì) AST 的訪問:
public class ArithmeticVisitor : ArithmeticBaseVisitor<int> { // 解析乘除法 public override int VisitMulDiv(ArithmeticParser.MulDivContext context) { // context 包含了當(dāng)前節(jié)點(diǎn)的信息 // context.expr(0) 和 context.expr(1) 分別表示乘除法的兩個(gè)操作數(shù) // 訪問子節(jié)點(diǎn),獲取操作數(shù)的值 int left = Visit(context.expr(0)); int right = Visit(context.expr(1)); return context.op.Text switch { "*" => left * right, "/" => left / right, _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.") }; } // 解析加減法 public override int VisitAddSub(ArithmeticParser.AddSubContext context) { int left = Visit(context.expr(0)); int right = Visit(context.expr(1)); return context.op.Text switch { "+" => left + right, "-" => left - right, _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.") }; } // 去掉括號(hào),訪問括號(hào)內(nèi)的表達(dá)式 public override int VisitParens(ArithmeticParser.ParensContext context) => Visit(context.expr()); // 解析整數(shù) public override int VisitInt(ArithmeticParser.IntContext context) => int.Parse(context.INT().GetText()); }
定義好了 visitor 之后,我們就可以使用它來(lái)解析表達(dá)式了。
Console.WriteLine(Evaluate("1 + 2 * 3")); // 7 Console.WriteLine(Evaluate("(1 + 2) * 3")); // 9 int Evaluate(string expression) { // 創(chuàng)建詞法分析器 var lexer = new ArithmeticLexer(new AntlrInputStream(expression)); var tokens = new CommonTokenStream(lexer); // 創(chuàng)建語(yǔ)法分析器,傳入詞法分析器的輸出的token流 var parser = new ArithmeticParser(tokens); // 用 visitor 模式解析表達(dá)式 var visitor = new ArithmeticVisitor(); return visitor.Visit(parser.expr()); }
使用 Listener 實(shí)現(xiàn)
ANTLR4 的 Parser 在遍歷 AST 時(shí)會(huì)調(diào)用 Listener 的 EnterXXX
和 ExitXXX
方法,我們可以通過重寫這些方法來(lái)實(shí)現(xiàn)對(duì) AST 的訪問。
EnterXXX
方法在進(jìn)入節(jié)點(diǎn)時(shí)調(diào)用,ExitXXX
方法在離開節(jié)點(diǎn)時(shí)調(diào)用。
我們可以在 ExitXXX
方法里將操作數(shù)壓入棧中,下次訪問時(shí)就可以從棧中彈出操作數(shù)進(jìn)行計(jì)算。
public class ArithmeticListener : ArithmeticBaseListener { // 使用棧來(lái)存儲(chǔ)操作數(shù) private readonly Stack<int> _stack = new(); public int Result => _stack.Pop(); public override void ExitMulDiv(ArithmeticParser.MulDivContext context) { int right = _stack.Pop(); int left = _stack.Pop(); int result = context.op.Text switch { "*" => left * right, "/" => left / right, _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.") }; _stack.Push(result); } public override void ExitAddSub(ArithmeticParser.AddSubContext context) { int right = _stack.Pop(); int left = _stack.Pop(); int result = context.op.Text switch { "+" => left + right, "-" => left - right, _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.") }; _stack.Push(result); } public override void ExitParens(ArithmeticParser.ParensContext context) { // ExitParens 方法在這里不需要做任何操作,因?yàn)槲覀円呀?jīng)在 MulDiv 和 AddSub 中處理了括號(hào)內(nèi)的表達(dá)式 } public override void ExitInt(ArithmeticParser.IntContext context) { int value = int.Parse(context.INT().GetText()); _stack.Push(value); } }
Console.WriteLine(Evaluate("1 + 2 * 3")); // 7 Console.WriteLine(Evaluate("(1 + 2) * 3")); // 9 int Evaluate(string expression) { // 創(chuàng)建詞法分析器 var lexer = new ArithmeticLexer(new AntlrInputStream(expression)); var tokens = new CommonTokenStream(lexer); // 創(chuàng)建語(yǔ)法分析器,傳入詞法分析器的輸出的token流 var parser = new ArithmeticParser(tokens); var listener = new ArithmeticListener(); // 解析表達(dá)式 parser.AddParseListener(listener); parser.expr(); // 獲取結(jié)果 return listener.Result; }
構(gòu)建自定義 AST 以解決復(fù)雜問題
上面的例子中,我們?cè)诒闅v AST 時(shí)直接計(jì)算了表達(dá)式的值,這種方式在簡(jiǎn)單的表達(dá)式中是可以的,但如果表達(dá)式的處理邏輯比較復(fù)雜,更建議將 原始AST 轉(zhuǎn)換成一個(gè)我們自定義的 AST,然后在后續(xù)的處理邏輯中使用這個(gè)自定義的 AST,將解析和處理邏輯分開,可以讓代碼更清晰,功能也容易實(shí)現(xiàn)。
下面我們定義一個(gè)比加減乘除法更復(fù)雜的需求:指定一個(gè)文件夾,用 sql 語(yǔ)句來(lái)查詢文件夾下的 csv 文件,支持過濾條件、排序等操作。表名是文件名,字段名是 csv 文件的列名。
為簡(jiǎn)化起見,我們只支持簡(jiǎn)單的查詢語(yǔ)句,支持 SELECT
、FROM
、WHERE
、ORDER BY
等關(guān)鍵字。數(shù)據(jù)類型僅用字符串類型做示范,支持的過濾方式有 =
、!=
、LIKE
,過濾條件之間只能用 AND
連接,排序方式支持 ASC
和 DESC
。
這里我們將詞法規(guī)則和語(yǔ)法規(guī)則分開定義,詞法規(guī)則定義在 SqlLexer.g4
文件中,語(yǔ)法規(guī)則定義在 SqlParser.g4
文件中。
SqlLexer.g4
文件的內(nèi)容如下:
lexer grammar SqlLexer; options { caseInsensitive = true; // 忽略大小寫 } // 關(guān)鍵字 SELECT : 'SELECT' ; FROM : 'FROM' ; WHERE : 'WHERE' ; ORDER : 'ORDER' ; BY : 'BY' ; ASC : 'ASC' ; DESC : 'DESC' ; AND : 'AND' ; OR : 'OR' ; COMMA : ',' ; STAR : '*' ; // 運(yùn)算符 EQ : '=' ; NEQ : '!=' ; LIKE : 'LIKE' ; // 字面量 STRING_LITERAL : '\'' ( ~('\'' | '\\') | '\\' . )* '\'' // 字符串字面量 ; // 標(biāo)識(shí)符 IDENTIFIER : [a-z_][a-z0-9_]* // 用于表名、列名等 ; WS : [ \t\r\n]+ -> skip ; // 忽略空格
SqlParser.g4
文件的內(nèi)容如下:
parser grammar SqlParser; options { tokenVocab=SqlLexer; } query : SELECT selectList FROM tableName (WHERE whereClause)? (ORDER BY orderByClause)? ; selectList : columnName (COMMA columnName)* | STAR ; columnName: IDENTIFIER ; tableName: IDENTIFIER ; whereClause : whereCondition (AND whereCondition)* ; whereCondition : columnName op=(EQ | NEQ) STRING_LITERAL | columnName op=LIKE STRING_LITERAL ; orderByClause : orderByCondition (COMMA orderByCondition)* ; orderByCondition : columnName (ASC | DESC)? ;
定義完后可以在 Rider 中使用 ANTLR4 的插件來(lái)檢查語(yǔ)法規(guī)則的正確性,選中 query
規(guī)則,右鍵選擇 Test Rule query
來(lái)測(cè)試語(yǔ)法規(guī)則是否正確。
下面我們先定義一組類型用來(lái)表示 SQL 語(yǔ)句的 AST:
public abstract class Expression; public class QueryExpression : Expression { public required string TableName { get; init; } public required bool SelectAll { get; init; } public required IEnumerable<string> SelectList { get; init; } public required IEnumerable<WhereCondition> WhereConditions { get; init; } public required IEnumerable<OrderByCondition> OrderByConditions { get; init; } } public class WhereCondition : Expression { public required string ColumnName { get; init; } public WhereConditionOperator Operator { get; init; } public required string Value { get; init; } } public enum WhereConditionOperator { Equal, NotEqual, StartsWith, EndsWith, Contains } public class OrderByCondition : Expression { public required string ColumnName { get; init; } public bool IsDescending { get; init; } }
我們定義一個(gè) SqlAstBuilder
類來(lái)實(shí)現(xiàn)對(duì) SQL 語(yǔ)句的解析:
public class SqlAstBuilder : SqlParserBaseVisitor<QueryExpression> { public override QueryExpression VisitQuery(SqlParser.QueryContext context) { var selectList = context.selectList(); bool selectAll = selectList?.STAR() != null; var columns = selectList?.columnName() .Select(c => c.GetText()) .ToList() ?? []; var tableName = context.tableName().GetText(); var whereConditions = context.whereClause() ?.whereCondition() .Select(c => { var stringValue = c.STRING_LITERAL().GetText().Trim('\''); var opText = c.op.Text.ToUpperInvariant(); var op = WhereConditionOperator.Equal; if (opText == "=") { op = WhereConditionOperator.Equal; } else if (opText == "!=") { op = WhereConditionOperator.NotEqual; } else if (opText == "LIKE") { if (stringValue.StartsWith("%") && stringValue.EndsWith("%")) { op = WhereConditionOperator.Contains; stringValue = stringValue.Substring(1, stringValue.Length - 2); } else if (stringValue.StartsWith("%")) { op = WhereConditionOperator.EndsWith; stringValue = stringValue.Substring(1); } else if (stringValue.EndsWith("%")) { op = WhereConditionOperator.StartsWith; stringValue = stringValue.Substring(0, stringValue.Length - 1); } } else { throw new NotSupportedException($"Operator {c.op.Text} is not supported."); } return new WhereCondition { ColumnName = c.columnName().GetText(), Operator = op, Value = stringValue }; }) .ToList() ?? []; var orderByConditions = context.orderByClause() ?.orderByCondition() .Select(c => new OrderByCondition { ColumnName = c.columnName().GetText(), IsDescending = c.DESC() != null }) .ToList() ?? []; return new QueryExpression { SelectAll = selectAll, SelectList = columns, TableName = tableName, WhereConditions = whereConditions, OrderByConditions = orderByConditions }; } }
SqlToCsvEngine
類用來(lái)執(zhí)行 SQL 語(yǔ)句并從 CSV 文件中讀取數(shù)據(jù):
public class SqlToCsvEngine(DirectoryInfo csvDirectory) { public IEnumerable<Dictionary<string, string>> ExecuteQuery(string query) { // 創(chuàng)建詞法分析器 var lexer = new SqlLexer(new AntlrInputStream(query)); // 創(chuàng)建語(yǔ)法分析器,傳入詞法分析器的輸出的token流 var tokens = new CommonTokenStream(lexer); var parser = new SqlParser(tokens); // 將查詢語(yǔ)句解析為自定義的 AST var astBuilder = new SqlAstBuilder(); var expression = astBuilder.Visit(parser.query()); // 處理 AST,執(zhí)行查詢 if (expression is not { } queryExpression) { throw new InvalidOperationException("Expected a query expression"); } // 讀取 CSV 文件 var csvData = ReadCsv(queryExpression.TableName); // 過濾數(shù)據(jù) var filteredData = csvData.Where(row => { // 處理 WHERE 條件 // 處理 WHERE 條件 bool isMatch = true; foreach (var condition in queryExpression.WhereConditions) { if (row.TryGetValue(condition.ColumnName, out var value)) { isMatch = condition.Operator switch { WhereConditionOperator.Equal => value == condition.Value, WhereConditionOperator.NotEqual => value != condition.Value, WhereConditionOperator.StartsWith => value.StartsWith(condition.Value), WhereConditionOperator.EndsWith => value.EndsWith(condition.Value), WhereConditionOperator.Contains => value.Contains(condition.Value), _ => throw new ArgumentOutOfRangeException() }; } else { throw new InvalidOperationException($"Column {condition.ColumnName} does not exist in CSV file."); } } return isMatch; }); // 處理 ORDER BY 條件 foreach (var orderByCondition in queryExpression.OrderByConditions) { Func<IEnumerable<Dictionary<string, string>>, Func<Dictionary<string, string>, string>, IEnumerable<Dictionary<string, string>>> orderByFunc; if (filteredData is IOrderedEnumerable<Dictionary<string, string>> orderedData) { orderByFunc = orderByCondition.IsDescending ? (_, keySelector) => orderedData.ThenByDescending(keySelector) : (_, keySelector) => orderedData.ThenBy(keySelector); } else { orderByFunc = orderByCondition.IsDescending ? Enumerable.OrderByDescending : Enumerable.OrderBy; } filteredData = orderByFunc(filteredData, row => { if (row.TryGetValue(orderByCondition.ColumnName, out var value)) { return value; } throw new InvalidOperationException( $"Order by column {orderByCondition.ColumnName} does not exist in CSV file."); }); } // 處理 SELECT 條件 if (queryExpression.SelectAll) { return filteredData; } var selectedData = filteredData.Select(row => { var selectedRow = new Dictionary<string, string>(); foreach (var columnName in queryExpression.SelectList) { if (row.TryGetValue(columnName, out var value)) { selectedRow[columnName] = value; } else { throw new InvalidOperationException($"Column {columnName} does not exist in CSV file."); } } return selectedRow; }); return selectedData; } private IEnumerable<Dictionary<string, string>> ReadCsv(string tableName) { var csvFile = new FileInfo(Path.Combine(csvDirectory.FullName, $"{tableName}.csv")); if (!csvFile.Exists) { throw new FileNotFoundException($"CSV file {csvFile.FullName} does not exist."); } using var reader = new StreamReader(csvFile.FullName); var headerLine = reader.ReadLine(); if (headerLine == null) { throw new InvalidOperationException($"CSV file {csvFile.FullName} is empty."); } var headers = headerLine.Split(','); while (!reader.EndOfStream) { var line = reader.ReadLine(); if (line == null) continue; var values = line.Split(','); yield return headers.Zip(values).ToDictionary(x => x.First, x => x.Second); } } }
接下來(lái)我們就可以開始測(cè)試了:
測(cè)試用的 CSV 文件內(nèi)容如下:
Name,City,Occupation,Company Alice,New York,Engineer,TechCorp Bob,Los Angeles,Designer,Creative Inc Ben,Atlanta,Writer,Publishing House Charlie,Chicago,Manager,Finance Group David,Houston,Teacher,School District Eve,Miami,Student,University Frank,Seattle,Chef,Restaurant Co Grace,San Francisco,Doctor,HealthCare Hannah,Boston,Lawyer,Legal Partners Ian,Denver,Architect,BuildIt
var directory = new DirectoryInfo("/Users/hkh/Desktop/test"); var engine = new SqlToCsvEngine(directory); var sql = """ SELECT Name, City, Occupation, Company FROM Employee WHERE City != 'Miami' AND Occupation LIKE '%er' ORDER BY Name ASC, Company DESC """; var result = engine.ExecuteQuery(sql); // 打印頭部 foreach (var column in result.First().Keys) { Console.Write($"{column}\t"); } foreach (var row in result) { Console.WriteLine(); foreach (var (_, value) in row) { Console.Write($"{value}\t"); } }
輸出結(jié)果如下:
Name City Occupation Company
Alice New York Engineer TechCorp
Ben Atlanta Writer Publishing House
Bob Los Angeles Designer Creative Inc
Charlie Chicago Manager Finance Group
David Houston Teacher School District
Hannah Boston Lawyer Legal Partners
參考資料
https://github.com/antlr/grammars-v4
https://wizardforcel.gitbooks.io/antlr4-short-course/content/basic-concept.html
https://github.com/antlr/antlr4/blob/master/doc/csharp-target.md
到此這篇關(guān)于如何在 .NET 中 使用 ANTLR4的文章就介紹到這了,更多相關(guān).NET 使用 ANTLR4內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
用C#中的params關(guān)鍵字實(shí)現(xiàn)方法形參個(gè)數(shù)可變
個(gè)人認(rèn)為,提供params關(guān)鍵字以實(shí)現(xiàn)方法形參個(gè)數(shù)可變是C#語(yǔ)法的一大優(yōu)點(diǎn)。在方法形參列表中,數(shù)組類型的參數(shù)前加params關(guān)鍵字,通??梢栽谡{(diào)用方法時(shí)代碼更加精練2012-01-01ASP.NET筆記之 Request 、Response 與Server的使用
本篇文章小編為大家介紹,ASP.NET筆記之 Request 、Response 與Server的使用。需要的朋友參考下2013-04-04ASP.NET下母版頁(yè)和內(nèi)容頁(yè)中的事件發(fā)生順序整理
母版頁(yè)與內(nèi)容頁(yè)合并后事件的發(fā)生順序,有需要區(qū)別的朋友能用的到2009-03-03在asp.net下實(shí)現(xiàn)Option條目中填充前導(dǎo)空格的方法
在asp.net下實(shí)現(xiàn)Option條目中填充前導(dǎo)空格的方法...2007-03-03asp.net安全、實(shí)用、簡(jiǎn)單的大容量存儲(chǔ)過程分頁(yè)
昨晚研究到2點(diǎn)多,對(duì)網(wǎng)絡(luò)上主流的分頁(yè)存儲(chǔ)過程大體看了一遍,但對(duì)安全以及如何使用很多文章都沒有過多的提及,而我要在這些文章的基礎(chǔ)上總結(jié)出一個(gè)比較實(shí)用的分頁(yè)存儲(chǔ)過程,方便大家在以后的項(xiàng)目中使用。2009-06-06java selenium智能等待頁(yè)面加載完成示例代碼
本文主要介紹java selenium智能等待頁(yè)面加載,這里整理了相關(guān)資料并詳細(xì)講解如何實(shí)現(xiàn)智能等待頁(yè)面加載,有需要的小伙伴可以參考下2016-08-08