第一节、lex和yacc是什么?
lex 代表 lexical analyzar(词法分析器),yacc 代表 yet another compiler compiler(编译器代码生成器)。lex和yacc在UNIX下分别叫flex和bison. 可以搜索到很多介绍flex&bison的文章,但这类文章对初学者来说不太容易看懂。
我们举个简单的例子来理解lex和yacc:在linux下,有很多系统配置文件,一些linux下的软件也有配置文件,那么程序是如何读取配置文件中的信息的呢?先用到lex词法分析器,读取配置文件中的关键词(后面说到的token标记其实可看做关键词);然后把关键词递交给yacc,yacc对一些关键词进行匹配,看是否符合一定的语法逻辑,如果符合就进行相应动作。
上面举的例子是分析配置文件内容的,当然可分析其他文件内容,或者制作编译器等。
第二节、一个简单的lex程序。
1、程序代码。
来看一个简单的lex程序,代码见下面,这段lex程序的目的是:输入几行字符串,输出行数,单词数和字符的个数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
|
/******************************************* * Name : test.l * Date : Mar. 11, 2014 * Blog : http://www.cnblogs.com/lucasysfeng/ * Description : 一个简单的lex例子,输入几行字符串, * 输出行数,单词数和字符的个数。 *******************************************/
/* 第一段 */ %{ int chars = 0; int words = 0; int lines = 0; %}
/* 第二段 */ %% [a-zA-Z]+ { words++; chars += strlen(yytext); } \n { chars++; lines++; } . { chars++; } %%
/* 第三段 */ main(int argc, char **argv) { yylex(); printf("%8d%8d%8d\n", lines, words, chars); }
|
程序中yytext是lex变量,匹配模式的文本存储在这一变量中。yylex()这一函数开始分析,它由lex自动生成。关于lex变量和函数后续再介绍,这里只是通过简单的lex程序来认识lex.
2、按照下面过程编译运行。
#flex test.l
#gcc lex.yy.c –lfl
#./a.out
然后输入一段文字,按ctrl+d结束输入,则会输出行数,单词数和字符的个数。
见下图:
3、分析上面的lex程序。
(1)%%把文件分为3段,第一段是c和lex的全局声明,第二段是规则段,第三段是c代码。
(2)第一段的c代码要用%{和%}括起来,第三段的c代码不用。
(3)第二段规则段,[a-zA-Z]+ \n . 是正则表达式,{}内的是c编写的动作。
4、编译时不加-lfl选项。
上面编译时用gcc lex.yy.c –lfl,那么如何直接用gcc lex.yy.c进行编译呢?答案是加上yywrap函数(具体原因见lex的库和函数分析),代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
|
/******************************************* * Name : test.l * Date : Mar. 11, 2014 * Blog : http://www.cnblogs.com/lucasysfeng/ * Description : 一个简单的lex例子,输入几行字符串, * 输出行数,单词数和字符的个数。 * 加yywrap函数。 *******************************************/ %{ int chars = 0; int words = 0; int lines = 0; %} %% [a-zA-Z]+ { words++; chars += strlen(yytext); } \n { chars++; lines++; } . { chars++; } %% main(int argc, char **argv) { yylex(); printf("%8d%8d%8d\n", lines, words, chars); } int yywrap() { return 1; }
|
第三节、lex进阶。
修改第二节程序,将正则表达式放在全局声明中,使逻辑更清晰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
|
/******************************************* * Name : test.l * Date : Mar. 11, 2014 * Blog : http://www.cnblogs.com/lucasysfeng/ * Description : 一个简单的lex例子,输入几行字符串, * 输出行数,单词数和字符的个数。 * 正则表达式放在全局声明中。 *******************************************/ int chars = 0; int words = 0; int lines = 0; %} mywords [a-zA-Z]+ mylines \n mychars . %% {mywords} { words++; chars += strlen(yytext); } {mylines} { chars++; lines++; } {mychars} { chars++; } %% main(int argc, char **argv) { yylex(); printf("%8d%8d%8d\n", lines, words, chars); }
|
编译运行同第二节。
第四节、lex再进阶—循环扫描。
下面给出一个lex程序,这个程序在扫描到 + 或 - 时做一个特殊输出。当调用yylex()函数时,若扫描到return对应的标记时,yylex返回,且值就为return后的值;若没扫描到return对应的标记,yylex继续执行,不返回。下次调用自动从前一次的扫描位置处开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
|
/******************************************* * Name : test.l * Date : Mar. 11, 2014 * Blog : http://www.cnblogs.com/lucasysfeng/ * Description : lex进阶,循环扫描。 *******************************************/ %{ enum yytokentype { ADD = 259, SUB = 260, }; %} myadd "+" mysub "-" myother . %% {myadd} { return ADD; } {mysub} { return SUB; } {myother} { printf("Mystery character\n"); } %% main(int argc, char **argv) { int tok; while (tok = yylex()) { if (tok == ADD || tok == SUB) { printf("meet + or -\n"); } else { printf("this else statement will not be printed, \ because if yylex return,the retrun value must be ADD or SUB."); } } }
|
编译和运行见下图:
第五节、yacc语法。
1、yacc语法规则部分和BNF类同,先来看BNF巴克斯范式。
(1)<> 内包含的内容为必选项;
(2)[] 内的包含的内容为可选项;
(3){ } 内包含的为可重复0至无数次的项;
(4) | 表示在其左右两边任选一项,相当于"OR"的意思;
(5)::= 是“被定义为”的意思;
(6)双引号“”内的内容代表这些字符本身;而double _quote用来表示双引号。
(7)BNF范式举例,下面的例子用来定义java中的for语句:
FOR_STATEMENT ::=
"for" "(" ( variable_declaration |
( expression ";" ) | ";" )
[ expression ] ";"
[ expression ]
")" statement
2、yacc语法。
注:components是根据规则放在一起的终端和非终端符号,后面是{}括起来的执行的动作。
3、语法例子。
1 2 3 4 5
|
|
param : NAME EQ NAME { printf("\tName:%s\tValue(name):%s\n", $1,$3); } | NAME EQ VALUE { printf("\tName:%s\tValue(value):%s\n",$1,$3);} ;
|
yacc文件第一段中定义的token,lex文件对目标进行扫描并返回这些token。yacc文件对规则冒号右边componets进行匹配,如果符合一定语法规则就执行相应动作。
1 2 3 4 5 6 7 8 9 10 11
|
|
simple_sentence: subject verb object | subject verb object prep_phrase ; subject: NOUN | PRONOUN | ADJECTIVE subject ; verb: VERB | ADVERB VERB | verb VERB ; object: NOUN | ADJECTIVE object ; prep_phrase: PREPOSITION NOUN ;
|
分析:|表示左右两边任选一项,如| subject verb object prep_phrase ;中|的左边为空,所以该句表示匹配空或者subject verb object prep_phrase ;而上面还有一句subject verb object ,所以
simple_sentence: subject verb object
| subject verb object prep_phrase ;
的意思是匹配subject verb object 或 subject verb object prep_phrase ;
第六节、lex和yacc结合使用。
1、lex程序。
当匹配a b c not时分别返回相应的token.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
|
/******************************************* * Name : test.l * Date : Mar. 11, 2014 * Blog : http://www.cnblogs.com/lucasysfeng/ * Description : lex和yacc结合使用。 *******************************************/ %{ #include "test.tab.h" #include <stdio.h> #include <stdlib.h> %} %% a { return A_STATE; } b { return B_STATE; } c { return C_STATE; } not { return NOT; } %%
|
2、yacc程序。
当扫描到A_STATE B_STATE时打印1,当扫描到A_STATE B_STATE
c_state_not_token时打印2,当扫描到NOT时打印3.
其中,A_STATE B_STATE NOT是token,c_state_not_token
是非终端符号,此处定义为
c_state_not_token : C_STATE {}.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
|
/******************************************* * Name : test.y * Date : Mar. 11, 2014 * Blog : http://www.cnblogs.com/lucasysfeng/ * Description : lex和yacc结合使用。 *******************************************/ %{ #include <stdio.h> #include <stdlib.h> %} %token A_STATE B_STATE C_STATE NOT %% program : A_STATE B_STATE { printf("1"); } c_state_not_token { printf("2"); } | NOT { printf("3"); } c_state_not_token : C_STATE {} %% yyerror(const char *s) { fprintf(stderr, "error: %s\n", s); } int main() { yyparse(); return 0; }
|
3、编译和运行。
lex和yacc在UNIX下分别叫flex和bison.
第七节、lex和yacc结合使用进阶。
1、我们希望用lex和yacc结合完成下面文件解析。
我们对文本test.txt进行分析,test.txt中的内容如下:
ZhangSan=23
LiSi=34
WangWu=43
扫描test.txt文本后,我们希望输出:
ZhangSan is 23 years old!!!
LiSi is 34 years old!!!
WangWu is 43 years old!!!
2、利用lex扫描test.txt文本,返回token.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
|
/******************************************* * Name : test.l * Date : Mar. 11, 2014 * Blog : http://www.cnblogs.com/lucasysfeng/ * Description : lex和yacc结合使用进阶。 *******************************************/ %{ #include "test.tab.h" #include <stdio.h> #include <string.h> %} char [A-Za-z] num [0-9] eq [=] name {char}+ age {num}+ %% {name} { yylval = strdup(yytext); return NAME; } {eq} { return EQ; } {age} { yylval = strdup(yytext); return AGE; } %% int yywrap() { return 1; }
|
3、yacc根据lex返回的token,判断这些token是否符合一定的语法,符合则进行相应动作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
|
/******************************************* * Name : test.y * Date : Mar. 11, 2014 * Blog : http://www.cnblogs.com/lucasysfeng/ * Description : lex和yacc结合使用进阶。 *******************************************/ %{ #include <stdio.h> #include <stdlib.h> typedef char* string; #define YYSTYPE string %} %token NAME EQ AGE %% file : record file | record ; record : NAME EQ AGE { printf("%s is %s years old!!!\n", $1, $3); } ; %% int main() { extern FILE* yyin; if (!(yyin = fopen("test.txt", "r"))) { perror("cannot open parsefile:"); return -1; } yyparse(); fclose(yyin); return 0; } int yyerror(char *msg) { printf("Error encountered: %s \n", msg); }
|
4、编译运行。
补充:lex变量和和函数。