文章目录
- 前言
- lex/yacc
- lex
- 定义块
- 翻译块
- 函数块
- 一些预定义的变量和函数
- lex示例
- yacc
- 声明块
- 语法规则块块
前言
当我们面对一个具有一定语法规则的文本内容,如log文件,CMakeLists.txt,甚至某种编程语言的源文件,希望提取出其中的有用信息时,我们该如何做?最简单的方式就是使用逐字或者逐行读取的方式,根据文本的规则去编写if条件语句,判断是否已经碰到了我们希望读取的内容,如果是则提取信息,否则跳过。
对于具有复杂规则的文本,这么做的效率很低,并且不够优雅。我们可以使用lex yacc来帮助我们进行这项工作。
lex/yacc
lex是Lexical Analyzer Generator(词法分析生成器)的缩写,它能生成一个词法分析程序,该程序运行后可以进行词法分析。
yacc是Yet Another Compiler Compiler(一个编译器的编译器)的缩写,它能生成语法分析器,需要与lex一起使用。
网上有时也能看到flex和bison这两个工具,其地位分别对应于lex和yacc,使用方法也比较类似
lex
lex的输入是一个lex源文件,通常以.l或.lex结尾,输出是一个词法分析程序的C代码,经过编译器编译链接就可以得到可执行程序(scanner),结构如下:
定义块(definition section)
%%
翻译块(translation section)
%%
函数块(function section)
文件由%%
分隔的三部分组成,定义(definition)块可将正则表达式命名,方便后面使用并且使lex源文件更具有可读性。规则块包含匹配到正则表达式规则时所对应的动作(action)。函数块部分会直接插入到输出源文件最后面。
定义块
lex允许将一个名字(name)和正则表达式关联起来,称之为定义(definition)
// 格式:name regular-expression
DIGIT [0-9]
ALPHA [a-zA-Z]
这样我们在其他definition或者规则需要使用该正则表达式的地方就可以使用改名字
VAR_NAME {ALPHA}*{DIGIT}*
定义块还可以包含所需的头文件,变量等:
%{
#include <stdio.h>
int global = 0;
%}
%{
和%}
之间的内容会直接插入输出源文件中。
该部分还可以包含一些选项,用于控制lex的一些行为,从而影响输出的源程序内容
%option nounput noyywrap
翻译块
该部分定义了规则和对应的动作(action)
// 格式:token-pattern { actions }
"Hello" {printf("Hi there!\n");}
[0-9] {return YYDIGIT;}
这里我们简单复习下正则表达式:
"if" //匹配任意位置出现的"if"字符串
“\n”
^"We" //匹配行开头的"We"字符串
"end"$ //匹配行结尾的"end"字符串
^"name"$ //匹配单行的"name"
[0-9a-z] //匹配数字和小写a-z单字符
"p.x" //表示p+任意字符+x的字符串
[0-9]* //匹配任意长度数字串(包括空)
[0-9]+ //匹配至少一个数字
[0-9]{3} //匹配三个数字
[0-9]{2,3} //匹配2-3个数字
A? //匹配空串或A
"high" | "low" //匹配两个表达式中的一个
当在字符串流中匹配到规则时,将会执行相应的代码,如打印信息,返回token值等等。
如果需要使用到一些变量,则其定义应该写在规则块或定义块的最前面,同样使用如下格式插入代码:
%{
int wordcount
%}
在翻译块的代码块会插入生成的yylex函数(稍后介绍)中,是局部变量,在定义段的代码块在yylex函数外,是全局变量。
函数块
这里定义需要用到的函数,直接编写即可。
一些预定义的变量和函数
int yylex()
,开始进行词法分析需要调用的函数。int yywrap()
,到达文件末尾时,将调用yywrap,如果不使用,可以定义其返回1,或者使用%option noyywrap
char* yytext
,匹配的字符串内容int yyleng
,匹配的字符串长度
``
lex示例
lex源文件:
%{
int word = 0;
%}
ALPHA [a-zA-Z]
WORD {ALPHA}*
%option noyywrap
%%
{WORD} { word++; printf("match WORD: %s\n", yytext); }
[\n\t ]* { printf("match space\n"); }
. { printf("Undefined char: %s\n", yytext); }
%%
//int yywrap() {
// return 1;
//}
int main() {
FILE* f;
f = fopen("test.txt", "r");
if (!f)
return -1;
yyin = f;
yylex();
printf("word count: %d\n", word);
fclose(f);
return 0;
}
输入文件test.txt:
Hello world!
This is a file to test lex.
end.
编译指令:
lex test.l
gcc lex.yy.c -o scanner
执行scanner,得到输出:
match WORD: Hello
match space
match WORD: world
Undefined char: !
match space
match WORD: This
match space
match WORD: is
match space
match WORD: a
match space
match WORD: file
match space
match WORD: to
match space
match WORD: test
match space
match WORD: lex
Undefined char: .
match space
match WORD: end
Undefined char: .
word count: 10
至此通过编写test.l,我们已经完成了一个简单的词数统计的程序。这里强烈建议查看lex生成的源码,在这里我们可以看到lex帮助我们生成的函数、变量、宏,以及我们自己编写的代码在其中的位置和所起的作用,从而更好的理解scanner的实现原理。
yacc
yacc同样需要输入yacc源文件,以生成parser,其结构如下:
声明块(declarations section)
%%
语法规则块(grammar rules section)
%%
函数块(functions section)
声明块
声明块包含token声明和函数变量声明,以及规则优先级。token声明格式如下:
%token name1 name2 ...
例如
%token WORD INTEGER
定义了两个token:WORD和INTEGER,yacc生成的程序中通过#define
语句定义这两个token的值,从257开始,这是为了避免和已有字符冲突。token的值由scanner在执行yylex过程中从action中返回,表明特定的字符串已经被匹配识别。通常token由大写的字母表示,避免和C中的关键字和yacc生成的函数及变量冲突。
左结合和右结合通过%left
和%right
定义,同一个结合性声明中符号的优先级相同,多个结合性声明中,先声明的符号集优先级更低,例如
%right '='
%left '+' '-'
%left '*' '/' '%'
=
是右结合,其他是左结合,优先级逐行递增。没有结合性的符号使用%nonassoc
声明。也可以声明标识符的结合性,这些标识符将自动声明为token。
变量及函数声明等代码部分同样通过%{
和%}
括起来。
语法规则块块
格式:
identifier : definition ;
例如:
paren_expr : '(' expr ')' ;
我们定义了一个语法:paren_expr
以(
开头)
结尾,中间是expr
,expr
可以是一个token也可以是另一个语法规则。可以继续展开的符号是非终结符,如冒号左边的符号一定是非终结符,不可继续展开的符号是终结符,如字符常量和token。非终结符可以有多个定义,例如:
if_stat : IF '(' expr ')' stat
│ IF '(' expr ')' stat ELSE stat
;
非终结符还可以是递归定义的:
list : item
│ list ',' item
;
这里list的定义是左递归
(yacc部分未完待续)
参考:
- https://baike.baidu.com/item/lex/8558986
- https://baike.baidu.com/item/Yacc/9855057
- https://www.ibm.com/docs/en/zos/2.1.0?topic=tools-tutorial-using-lex-yacc