1. Flex和Bison:
1) 前身是Unix的lex和yacc,而flex和bison是自由软件基金会的项目(FSF);
2) 其职责所在就是处理结构化输入(即处理具有一定规则的文本输入),最初用来制作编译器;
3) flex用于词法分析,bison用于语法分析;
4) 大多Linux、BSD系统自带这两个工具,Mac OS X也自带flex和bison;
2. 词法分析和记号:
1) 把输入分割成一个个有意义的词块(称为记号,token);
2) 记号,在一般源程序中有多种形式,比如关键字int、double等就属于记号,通常源程序中所有的记号都会被编译器保存在一张记号表中;
3) 比如:a = b + c; 这段代码就可以被分割成a、等号、b、加号、c、分号这6个记号;
4) 通常记号由两部分组成,一个是记号编号,另一个是记号值;
5) 记号编号表示记号所属的类别,通常用一个数字宏来表示,比如:
#define NUMBER 280
而这个数字宏NUMBER(280)就表示一个记号编号,可以表示所有数字类的字符串
6) 记号的值就表示该记号所呈现出来的字符串字面值,比如12345就是一个记号,它所属的类别是NUMBER,或者说属于280号类别,其值就是字符串字面值"12345";
7) 通常记号在bison程序中定义,定义语法例如“%token NUMBER STRING CHARACTER”,关键字%token就表示接下来要定义一组记号,原理就是和#define或者说时C语言的enum很像,但是记号的编号通常是从258开始的(这样可以避免与文字字符记号相冲突,一般为扩展ASCII集(0 ~ 255),bison记号和扩展ASCII集中间还隔了2个位置),就相当于enum { NUMBER = 258, STRING, CHARACTER; };
8) 一般是在bison程序中定义这些记号,用bison编译后会产生一个头文件yy.tab.h,该头文件中就包含了这些记号,然后在flex程序中包含该头文件,这样就可以在flex程序中使用这些记号了;
3. 语法分析:
1) 一般和此法分析配合使用,词法分析找出了各种词块(记号),而语法分析则是找出这些记号之间的关系,并根据不同的关系做出不同的动作;
2) bison的工作原理就是用户在bison程序中定义一组规则(即推导规则),然后bison根据这些规则生成一棵语法树,该语法树用来解析flex分析的各个词块之间的关系(该过程成为解析parse);
4. 正则表达式简介:
1) 词法分析就是拿输入和预定义的模式进行匹配的过程;
2) 模式就是一种字符串的描述,描述了字符串是按照何种规则组成的,比如“由数字组成的长度任意的字符串”就是一个模式,234243、23411等都是该模式描述的字符串;
3) 正则表达式就是一组简介的模式描述方式(模式描述规则);
4) flex程序主要就是由一系列带有指令的正则表达式组成,其中每个正则表达式描述了要匹配的一类字符串,而其对应的指令就是指当匹配到了一个该模式的字符串后要执行的相关动作;
5) flex会利用所有的正则表达式生成一个DFA(确定性有限状态自动机),因此此法分析时的复杂度只和输入的长度有关(复杂度和AC自动机类似);
5.flex和bison的编译流程:
1) bison文件为.y,经过bison编译后生成yy.tab.h和yy.tab.c;
2) flex的文件为.l,接着在.l中include刚刚生成yy.tab.h,用flex编译后生成lex.yy.c;
3) 接着用C编译器分别编译两个.c文件生成各自的.o文件,最后链接生成可执行文件;
4) 当然也可以直接用C编译器对两个.c文件联合编译直接生成可执行文件;
5) 其中yy部分就是用户可以随意取名的部分;
6) 一个典型的便以流程演示:
bison -o test.tab.c -d test.y flex -o lex.test.c test.l cc -o a.out -ll test.tab.c lex.test.c其中bison的选项-d表示要为flex生成一个.h头文件,如果-o指定的输出文件名为name.c,则输出的头文件必为name.h,该头文件里就定义了供flex使用的各种宏(标记)等信息;
cc的-ll选项表示要链接lex的库,即可以分解成-l l后者-l fl,选项参数l表示lex,fl表示flex,因为bison在运行的时候依赖flex的库函数yyflex(),因此需要链接该库来使用相关的函数;
6. 一个简单的纯flex程序示例:
注意!flex和bison源程序只支持C风格的注释/* */,但不支持C++风格的注释//!
/* a.l */ /* 声明和选项设置部分 */ /* flex内部功能选项设置部分写在%{%}外部 */ %{ /* 变量、结构、函数原型等的声明写在%{和%}之中 */ /* 这部分的内容会被照抄到生成的.c文件中 */ /* flex、bison和C语言是强耦合的!*/ int nchar; int nword; int nline; %} /* %%用来表示一个部分的结束 */ /* flex和bison源程序都是严格地由三部分组成,因此必须包含两个%% */ /* 用以分隔这三个部分 */ /* 第二部分是带命令的正则表达式列表 */ /* 每个正则表达式必须定义在每行的开头,如果以空白开始一行,则flex会认为要匹配的模式是一个空白符 */ /* 在每个正则表达式后面跟一个{}包围起来的一组命令,命令用C语言写的 */ /* 表示当匹配到属于该模式的字符串后应该执行的动作 */ /* 读取文本、匹配文本并执行指令的过程是由flex的库函数yylex()执行的 */ /* 因此在最后链接是一定要把flex库链上 */ /* yytext是一个flex内部维护的字符串,匹配上的记号的记号值就保存在里面 */ /* 可以把带命令的正则表达式看做一个个函数,比如下面的这个例子就可以看成 */ /* _func_match_[a-zA-Z]+_( char *yytext ) { ... } */ /* 由于规则部分每一行的开始都会作为正则表达式,因此不能加注释!*/ %% [a-zA-Z]+ { nword++; nchar += strlen( yytext ); } /* 匹配字母组成的单词 */ \n { nchar++; nline++; } /* 匹配换行符 */ . { nchar++; } /* 匹配任意的单个字符 */ %% /* 第二部分结束 */ /* 第三部分:与第二部分指令动作相关的例程,会被直接拷贝到生的词法分析器的C代码中 */ int main( int argc, char **argv ) { yylex(); printf( "%8d%8d%8d\n", nline, nword, nchar ); return 0; } /* 该程序实现和wc命令相似的功能 */
运行改程序时,由于没有对命令行参数进行处理程序只能从标准输入stdin读入文本数据!
结果:
$ flex a.l $ cc -ll lex.yy.c $ ./a.out The boy stood on the burning deck shelling peanuts by the peck ^D 2 12 63 $
1) 使用元语言(metalanguage)来描述模式,而元语言使用标准的ASCII文本字符;
2) 这些字符中一部分用来匹配自身,而另一部分则代表匹配模式(即具有特殊意义的字符);
3) 一下列出具有特殊意义的字符,其余都将匹配自身:
. 匹配除换行符\n以外的任意单一字符
* 匹配0个或多个紧接在前面的表达式
+ 匹配1个或多个进阶在前面的表达式(表达式即模式)
? 匹配0个或1个紧接在前面的表达式,即表示或有或无的字面意思,比如-?[0-9]就表示数字之前有一个可选的前置负号
| 或运算,表示匹配前一个或后一个紧接着的表达式,比如cow|cat|dog就表示匹配三个词中的任意一个(注意是一个!),这种情况下|两边不能有空白符
另一种特殊的用法,在flex程序中的第二部分(即定义带动作的正则表达式中):
cat |
dog |
cow { puts( "Animal!" ); }
这种情况下|必须和左边的表达式之间用任意多个(至少一个空白符)隔开,表示下一个模式应用相同的动作,因此这个例子中所有的模式都应用为最后一个词所指定的动作其实让|和前面的模式空开,就表示空白后面的是动作而不是模式的一部分了!即空开后|将被理解成一种动作而不是模式的一部分了,而此时的|就表示重复和下面相同的动作的意思!
[ ] 表示一个字符类,可以匹配方括号中任意一个(注意是一个!)字符,如果方括号中第一个字符是^(称为抑扬符号),则其意思就是匹配除方括号内字符(不包括抑扬符^本身)以外的任何字符,其中-符号在[ ]中也有特殊含义,如果是a-z出现在方括号中,则表示一个字符范围,即所有小写字母,如果是0-9则表示所有数字,其实-范围符号的本质就是匹配两边字符ASCII码所处的范围,[ ]中的所有字符包括空格也会被看做是字符类中的一元,比如[a-z A-Z]这就表示所有英文字母再加上空格都会被匹配,如果是[A-z]则会匹配所有英文字母以及Z和a之间的6个标点符号;
小结:[ ]中具有特殊含义的字符:^(只有作为[ ]中的起始符号时有用,表示取补集)、-(只有当其两盘都有值且ASCII值左边比右边小时才有实际意义,表示一个字符范围)、\(表示C语言中的转义字符,因此[ ]中可以识别转移字符)
最新flex的新特性,比如[a-z]{-}[jv],表示前一个字符类减去后一个字符类中的内容,只有最新版本才支持!
^ 放在正则表达式的首部时表示只匹配行首,在[ ]中的开头表示取补集
$ 放在正则表达式的末尾时表示只匹配行尾
{ } 当里面包含两个数字时表示匹配前一个模式的最小和最大次数,比如A{1, 2}表示可以匹配AA或A,而只有一个数字则表示只能匹配多少个紧挨在前面的模式,比如0{5}表示只能匹配00000,如果里面包含的是一个宏名,则表示该宏所代表的模式,类似Shell中的$的作用;
因为在flex程序的第一部分可以定义模式宏,比如:
NAME [a-zA-Z]
则{NAME}就表示NAME宏所代表的模式[a-zA-Z]
"..." 所有引号中的字符将基于它的字面意义,但是转义字符仍然有效,比如"\n"还是表示换行,只有"\\"才表示真正的\符号,建议的良好习惯就是用引号引起所有基于字面意义匹配的字符
( ) 将一些列正则表达式组合成一个新的正则表达式
/ 尾部上下文匹配符号,只匹配斜线前的表达式,但要求其后紧跟着斜线后的表达式,比如0/1来匹配字符串01的时候,会匹配出01中的0单不会匹配02中的0,但是斜线后的内容不会被“消耗掉”,只是匹配该模式的时候会暂时借用一下,用完之后会返还给输入流以便继续匹配下去;
注意!每个模式中最多只能包含一个尾部上下文匹配符号!
<<EOF>> 特殊的模式,只用来匹配文件结束符
8. 关于数字匹配的正则表达式的例子:
[0-9] 单个数字
[0-9]+ 任意位数的数字,可以以0开头
[0-9]* 上述情况在加上空
-?[0-9] 可选一元前置负号
[-+]?[0-9]+ 前置可选正负号,由于[ ]中-作为首位,因此表示其字面意义本身
浮点数的表示:
[-+]?([0-9]*\.[0-9]+|[0-9]+\.) 可表示12./.25/23.323等并且可以带有前导可选正负号,但是不能表示单个.,因此这是一个完备的用单个小数点表示浮点数的正则表达式
但是接下来的这个正则表达式也具有和上述等价的意义:
[-+]?([0-9]*\.[0-9]+|[0-9]+\.[0-9]*)
在匹配诸如3.234等小数点两边都有数字的情况时|负号前后的两个模式都能匹配,因此产生了二义性,但是flex的原则就是如果都能匹配则匹配flex程序中先出现的那个表达式,因此这里会匹配|前面的那个模式
接下来是更一般的带有指数负号E/e的浮点数计数方式:
先定义一个宏REAL [-+]?([0-9]*\.?[0-9]+|[0-9]+\.) ,注意在\.后面还多了一个?,表示小数点可有可无,这样就把整数也算进去了
再为指数部分定义表达式:EXPO [eE][-+]?[0-9]+
因此诸如C语言中的浮点数的常数的具体定义过程可以表示为:
POINT_FLOAT [-+]?([0-9]*\.[0-9]+|[0-9]+\.) // 必须带小数点
REAL [-+]?([0-9]*\.?[0-9]+|[0-9]+\.) // POINT_FLOAT + 整型
EXPO [eE][-+]?[0-9]+ // 必须带有指数部分
DOUBLE {POINT_FLOAT}|{REAL}{EXPO} // 不带有指数部分时必须是一个小数,因此必须是带有小数点的POINT_FLOAT,带有指数部分后,指数前面的部分可以是小数点小数也可以是整数
以上的DOUBLE也是C语言中DOUBLE类型常数定义的完备表示!
这里给出一个模板:
POINT_FLOAT [-+]?([0-9]*\.[0-9]+|[0-9]+\.) REAL [-+]?([0-9]*\.?[0-9]+|[0-9]+\.) EXPO [eE][-+]?[0-9]+ DOUBLE {POINT_FLOAT}|{REAL}{EXPO}
!两个注释的匹配:
#.* Shell注释
\/\/.* C++注释
9. flex处理二义性匹配:
1) 一共有两个原则:贪心原则和顺序原则;
2) 贪心原则:匹配的文本越长越好
3) 顺序原则:如果一段文本对应多个表达式,则匹配源代码中最先出现的那个表达式
4) 例子:
"if" { }
[a-zA-Z] { }
对于ifxxx的匹配,根据贪心原则最长匹配到ifxxx,符合[a-zA-Z]因此匹配上第二个表达式
对于if的匹配,两个表达式都可以,但是if排在前面因此匹配上第一个表达式(顺序原则)
5) 总得来讲顺序原则就是通过规定优先级(越前面出现的优先级越高)来排除二义性,贪心原则则是匹配的长度越长越好!