Lex & Yacc 学习笔记(4)- Lex深入学习

一、背景

为了读懂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"); }
... ...

将会在 "INITIAL", "STRING", "QUOTE"三者之一的条件下被激活。

开始条件在定义段被申明,在'%s' 或 '%x'后跟随着名字列表。 %s申明了包含的开始条件,%x申明了排他的开始条件。开始条件被BEGIN动作激活。直到下一个BEGIN动作,满足开始条件名称的规则将会被规则,不满足启动条件的规则将不会被执行。
如果是包含条件,没有开始条件的规则也会被激活执行,如果时排他条件,只有满足开始条件的规则才会被执行。

%s example
%%
foo   do_something();
bar            something_else();

等同于

%x example
%%
foo   do_something();
bar    something_else();

上面的程序中如果没有,在example条件下bar规则将永远不会被激活。如果使用,将会导致只能在exmaple开始条件下激活,而INITIAL条件下不会被激活。而第一个程序中在任何条件下bar都被会激活。因为第一个程序用example时%s,时包含条件。也可以通过特殊开始条件<*>来配置任何开始条件,上面的程序还可以写为:

%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 )

你可能感兴趣的:(Lex & Yacc 学习笔记(4)- Lex深入学习)