定制嵌入式语法
我们可以在语法规则的定义中,加上嵌入式的动作。
我们将从一个文本文件中,根据列号,取出该列每行的值。
预期效果
输入 文件t.rows:
parrt Terence parr 101 tombu Tom Burns 020 bke Kevin Edgar 008
输出:
语法文件
观察以下代码:
我们在该语法分析器中加入了新成员:col(也就是列号)、一个自定义的RowsParser(获取输入的文件及列号);
在row规则中,我们引入了一个locals本地变量i,在STUFF读取完之前,i会++多次,并判断是否与col相同以决定是否要打印STUFF的内容。
//Rows.g4
grammar Rows;
@parser::members { // add members to generated RowsParser
int col;
public RowsParser(TokenStream input, int col) { // custom constructor
this(input);
this.col = col;
}
}
file: (row NL)+ ;
row
locals [int i=0]
: ( STUFF
{
$i++;
if ( $i == col ) System.out.println($STUFF.text);
}
)+
;
TAB : '\t' -> skip ; // match but don't pass to the parser
NL : '\r'? '\n' ; // match and pass to the parser
STUFF: ~[\t\r\n]+ ; // match any chars except tab, newline
运行效果
观察以下代码:
注意自定义的RowsParser的实例获取了输入文件以及列号,并且关掉了语法分析树的构建。
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import java.io.FileInputStream;
import java.io.InputStream;
public class Col {
public static void main(String[] args) throws Exception {
ANTLRInputStream input = new ANTLRInputStream(System.in);
RowsLexer lexer = new RowsLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
int col = Integer.valueOf(args[0]);
RowsParser parser = new RowsParser(tokens, col); // pass column number!
parser.setBuildParseTree(false); // don't waste time bulding a tree
parser.file(); // parse
}
}
java Col 1 < t.rows
java Col 2 < t.rows
java Col 3 < t.rows
得到结果:
词法分析特性
接下来将介绍三个词法分析的特性。
通过模式切换处理相同文件中的不同格式
以xml为例,当xml文件看到<时,词法分析器会切换到标签内部模式;而看到>时,会切换到默认方式。
我们可以通过设置一个哨兵,让语法分析器进行这种模式的切换,来应对不同格式,进行处理。
语法文件
观察以下代码:
我们发现当检查的词法OPEN时,会跳转到模式INSIDE。
而匹配到CLOSE或者SLASH_CLOSE时,会跳转出当前模式,回到默认模式。
//XMLLexer.g4
lexer grammar XMLLexer;
// Default "mode": Everything OUTSIDE of a tag
OPEN : '<' -> pushMode(INSIDE) ;
COMMENT : '<!--' .*? '-->' -> skip ;
EntityRef : '&' [a-z]+ ';' ;
TEXT : ~('<'|'&')+ ; // match any 16 bit char minus < and &
// ----------------- Everything INSIDE of a tag ---------------------
mode INSIDE;
CLOSE : '>' -> popMode ; // back to default mode
SLASH_CLOSE : '/>' -> popMode ;
EQUALS : '=' ;
STRING : '"' .*? '"' ;
SlashName : '/' Name ;
Name : ALPHA (ALPHA|DIGIT)* ;
S : [ \t\r\n] -> skip ;
fragment
ALPHA : [a-zA-Z] ;
fragment
DIGIT : [0-9] ;
运行词法分析器
输入 t.xml:
<tools>
<tool name="ANTLR">A parser generator</tool>
</tools>
运行词法分析器:
antlr4 XMLLexer.g4
javac XML*.java
grun XML tokens -tokens t.xml
结果:
从左至右分别是 token序号 字符起始位置 字符终止位置 内容文本 规则序号 行号 行内位置。
重写输入流
我们想实现这样一件事:读取输入,稍微修改以后输出。
这样的小动作我们不想通过Listener去更改每个方法,比如以下例子
输入:
//Demo.java
import java.util.List;
import java.util.Map;
public class Demo {
void f(int x, String y) { }
int[ ] g(/*no args*/) { return null; }
List<Map<String, Integer>>[] h() { return null; }
}
输出:
即:在进入classBody时,将这个serialVersionUID的常量字段打印出来。
重写Listener
实际上我们只需要重写enterClassBody就足够了。
首先我们创建一个重写器,并让这个rewriter获取tokens。
并在enterClassBody方法内嵌入这个rewriter,赋给它常量字段。
但注意,现在并没有触发重写。
//InsertSerialIDListener.java
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.TokenStreamRewriter;
public class InsertSerialIDListener extends JavaBaseListener {
TokenStreamRewriter rewriter;
public InsertSerialIDListener(TokenStream tokens) {
rewriter = new TokenStreamRewriter(tokens);
}
@Override
public void enterClassBody(JavaParser.ClassBodyContext ctx) {
String field = "\n\tpublic static final long serialVersionUID = 1L;";
rewriter.insertAfter(ctx.start, field);
}
}
运行结果
//InsertSerialID.java
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.*;
import java.io.FileInputStream;
import java.io.InputStream;
public class InsertSerialID {
public static void main(String[] args) throws Exception {
String inputFile = null;
if ( args.length>0 ) inputFile = args[0];
InputStream is = System.in;
if ( inputFile!=null ) {
is = new FileInputStream(inputFile);
}
ANTLRInputStream input = new ANTLRInputStream(is);
JavaLexer lexer = new JavaLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
JavaParser parser = new JavaParser(tokens);
ParseTree tree = parser.compilationUnit(); // parse
ParseTreeWalker walker = new ParseTreeWalker(); // create standard walker
InsertSerialIDListener extractor = new InsertSerialIDListener(tokens);
walker.walk(extractor, tree); // initiate walk of tree with listener
// print back ALTERED stream
System.out.println(extractor.rewriter.getText());
}
}
注意最后一行我们才通过rewriter将常量字段打印出来,打印的位置就是rewriter嵌入的位置。
因此rewriter更像是一个随取随用的工具。
将词法符号送入不同通道
在之前通过Listener模式识别Java的例子中,我们曾经保留了Java签名的空白字符和注释。
但通常语法需要让我们忽略它们。如果我们通过->skip的方式忽略,那么它们直接被丢弃了,我们无法再获取它们进行处理。
这里我们通过改变channel的方式,让空白字符和注释进入隐藏的channel。即:虽然忽略,但是不丢弃。
COMMENT
: '/*' .*? '*/' -> channel(HIDDEN) // match anything between /* and */
;
WS : [ \r\t\u000C\n]+ -> channel(HIDDEN)
;
LINE_COMMENT
: '//' ~[\r\n]* '\r'? '\n' -> channel(HIDDEN)
;