2007年11月28日星期三

lex/yacc系列:lex简介

下面结合一个基本的单词计数程序(类似UNIX程序wc)来看一下lex规范的实际结构。

lex规范由三部分组成:定义段、规则段和用户子例程段,各部分由符号“%%”分隔。

第一部分,定义段,处理lex用于词法分析程序中的选项,并且一般建立词法分析程序运行的执行环境。

单词计数示例的定义段如下:

%{
unsigned charCount = 0, wordCount = 0, lineCount = 0;
%}

word [^ \t\n]+
eol \n

被“%{”和“%}”扩住的部分是C代码,它们将被原封不动地拷贝到词法分析程序中,置于输出代码的靠前的部分,因此这里定义的数据可以由规则段中的代码引用。

定义段的最后两行是模式定义,通过这种简单的替换机制,lex中可以方便地定义长的或复杂的模式。示例中的第一个定义提供了单词描述:除了空格、制表符和换行符之外的字符的非空组合;第二个定义描述行结束字符,即换行。

规则段包含指定词法分析程序的模式和动作。每个规则由两部分组成:模式和动作,由空格分开。当lex生成的词法分析程序识别出某个模式时,将执行相应的动作。模式由正则表达式描述。当匹配模式时,lex拥有一套简单的消除歧义的规则:

  1. lex模式只匹配输入字符或字符串一次
  2. lex总是尝试匹配尽可能长的字符串
  3. 当存在同样长度的字符串匹配时,lex使用先定义的规则

如果输入不匹配任何模式,默认动作是将输入拷贝到输出。如果动作为空,则意味着丢弃被匹配的输入。如果动作仅仅是一条竖线“¦”,意思是与下一规则所使用的动作相同。

下面是单词计数示例的规则段:

%%
{word} { ++wordCount; charCount += yyleng; }
{eol} { ++charCount; ++lineCount; }
. ++charCount;

%%是分隔符,标志规则段的开始。在模式指定中,lex将大括号{}中的名字,用定义段中定义的实际的正则表达式代替。示例中还使用了lex的内部变量yyleng,它表示词法分析程序识别的字符串的程度。

在词法分析程序识别了完整单词时,它就增加单词和字符的数目;当识别一个换行时,就增加字符数和行数;如果识别任意其他字符,就增加字符数。对于这个示例程序,它识别的唯一的“其他字符”是空格或制表符,其他的字符匹配第一个正则表达式,被当作一个单词。(由规则3,如果遇到例如“I”这样的单词,它会由第一个规则匹配,而不是由“.”规则匹配。)

lex规范的第三部分是用户子例程段。该部分包含任何有效的C代码,它们将被逐字拷贝到生成的词法分析程序中。一般来说,这一部分包含支持例程。对于这个示例,“支持”代码是主程序:

%%
int
main(void)
{
 yylex();
 printf("%d %d %d\n", lineCount, wordCount, charCount);
 return 0;
}

%%标志用户子例程段的开始。在主程序中,首先调用词法分析程序的入口yylex(),然后调用printf()输出运行结果。上面的示例不接受命令行参数,不打开任何文件,只是使用lex默认,读取标准输入。下面是重新连接lex的输入流的示例:

%%
int
main(int argc, char *argv[])
{
 if (argc > 1) {
  FILE *file;

  file = fopen(argv[1], "r");
  if (!file) {
   fprintf(stderr, "could not open %s\n", argv[1]);
   exit(1);
  }
  yyin = file;
 }

 yylex();
 printf("%d %d %d\n", lineCount, wordCount, charCount);
 return 0;
}

这个示例假设通过命令行参数传递要处理的文件。lex词法分析程序从标准I/O文件yyin中读取输入,所以当需要时,只需要改变yyinyyin的默认值是stdin,所以默认输入源是标准输入。

传统上,lex源文件的后缀为.l。将上面的三个部分保存为源文件mywc2.l,创建可执行程序mywc2,输入下面的命令:

% lex mywc2.l
% cc lex.yy.c -o mywc2 -ll

lex将lex规范译成C源文件,缺省文件名为lex.yy.c,对它进行编译时需要通过“-ll”链接lex库。

没有评论: