下一页 上一页 目录

3. Lex

程序 Lex 生成一个所谓的“词法分析器”。这是一个函数,它接受字符流作为输入,并且每当它看到一组与某个关键字匹配的字符时,就会采取特定的动作。一个非常简单的例子

%{
#include <stdio.h>
%}

%%
stop    printf("Stop command received\n");
start   printf("Start command received\n");
%%

第一个部分,在 %{ 和 %} 对之间的内容会直接包含在输出程序中。我们需要这样做,因为我们稍后会使用 printf,它在 stdio.h 中定义。

节与节之间使用 '%%' 分隔,所以第二个节的第一行以 'stop' 关键字开始。每当在输入中遇到 'stop' 关键字时,该行剩余的部分(一个 printf() 调用)就会被执行。

除了 'stop' 之外,我们还定义了 'start',它在其他方面基本相同。

我们再次用 '%%' 结束代码段。

要编译示例 1,请这样做

lex example1.l
cc lex.yy.c -o example1 -ll

注意:如果您使用的是 flex 而不是 lex,您可能需要在编译脚本中将 '-ll' 更改为 '-lfl'。RedHat 6.x 和 SuSE 需要这样做,即使您以 'lex' 的方式调用 'flex'!

这将生成文件 'example1'。如果您运行它,它会等待您输入一些内容。每当您输入的内容与任何定义的关键字(即 'stop' 和 'start')不匹配时,它会再次输出。如果您输入 'stop',它将输出 'Stop command received';

使用 EOF (^D) 终止。

您可能想知道程序是如何运行的,因为我们没有定义 main() 函数。此函数在 libl (liblex) 中为您定义,我们使用 -ll 命令将其编译链接进来。

3.1 匹配中的正则表达式

这个例子本身并不是很有用,我们的下一个例子也不会很有用。但是,它将展示如何在 Lex 中使用正则表达式,这在以后非常有用。

示例 2

%{
#include <stdio.h>
%}

%%
[0123456789]+           printf("NUMBER\n");
[a-zA-Z][a-zA-Z0-9]*    printf("WORD\n");
%%

这个 Lex 文件描述了两种类型的匹配(标记):WORD 和 NUMBER。正则表达式可能看起来令人生畏,但只要稍加努力,就很容易理解它们。让我们来看看 NUMBER 匹配

[0123456789]+

这表示:来自 0123456789 组的一个或多个字符的序列。我们也可以将其写得更简洁,如下所示

[0-9]+

现在,WORD 匹配稍微复杂一些

[a-zA-Z][a-zA-Z0-9]*

第一部分匹配 1 个且仅 1 个字符,该字符介于 'a' 和 'z' 之间,或介于 'A' 和 'Z' 之间。换句话说,一个字母。这个首字母之后需要跟零个或多个字符,这些字符可以是字母或数字。为什么这里使用星号?'+' 表示 1 个或多个匹配,但 WORD 很可能只包含一个字符,我们已经匹配了它。所以第二部分可能没有匹配项,因此我们写一个 '*'。

这样,我们模拟了许多编程语言的行为,这些语言要求变量名 *必须* 以字母开头,但之后可以包含数字。换句话说,'temperature1' 是一个有效的名称,但 '1temperature' 不是。

尝试编译示例 2,就像示例 1 一样,并给它输入一些文本。这是一个示例会话

$ ./example2
foo
WORD

bar
WORD

123
NUMBER

bar123
WORD

123bar
NUMBER
WORD

您可能也想知道输出中所有这些空白来自哪里。原因很简单:它在输入中,而且我们没有在任何地方匹配它,所以它又被输出了。

Flex 手册详细记录了其正则表达式。许多人认为 perl 正则表达式手册 (perlre) 也非常有用,尽管 Flex 没有实现 perl 的所有功能。

请确保您不要创建零长度匹配,例如 '[0-9]*' - 您的词法分析器可能会感到困惑并开始重复匹配空字符串。

3.2 一个更复杂的 C 类似语法的示例

假设我们要解析一个看起来像这样的文件

logging {
        category lame-servers { null; };
        category cname { null; };
};

zone "." {
        type hint;
        file "/etc/bind/db.root";
};

我们清楚地看到此文件中有许多类别(标记)

相应的 Lex 文件是示例 3

%{
#include <stdio.h>
%}

%%
[a-zA-Z][a-zA-Z0-9]*    printf("WORD ");
[a-zA-Z0-9\/.-]+        printf("FILENAME ");
\"                      printf("QUOTE ");
\{                      printf("OBRACE ");
\}                      printf("EBRACE ");
;                       printf("SEMICOLON ");
\n                      printf("\n");
[ \t]+                  /* ignore whitespace */;
%%

当我们把我们的文件输入到这个 Lex 文件生成的程序中(使用 example3.compile),我们得到

WORD OBRACE 
WORD FILENAME OBRACE WORD SEMICOLON EBRACE SEMICOLON 
WORD WORD OBRACE WORD SEMICOLON EBRACE SEMICOLON 
EBRACE SEMICOLON 

WORD QUOTE FILENAME QUOTE OBRACE 
WORD WORD SEMICOLON 
WORD QUOTE FILENAME QUOTE SEMICOLON 
EBRACE SEMICOLON 

与上面提到的配置文件相比,很明显我们已经整齐地 '标记化' 了它。配置文件的每个部分都已被匹配,并转换为一个标记。

而这正是我们需要充分利用 YACC 的地方。

3.3 我们所看到的

我们已经看到 Lex 能够读取任意输入,并确定输入的每个部分是什么。这被称为 '标记化'。


下一页 上一页 目录