lex 是一个经典的词法分析器生成器,它的主要作用是根据用户定义的词法规则(也就是描述“单词”或“记号”的模式),自动生成一个名为 lex.yy.c 的 C 语言源代码文件,这个 C 文件包含了能够从输入流中识别出这些“单词”并执行相应动作的代码。

生成的 lex.yy.c 文件通常需要与一个语法分析器(最常见的是由 yacc 或 bison 生成的)配合使用,来完成整个编译或解释过程。
lex 的工作流程
lex 的工作流程可以分为以下四个步骤:
-
创建
.l文件: 用户编写一个以.l或.L为后缀的源文件,这个文件被称为lex源文件,它包含了词法规则和对应的动作代码。 -
运行
lex命令: 在命令行中,对.l文件运行lex命令。
(图片来源网络,侵删)lex your_file.l
lex编译器会读取.l文件,并生成一个名为lex.yy.c的 C 语言文件,这个文件包含了一个名为yylex()的函数,这个函数就是词法分析器的核心。 -
编译并链接: 将
lex.yy.c文件与 C 编译器(如gcc)一起编译,你还需要链接lex或flex的运行时库(-ll)。gcc lex.yy.c -o my_program -ll
-o my_program:指定输出的可执行文件名为my_program。-ll:链接lex的库文件。注意: 这个-ll必须放在源文件之后,否则可能会导致链接错误。
-
运行程序: 执行编译好的程序,程序会从标准输入(通常是你的键盘)读取文本,
yylex()函数会开始工作,不断地匹配规则并执行动作。
.l 文件的结构
一个 lex 源文件由三个主要部分组成,它们之间由 分隔。

