8.1 flex 简介

flex 是一个快速词法分析生成器,它可以将用户用正则表达式写的分词匹配模式构造成一个有限状态自动机(一个C函数),目前很多编译器都采用它来生成词法分析器。flex 的主页:http://flex.sourceforge.net/ 。下面以两个简单的例子来说明 flex 的使用方法。

首先安装 flex , 可以在终端输入以下命令来安装:

  1. $ sudo apt-get install flex

或者在其主页上下载相应版本的安装文件安装。

然后新建一个文本文件,输入以下内容:

  1. %%
  2. [0-9]+ printf("?");
  3. # return 0;
  4. . ECHO;
  5. %%
  6.  
  7. int main(int argc, char* argv[]) {
  8. yylex();
  9. return 0;
  10. }
  11.  
  12. int yywrap() {
  13. return 1;
  14. }

将此文件另存为 hide-digits.l 。注意此文件中的 %% 必须在本行的最前面(即 %% 前面不能有任何空格)。

之后,在终端输入:

  1. $ flex hide-digits.l

此时目录下多了一个 “lex.yy.c” 文件,把这个 C 文件编译并运行一遍:

  1. $ gcc -o hide-digits lex.yy.c
  2. $ ./hide-digits

然后在终端不停的敲入任意键并回车,可以发现,敲入的内容中,除数字外的字符都被原样的输出了,而每串数字字符都被替换成 ? 了。最后敲入 # 后程序退出了。如下:

  1. eruiewdkfj
  2. eruiewdkfj
  3. 1245
  4. ?
  5. fdsaf4578
  6. fdsaf?
  7. ...
  8. #

也就是说,hide-digits 这个程序的作用就是不停从标准输入(键盘)中读入字符,将其中的数字串替换成 ? 后再输出到标准输出(终端),当遇到 # 后程序退出。想象一下,如果要手工编程实现一个这样的程序,需要的代码量肯定比 hide-digits.l 文件多很多吧。

当在命令行中运行 flex 时,第二个命令行参数(此处是 hide-digits.l )是提供给 flex 的分词模式文件, 此模式文件中主要是用户用正则表达式写的分词匹配模式,用flex 会将这些正则表达式翻译成 C 代码格式的函数 yylex ,并输出到 lex.yy.c 文件中,该函数可以看成一个有限状态自动机。

下面再来详细解释一下 hide-digits.l 文件中的代码,首先第一段是:

  1. %%
  2. [0-9]+ printf("?");
  3. # return 0;
  4. . ECHO;
  5. %%

flex 模式文件中,%%%% 之间的内容被称为 规则(rules),本文件中每一行都是一条规则,每条规则由 匹配模式(pattern)事件(action) 组成, 模式在前面,用正则表达式表示,事件在后面,即 C 代码。每当一个模式被匹配到时,后面的 C 代码被执行。

简单来说,flex 会将本段内容翻译成一个名为 yylex 的函数,该函数的作用就是扫描输入文件(默认情况下为标准输入),当扫描到一个完整的、最长的、可以和某条规则的正则表达式所匹配的字符串时,该函数会执行此规则后面的 C 代码。如果这些 C 代码中没有 return 语句,则执行完这些 C 代码后, yylex 函数会继续运行,开始下一轮的扫描和匹配。

当有多条规则的模式被匹配到时, yylex 会选择匹配长度最长的那条规则,如果有匹配长度相等的规则,则选择排在最前面的规则。

第二段中的 main 函数是程序的入口, flex 会将这些代码原样的复制到 lex.yy.c 文件的最后面。最后一行的 yywrap 函数的作用后面再讲,总之就是 flex 要求有这么一个函数。

  1. int main(int argc, char *argv[]) {
  2. yylex();
  3. return 0;
  4. }
  5.  
  6. int yywrap() { return 1; }

因此,程序开始运行后,就开始执行 yylex 函数,然后开始扫描标准输入。当扫描出一串数字时,[0-9]+ 被匹配到,因此执行了 printf(”?”) ,当扫描到其他字符时,若不是 # ,则 . 被匹配,后面的 ECHO 被执行, ECHO 是 flex 提供的一个宏,作用是将匹配到的字符串原样输出,当扫描到 # 后, # 被匹配, return 0 被执行, yylex 函数返回到 main 函数,之后程序结束。

