编译原理中,将源代码分解为若干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章节一同观看。
一些Linux发行版会自带Flex程序,如果系统没有预装Flex,我们直接通过软件源等方式安装即可。Lex文件建议使用Vim作为编辑器,Vim对Lex语法的高亮支持良好。此外我们可以查看Flex工具的手册,里面介绍了相关的命令行选项。
man flex
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语言代码,为正则表达式命名等。{}
包含。%{
和%}
。5-11行我们定义了若干正则表达式和对应的操作,这里只是使用prinf()
进行输出。注意:
yytext
,这个就是当前Token的字符串。[a-zA-Z0-9]+
的结果是包含[0-9]+
的,但是我们要记住,Lex会识别正则表达式的定义顺序,我们先定义的[0-9]+
,那么遇到123
这种字符串,就会优先匹配。[ \t]+
,这个正则表达式实际上是用来匹配空格和制表符的,但是其后面的C语言代码是空的,这意味着我们的词法分析器会忽略空格和制表符。13行在用户代码区块中,我们定义了一个main
函数并调用yylex()
,这个函数就代表整个Lex词法分析器。
我们尝试运行一下:
这确实是我们想要的结果。但是运行一下就会发现,我们并没有编写scanf
啊,这个程序为什么会接收输入?
这是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
替代。int yylex(void)
:词法分析程序,它自动移动yyin
和yyout
。在定义匹配动作时,用户可用return
语句结束yylex()
,此处返回值必须是一个整数。
由于yylex()
的运行环境都是以全局变量的方式保存,因此,在下一次调用yylex()
时,它可从上次扫描的断点处继续扫描,在语法分析时我们可利用这一特性。
若用户未定义相应的return
语句,则yylex()
继续分析被扫描的文件直到碰到文件结束标志EOF。在读到EOF时,yylex()
调用yywrap()
函数(该函数用户必须提供),若该函数返回非0
值,则yylex()
返回0
而结束。否则,yylex()
继续对yyin
指向的文件扫描。因此,yywrap()
可以用来实现扫描多个文件。
yymore()
:将当前Token保留在yytext
中,分析器下次扫描识别的Token将加追加在yytext
中,下面是一个例子。
hello {printf(“%s!”,yytext);yymore();}
world {printf(“%s!”,yytext);}
当输入串为”helloworld”时,将输出hello!helloworld!
yyless(int n)
:回退当前识别的Token中n个字符到输入中,也就是说,下次扫描还会扫到刚刚回退的Token。
unput(char c)
:回退字符c到输入,下次会扫描到字符c。
input()
:让分析器从输入缓冲区中读取当前字符,并将yyin
指向下一字符。
yyterminate()
:中断对当前文件的分析,将yyin
指向EOF。
yyrestart(FILE * file)
:重新设置分析器的扫描文件为file
。