/* 第一部分:定义和声明 */
%{
/* C 代码块 */
#include <stdio.h>
int my_variable = 0;
%}
/* 正则表达式定义 */
DIGIT [0-9]
ID [a-zA-Z][a-zA-Z0-9]*
%%
/* 第二部分:规则 */
/* 格式: <正则表达式> { <C 代码动作> } */
{DIGIT}+ {
printf("Found an integer: %s\n", yytext);
/* yytext 是一个全局变量,指向当前匹配的文本 */
}
{ID} {
printf("Found an identifier: %s\n", yytext);
}
"if" {
printf("Found a keyword: 'if'\n");
}
[ \t\n] {
/* 忽略空白字符(空格、制表符、换行) */
}
. {
/* 匹配任何其他单个字符,并打印警告 */
printf("Unknown character: %s\n", yytext);
}
%%
/* 第三部分:C 语言代码 */
/* 这里的代码会被原样复制到 lex.yy.c 文件的末尾 */
int main() {
printf("Starting lexical analyzer...\n");
yylex(); /* 调用词法分析器 */
printf("Analysis finished.\n");
return 0;
}
各部分详解:
第一部分:定义和声明
- 双百分号包裹的区域,这里的任何代码都会被原封不动地复制到生成的
lex.yy.c文件中,通常用于包含头文件(如stdio.h)、定义全局变量或函数原型。 - 正则表达式定义:在这里可以定义简单的正则表达式,以便在规则部分重复使用,使代码更清晰。
DIGIT定义了[0-9],ID定义了标识符的模式。
第二部分:规则
这是 lex 文件的核心,每一行规则由两部分组成:
- 正则表达式:描述要匹配的模式。
- 动作:用花括号 包裹的 C 语言代码,当输入文本与正则表达式匹配时,就会执行这部分代码。
yytext:这是一个非常重要的全局字符数组,它包含了当前匹配到的字符串。yyleng:一个全局整型变量,表示当前匹配到的字符串的长度。yylex():lex生成的词法分析器函数,它会持续从输入读取字符,尝试与规则进行匹配,一旦找到最长的匹配项,就会执行对应的动作,然后返回。
匹配优先级:
lex总是尝试匹配最长的可能字符串。- 如果有多个规则匹配了相同长度的字符串,那么在
.l文件中写在最前面的规则优先。
第三部分:C 语言代码
同样会被原样复制到 lex.yy.c 文件的末尾,通常用于放置 main() 函数或其他辅助函数。
一个完整的示例
让我们创建一个简单的计算器词法分析器,它能识别整数、加号、减号和换行符。
文件名:calculator.l
%{
#include <stdio.h>
%}
/* 定义数字和操作符 */
DIGIT [0-9]
PLUS \+
MINUS \-
NEWLINE \n
%%
{DIGIT}+ {
printf("Integer: %s\n", yytext);
}
{PLUS} {
printf("Operator: PLUS\n");
}
{MINUS} {
printf("Operator: MINUS\n");
}
{NEWLINE} {
/* 忽略换行符,或者用它来分隔表达式 */
/* printf("End of line.\n"); */
}
[ \t] {
/* 忽略空格和制表符 */
}
. {
printf("Unknown character: %s\n", yytext);
}
%%
int main() {
printf("Enter an expression (e.g., 123+456-789):\n");
yylex(); /* 开始词法分析 */
return 0;
}
编译和运行:
-
生成 C 代码
lex calculator.l
这会生成
lex.yy.c。 -
编译并链接
gcc lex.yy.c -o calculator -ll
- 注意
-ll的位置。
- 注意
-
运行程序
./calculator
测试运行:
$ ./calculator
Enter an expression (e.g., 123+456-789):
100 + 200 - 50
Integer: 100
Operator: PLUS
Integer: 200
Operator: MINUS
Integer: 50
^D (按下 Ctrl+D 表示文件结束)
lex 的现代替代品:flex
在现代的 Linux/Unix 系统中,你几乎不会直接使用 lex 命令,而是使用它的一个增强版替代品——flex (Fast Lexical Analyzer Generator)。
- 兼容性:
flex是lex的超集,绝大多数为lex编写的代码都可以在flex下直接编译和运行。 - 功能增强:
flex提供了更多功能,比如更好的错误信息、REJECT 选项、开始状态等。 - 默认安装:在大多数现代发行版中,
flex是默认安装的,而lex可能只是一个指向flex的符号链接。
在实际使用中,你可能会这样操作:
# 使用 flex 代替 lex flex your_file.l # 剩下的编译步骤完全一样 gcc lex.yy.c -o my_program -ll
lex 与 yacc/bison 的协作
lex 只负责词法分析,它识别出一个个的“记号”(Token),INTEGER, PLUS, IDENTIFIER 等,但它不理解这些记号组合起来的语法结构(IF 后面必须跟着一个表达式)。
这就需要语法分析器(如 yacc 或 bison)来上场。
lex的任务:从输入流中提取记号,并将它们“喂”给语法分析器,当lex匹配到一个记号时,它会返回一个代表该记号的整数值(Token ID)。yacc/bison的任务:接收这些记号,并根据用户定义的语法规则来判断输入的记号流是否符合语法结构,如果符合,就执行相应的语义动作(比如构建抽象语法树 AST 或直接计算结果)。
协作流程:
- 你用
lex写.l文件,定义所有记号的模式。 - 你用
yacc或bison写.y文件,定义语法规则和记号的整数值。 lex生成的yylex()函数在匹配到规则后,不是执行打印等操作,而是返回一个记号 ID 给yacc/bison。yacc/bison生成的yyparse()函数会调用yylex()来获取下一个记号,并根据语法规则进行解析。
这是一个编译器前端的标准工作模式。
| 特性 | 描述 |
|---|---|
| 全称 | Lexical Analyzer Generator |
| 作用 | 根据词法规则,自动生成 C 语言的词法分析器代码 (lex.yy.c)。 |
| 输入文件 | .l 文件,包含定义、规则和 C 代码。 |
| 核心函数 | yylex(),由 lex 生成,负责匹配输入和执行动作。 |
| 关键变量 | yytext (匹配的文本), yyleng (文本长度)。 |
| 编译命令 | lex file.l 生成 C 代码,gcc lex.yy.c -ll 编译链接。 |
| 现代替代品 | flex (Fast Lexical Analyzer),功能更强,是事实上的标准。 |
| 协作工具 | 通常与 yacc 或 bison (语法分析器生成器) 配合使用,共同构成编译器前端。 |