下面再来看一个稍微复杂一点的例子:

  1. %{
  2. #define T_WORD 1
  3. int numChars = 0, numWords = 0, numLines = 0;
  4. %}
  5.  
  6. WORD ([^ \t\n\r\a]+)
  7.  
  8. %%
  9. \n { numLines++; numChars++; }
  10. {WORD} { numWords++; numChars += yyleng; return T_WORD; }
  11. <<EOF>> { return 0; }
  12. . { numChars++; }
  13. %%
  14.  
  15. int main() {
  16. int token_type;
  17. while (token_type = yylex()) {
  18. printf("WORD:\t%s\n", yytext);
  19. }
  20. printf("\nChars\tWords\tLines\n");
  21. printf("%d\t%d\t%d\n", numChars, numWords, numLines);
  22. return 0;
  23. }
  24.  
  25. int yywrap() {
  26. return 1;
  27. }

将此文件另存为 word-spliter.l 。注意此文件中的 %{%} 必须在本行的最前面(前面不能有空格),同时,注意 %} 不要写成 }% 了。在终端输入:

  1. $ flex word-spliter.l
  2. $ gcc -o word-spliter lex.yy.c
  3. $ ./word-spliter < word-spliter.l

将输出:

  1. WORD: %{
  2. WORD: #define
  3. ...
  4. WORD: }
  5.  
  6. Chars Words Lines
  7. 470 70 27

可见此程序其实就是一个原始的分词器,它将输入文件分割成一个个的 WORD 再输出到终端,同时统计输入文件中的字符数、单词数和行数。此处的 WORD 指一串连续的非空格字符。

下面,详细介绍 flex 输入文件的完整格式,同时解释一下本文件的代码。一个完整的 flex 输入文件的格式为:

  1. %{
  2. Declarations
  3. %}
  4. Definitions
  5. %%
  6. Rules
  7. %%
  8. User subroutines

输入文件的第 1 段 %{%} 之间的为 声明(Declarations) ,都是 C 代码,这些代码会被原样的复制到 lex.yy.c 文件中,一般在这里声明一些全局变量和函数,这样在后面可以使用这些变量和函数。

第 2 段 %} 和 %% 之间的为 定义(Definitions),在这里可以定义正则表达式中的一些名字,可以在 规则(Rules) 段被使用,如本文件中定义了 WORD([^**\t\n\r\a]+) , 这样在后面可以用 {WORD}** 代替这个正则表达式。

第 3 段为 规则(Rules) 段,上一个例子中已经详细说明过了。

第 4 段为 用户定义过程(User subroutines) 段,也都是 C 代码,本段内容会被原样复制到 yylex.c 文件的最末尾,一般在此定义第 1 段中声明的函数。

以上 4 段中,除了 Rules 段是必须要有的外,其他三个段都是可选的。

输入文件中最后一行的 yywrap 函数的作用是将多个输入文件打包成一个输入,当 yylex 函数读入到一个文件结束(EOF)时,它会向 yywrap 函数询问, yywrap 函数返回 1 的意思是告诉 yylex 函数后面没有其他输入文件了,此时 yylex 函数结束,yywrap 函数也可以打开下一个输入文件,再向 yylex 函数返回 0 ,告诉它后面还有别的输入文件,此时 yylex 函数会继续解析下一个输入文件。总之,由于我们不考虑连续解析多个文件,因此此处返回 1 。

和上一个例子不同的是,本例中的 action 中有 return 语句,而 main 函数内是一个 while 循环,只要 yylex 函数的返回值不为 0 ,则 yylex 函数将被继续调用,此时将从下一个字符开始新一轮的扫描。

另外,本例中使用到了 flex 提供的两个全局变量 yytextyyleng,分别用来表示刚刚匹配到的字符串以及它的长度。

为方便编译,使用 makefile 进行编译及运行:

  1. run: word-spliter
  2. ./word-spliter < word-spliter.l
  3.  
  4. word-spliter: lex.yy.c
  5. gcc -o $@ $<
  6.  
  7. lex.yy.c: word-spliter.l
  8. flex $<

将以上内容保存为 makefile ,和 word-spliter.l 文件放在当前目录,再在终端输入:

  1. make

将输出和前面一样的内容。 makefile 的语法本站就不介绍了,后文中的大部分程序都将使用 makefile 编译。

好了, flex 的使用就简单介绍到这,以上介绍的功能用来解析 TinyC 文件已经差不多够了。有兴趣的读者可以到其主页上去阅读一下它的手册,学习更强大的功能。下面介绍如何使用 flex 对 TinyC 源文件进行词法分析。