lex词法分析器
编译原理中,将源代码分解为若干Token的过程叫做词法分析。Lex(LEXical compiler)是一个词法分析工具,我们使用Lex,只需按照其规则像定义配置文件一样定义好.l(或.lex)文件然后执行Lex程序就能生成我们想要的词法分析器的C语言代码了。Lex原本由贝尔实验室开发,主要用于早期的Unix操作系统,Flex则是Lex的一个现代的替代品,它的基本用法和Lex是完全相同的,因此现在我们常说的Lex其实就是Flex程序(后文所说的Lex均指Flex程序),它最初由Vern Paxson于1987年用C语言写成。
本篇笔记我们主要学习Lex的基本用法,阅读本文需要正则表达式的前置知识。Lex一般结合Yacc使用,因此建议本文和Yacc章节一同观看。
安装Flex
一些Linux发行版会自带Flex程序,如果系统没有预装Flex,我们直接通过软件源等方式安装即可。Lex文件建议使用Vim作为编辑器,Vim对Lex语法的高亮支持良好。此外我们可以查看Flex工具的手册,里面介绍了相关的命令行选项。
man flex
Lex的使用
Lex使用起来不复杂,但是比较难用文字描述。这里我们直接从例子入手,看看Lex是如何使用的。
我们使用Lex编写一个最简单的例子,这个例子能够区分文本和整数型数字。我们的需求很简单,定义纯数字为INT类型,带有英文字符的为TEXT类型,空格和制表符忽略,例如:输入abc 123 ab1,对应TEXT INT TEXT。
demo.l
%{
#include <stdio.h>
%}
%%
[0-9]+ {
printf("INT:%s\n", yytext);
}
[a-zA-Z0-9]+ {
printf("TEXT:%s\n", yytext);
}
[ \t]+ {}
%%
int main(void)
{
yylex();
return 0;
}
int yywrap(void)
{
return 1;
}
执行以下命令,将Lex源代码文件编译为可执行文件。
lex demo.l && gcc lex.yy.c
首先我们要知道,执行Lex程序,会把demo.l编译成lex.yy.c,然后我们使用GCC编译这个C语言源代码文件即可。
第1行和第3行出现了%{和%}这两个标记,这部分的C语言代码Lex会原样输出到lex.yy.c,这一部分可以写include语句以及定义一些函数、全局变量等操作。
18行定义了yywrap()函数,我们只要知道把它加上就好了,在这里不对这个函数做深入分析,在后文有一些说明。
4行和12行分别有一个%%,这个是Lex文件的区块分隔符。Lex文件分为三个区块:
- 定义区块:可以使用
%{和%}放置C语言代码,为正则表达式命名等。 - 规则区块:用正则表达式描述Token和对应操作。注意规则区块的写法要求:一个正则表达式后面跟随空格,然后接上C语言代码,多行C语言代码需要使用
{}包含。 - 用户代码区块:用户代码区块可以编写任意C代码,也会原样输出,与定义区块不同,用户代码区块无需使用
%{和%}。
5-11行我们定义了若干正则表达式和对应的操作,这里只是使用prinf()进行输出。注意:
yytext,这个就是当前Token的字符串。- 观察5行和8行的正则表达式,我们可以发现,实际上
[a-zA-Z0-9]+的结果是包含[0-9]+的,但是我们要记住,Lex会识别正则表达式的定义顺序,我们先定义的[0-9]+,那么遇到123这种字符串,就会优先匹配。 - 注意最后的
[ \t]+,这个正则表达式实际上是用来匹配空格和制表符的,但是其后面的C语言代码是空的,这意味着我们的词法分析器会忽略空格和制表符。
13行在用户代码区块中,我们定义了一个main函数并调用yylex(),这个函数就代表整个Lex词法分析器。
我们尝试运行一下:

这确实是我们想要的结果。但是运行一下就会发现,我们并没有编写scanf啊,这个程序为什么会接收输入?
这是Lex的默认行为,从标准输入读取字符串,接着往后看就明白了。
Lex全局变量和函数
上面我们看到,实际上Lex工具将.l文件编译成了一个C语言源文件,我们可以打开这个文件观看它都定义了哪些内容。
如果我们想在我们的程序中使用Lex该怎么办呢?Lex在生成的.c文件中,定义了很多全局变量和函数。我们调用Lex或者向Lex传递数据,实际上就需要通过这些全局变量和函数来实现。
全局变量
FILE *yyin和FILE *yyout:这两个变量是Lex的输入和输出流指针,如果用户未对其进行定义,默认指向stdin和stdout。char *yytext:存放当前被识别的Token。ECHO:Lex预定义的宏,将当前识别的Token输出到yyout,其实上面例子printf()就可以使用ECHO替代。
yylex
int yylex(void):词法分析程序,它自动移动yyin和yyout。在定义匹配动作时,用户可用return语句结束yylex(),此处返回值必须是一个整数。
由于yylex()的运行环境都是以全局变量的方式保存,因此,在下一次调用yylex()时,它可从上次扫描的断点处继续扫描,在语法分析时我们可利用这一特性。
若用户未定义相应的return语句,则yylex()继续分析被扫描的文件直到碰到文件结束标志EOF。在读到EOF时,yylex()调用yywrap()函数(该函数用户必须提供),若该函数返回非0值,则yylex()返回0而结束。否则,yylex()继续对yyin指向的文件扫描。因此,yywrap()可以用来实现扫描多个文件。
yymore
yymore():将当前Token保留在yytext中,分析器下次扫描识别的Token将加追加在yytext中,下面是一个例子。
hello {printf(“%s!”,yytext);yymore();}
world {printf(“%s!”,yytext);}
当输入串为”helloworld”时,将输出hello!helloworld!
yyless
yyless(int n):回退当前识别的Token中n个字符到输入中,也就是说,下次扫描还会扫到刚刚回退的Token。
unput
unput(char c):回退字符c到输入,下次会扫描到字符c。
input
input():让分析器从输入缓冲区中读取当前字符,并将yyin指向下一字符。
yyterminate
yyterminate():中断对当前文件的分析,将yyin指向EOF。
yyrestart
yyrestart(FILE * file):重新设置分析器的扫描文件为file。