一、背景
为了读懂postgresql的语法分析和更好的使用 Lex ,所以继续学习lex的进阶部分。
Lex & Yacc 学习笔记(2)- 简单计算器 中介绍了Lex的结构规范
Lex & Yacc 学习笔记(3)- 正则表达式语法介绍了Lex的正则表达式部分。
.l文件 的结构
Definition section(定义段)
%%
Rules section(规则段)
%%
C code section (用户子程序段)
下面以一个单词统计程序详细说明。
二、Definition Section(定义段)
这块可以放C语言的各种各种include,define等声明语句,但是要用%{ %}括起来。
可以放预定义的正则表达式:minus “-” 还要放token的定义,方法是:代号 正则表达式。然后到了规则段就可以通过{代号} 来引用正则表达式
%{
#include // include
int wordCount = 0; // 定义全局变量 单词计数器
%}
// 预定义代号表
chars [A-za-z\_\'\.\"]
numbers ([0-9])+
delim [" "\n\t]
whitespace {delim}+ // 使用前面的代号 来 定义代号
words {chars}+ // 使用前面的代号 来 定义代号
%%
三、Rules section(规则段)
在这里放置的rules就是每个正则表达式要对应的动作,一般是返回一个token
这里的动作都是用{}扩起来的,用C语言来描述,这些代码可以做你任何想要做的事情
{words} { wordCount++; /* wordCount 加1 */ }
{whitespace} { /* 空白,什么也不做*/ }
{numbers} { /* 这里可以加入如果遇到数字的处理逻辑*/ }
%%
四、C code section (用户子程序段)
Lex 编程的第三段,也就是最后一段覆盖了 C 的函数声明(有时是主函数)。注意这一段必须包括 yywrap() 函数。 Lex 有一套可供使用的函数和变量。 其中之一就是 yywrap。
/* main函数 */
void main()
{
yylex(); //这一函数开始分析。 它由 Lex 自动生成。
printf(" No of words: %d\n", wordCount);
}
/*这一函数在文件(或输入)的末尾调用。 如果函数的返回值是1,就停止解析。
因此它可以用来解析多个文件。 代码可以写在第三段,这就能够解析多个文件。
方法是使用 yyin 文件指针指向不同的文件,直到所有的文件都被解析。
最后,yywrap() 可以返回 1 来表示解析的结束。*/
int yywrap()
{
return 1;
}
五、编译运行
[appusr@postgre wordCount]$ flex wordcount.l
[appusr@postgre wordCount]$ gcc -o wordcount lex.yy.c -lfl
[appusr@postgre wordCount]$ ./wordcount
[appusr@postgre wordCount]$ ./wordcount < text.txt #运行程序,将输入流重定向为要分析的文件
六、常用的全局变量和宏
lex.yy.c中,有很多全局变量、函数、宏。这里之处部分最常用的
FILE *yyin:
FILE *yyout: 这是Lex中本身已定义的输入和输出文件指针。这两个变量指明了lex生成的词法分析器从哪里获得输入和输出到哪里。默认:键盘输入,屏幕输出。char *yytext:指向当前识别的词法单元(词文)的指针
int yyleng:当前词法单元的长度。
yylineno 提供当前的行数信息。(lexer不一定支持。)
ECHO:Lex中预定义的宏,可以出现在动作中,相当于fprintf(yyout, “%s”,yytext),即输出当前匹配的词法单元。
REJECT: 指示简析器对当前规则不做处理,而是采用第二匹配规则。
因为解析器在通常情况下,每个被匹配的对象只会对一个动作生效。REJECT指示解析器,会寻找下一个最配的规则来做处理。下面的规则会把输入的"abcd"处理后输出"abcd|abc|ab|a|abcd"。a | ab | abc | abcd {ECHO;printf("|");REJECT;} %%
BEGIN 开始一个条件处理块
int yylex():词法分析器驱动程序,用Lex翻译器生成的lex.yy.c内必然含有这个函数。它自动移动文件指针
yyin 和 yyout 。
在定义模式动作时,可以用 return 语句来结束 yylex分析函数。return 需要返回一个整数。
由于 yylex() 函数的运行环境都是以全局变量的方式来保存,因此在下一次调用 yylex() 时,yylex()可以从上次扫描的断点处继续扫描。若用户未定义相应的return语句,则yylex()继续分析被扫描的文件,直到碰到文件结束标识符EOF。
在读取到EOF时,yylex() 函数调用 int yywrap() 函数,若yywrap()返回非0值,则yylex() 函数结束返回0;否则,yylex()继续对yyin指向的文件扫描。int yywrap():词法分析器遇到文件结尾时会调用yywrap()来决定下一步怎么做:
若yywrap()返回0,则继续扫描
若返回1,则返回报告文件结尾的0标记。
由于词法分析器总会调用yywrap,因此辅助函数中最好提供yywrap,如果不提供,则在用C编译器编译lex.yy.c时,需要链接相应的库,库中会给出标准的yywrap函数(标准函数返回1)。yymore()这一函数告诉 Lexer 将下一个标记附加到当前标记后。
`yymore()' 告诉解析器下一次匹配的规则,满足的部分将会添加到当前yytext值得后面而不是替换它。 例如,指定的输入"mega-kludge"经过下面的程序处理后将会输出"mega-mega-kludge"。%% mega- ECHO; yymore(); kludge ECHO;/* 这时,yytext的值为mega-kludge */
yyless(int n) 返回当前匹配项除了开始的n个字符内的所有的内容到输入缓存区,解析器处理下一个匹配时,它们将会被重新解析。yyless将会导致yytext与yyleng的调整。(yyleng将会等于=n) 如输入"createtable"被下面的程序处理后,将会输出"createtableatetable". 因为前n=3个字符foo外的字符atetable被重新返回到输入缓存区,再次解析。
createtable ECHO; yyless(3); [a-zA-Z]+ ECHO; \n return 0; %%
unput(c) 将字符c放回到输入流中,该字符可以重新被解析。下面的动作将当前的匹配值附上括号后重新进行匹配。
{ int i; /* Copy yytext because unput() trashes yytext */ char *yycopy = strdup( yytext ); unput( ')' ); for ( i = yyleng - 1; i >= 0; --i ) unput( yycopy[i] ); unput( '(' ); free( yycopy ); }
注意: 由于每次unput()将指定的字符添加到输入源的开头,所以将字符串添加到输入源开头必须从后道前处理。一个比较重要的潜在问题是使用unput()的时候,如果采用了%pointer指针模式保存yytext,unput会破坏yytext的内容,从最右边的字符开始将会破坏左边的一个字符。如果在unput()后要用到yytext,你首先必须复制一份yytext,或者用%array模式来保存yytext. 最后不能尝试放一个EOF标志输入流的结束。
input() 从输入源中读取下一个字符。
下面例子将会吃掉C语言注释%% "/*" { register int c; for ( ; ; ) { while ( (c = input()) != '*' &&>c != EOF ); /* eat up text of comment */ if ( c == '*' ) { while ( (c = input()) == '*' ); if ( c == '/' ) break; /* found the end */ } if ( c == EOF ) { error( "EOF in comment" ); break; } } }
yyterminate() 可以在动作内部返回描述区域中使用,它将终止解析器并返回0给解析器调用者,表示操作完成。缺省情况下,到达文件结束位置也会被调用,它是一个宏,并且可能重定义。
七、条件模式
LEX提供控制模式在一定状态下使用的功能,称为条件模式。LEX首先在定义部份通过 "%start/x/s 条件名" 来定义条件句。在规则部份可通过宏 "BEGIN(条件名)" 来激活条件。"BEGIN(INITIAL)" 或 "BEGIN(0)" 将休眠所有的条件模式,使分析器回到开始状态。
下面是postgresql里面的一段LEX代码,解析SQL里面的comments
... ...
op_chars [\~\!\@\#\^\&\|\`\?\+\-\*\/\%\<\>\=]
... ...
/* C-style comments
*
* The "extended comment" syntax closely resembles allowable operator syntax.
* The tricky part here is to get lex to recognize a string starting with
* slash-star as a comment, when interpreting it as an operator would produce
* a longer match --- remember lex will prefer a longer match! Also, if we
* have something like plus-slash-star, lex will think this is a 3-character
* operator whereas we want to see it as a + operator and a comment start.
* The solution is two-fold:
* 1. append {op_chars}* to xcstart so that it matches as much text as
* {operator} would. Then the tie-breaker (first matching rule of same
* length) ensures xcstart wins. We put back the extra stuff with yyless()
* in case it contains a star-slash that should terminate the comment.
* 2. In the operator rule, check for slash-star within the operator, and
* if found throw it back with yyless(). This handles the plus-slash-star
* problem.
* Dash-dash comments have similar interactions with the operator rule.
*/
xcstart \/\*{op_chars}*
xcstop \*+\/
xcinside [^*/]+
... ...
%%
... ...
{xcstart} {
/* Set location in case of syntax error in comment */
SET_YYLLOC();
yyextra->xcdepth = 0;
BEGIN(xc);
/* Put back any characters past slash-star; see above */
yyless(2);
}
{xcstart} {
(yyextra->xcdepth)++;
/* Put back any characters past slash-star; see above */
yyless(2);
}
{xcstop} {
if (yyextra->xcdepth <= 0)
BEGIN(INITIAL);
else
(yyextra->xcdepth)--;
}
{xcinside} {
/* ignore */
}
{op_chars} {
/* ignore */
}
\*+ {
/* ignore */
}
<> { yyerror("unterminated /* comment"); }
... ...
开始条件在定义段被申明,在'%s' 或 '%x'后跟随着名字列表。 %s申明了包含的开始条件,%x申明了排他的开始条件。开始条件被BEGIN动作激活。直到下一个BEGIN动作,满足开始条件名称的规则将会被规则,不满足启动条件的规则将不会被执行。
如果是包含条件,没有开始条件的规则也会被激活执行,如果时排他条件,只有满足开始条件的规则才会被执行。
%s example
%%
foo do_something();
bar something_else();
等同于
%x example
%%
foo do_something();
bar something_else();
上面的程序中如果没有
%x example
%%
foo do_something();
<*>bar something_else();
YY_START 开始条件的名字实际上时一个整形值并且能够被保存,可以使用 YY_START 宏来访问当前的开始条件。YYSTATE 是 YY_START 的别名(AT&T lex使用了YYSTATE)。
下面的代码能够是被C语言注释并且统计行数。
%x comment foo
%%
int line_num = 1;
int comment_caller;
"/*" {
comment_caller = INITIAL;
BEGIN(comment);
}
...
"/*" {
comment_caller = foo; /* comment_caller = foo ==》comment_caller = YY_START */
BEGIN(comment);
}
[^*\n]* /* eat anything that's not a '*' */
"*"+[^*/]* /* eat up '*'s not followed by '/'s */
\n ++line_num;
"*"+"/" BEGIN(comment_caller);
开始条件范围
前面条件模式的代码中,会有许多相同开始条件的处理。使用开始条件范围可以简化重复操作。
{
}
SCs 是一个或开始条件的列表。在这个开始条件范围内,每个规则将会自动具有前缀 '
{
[^*]* /* eat anything that's not a '*' */
"*"+[^*/]* /* eat up '*'s not followed by '/'s */
\n ++line_num;
"*"+"/" BEGIN(comment_caller);
}
等价于
[^*]* /* eat anything that's not a '*' */
"*"+[^*/]* /* eat up '*'s not followed by '/'s */
\n ++line_num;
"*"+"/" BEGIN(comment_caller);
条件嵌套
开始条件也可以嵌套,下面时三个管理开始条件堆栈的参数。
void yy_push_state(int new_state) 将当前的开始条件压栈,切换到 new_state 与使用 'BEGIN new_state'类似。
void yy_pop_state() 从栈顶弹出,类似于 BEGIN.
int yy_top_state() 返回栈顶值,不改变栈内容。
开始条件栈动态增长,没有固定限制,如果内存用尽,程序自会终止。
八、多输入缓存区(未完成)
YY_BUFFER_STATE yy_create_buffer( FILE *file, int size )
void yy_switch_to_buffer( YY_BUFFER_STATE new_buffer )
void yy_delete_buffer( YY_BUFFER_STATE buffer )
void yy_flush_buffer( YY_BUFFER_STATE buffer )