3、词法分析器生成工具:Lex/Flex使用入门,正则规则编写技巧

说实话,很多同学一听到「词法分析器」就觉得很高大上。其实说白了,它就是做一件事:把源代码的字符流,变成有意义的单词序列。我当年第一次接触 Lex 时,也觉得这东西很神秘。后来在项目中手写过一个简易的词法分析器,那叫一个痛苦。直到用上 Flex,才感叹:早该用工具的

3.1 什么是 Lex / Flex?

Lex 是 Unix 上经典的词法分析器生成工具。Flex 是它的开源增强版,更快、更稳定。你只需要写一个 .l 文件,描述「什么样的字符串对应什么样的 token」,Flex 就能自动生成 C 代码。

我在项目中遇到过这样一个场景:需要解析一种自定义的脚本语言,里面有几十种关键字和运算符。如果手写状态机,光调试就要花一周。用 Flex,半天搞定。嗯,这就是工具的价值。

核心思想:你定义规则,工具生成代码。你只需要关注「规则怎么写」,不用管「怎么匹配」。

3.2 安装与基本使用

Flex 的安装很简单。Linux 上直接 sudo apt install flex。macOS 用 brew install flex。Windows 的话,我建议用 WSL 或者 Cygwin。

一个最简单的 .l 文件长这样:

%{
#include <stdio.h>
%}

%%
[0-9]+      { printf("NUMBER: %s\n", yytext); }
[a-zA-Z]+   { printf("WORD: %s\n", yytext); }
\n          { /* 忽略换行 */ }
.           { /* 忽略其他字符 */ }
%%

int main() {
    yylex();
    return 0;
}

编译命令:

flex example.l
gcc lex.yy.c -lfl -o example
./example

你输入 hello 123,它会输出:

WORD: hello
NUMBER: 123

是不是很简单?但别急,真正的坑在后面

3.3 正则规则编写技巧

写正则规则,是 Flex 使用的核心。我总结了几个实战中特别有用的技巧。

3.3.1 优先级:谁先匹配谁说了算

Flex 的匹配规则很简单:最长匹配优先,相同长度则靠前的规则优先。你想想看,如果关键字 if 和标识符规则冲突了怎么办?

我的做法是:把关键字规则写在标识符规则前面

"if"       { return IF; }
"else"     { return ELSE; }
[a-zA-Z]+  { return IDENTIFIER; }

这样 if 会被识别为关键字,而不是标识符。我曾经在项目中因为顺序搞反,调试了整整两个小时。嗯,血的教训。

3.3.2 灵活运用开始状态

有些语言有上下文相关的词法规则。比如 C 语言的注释 /* ... */,或者字符串中的转义字符。这时候就需要「开始状态」。

%x COMMENT

%%
"/*"            { BEGIN(COMMENT); }
<COMMENT>"*/"   { BEGIN(INITIAL); }
<COMMENT>.      { /* 忽略注释内容 */ }
<COMMENT>\n     { /* 忽略换行 */ }

我个人习惯把开始状态命名为大写,这样一眼就能看出来是状态名。这个技巧在处理多行字符串时特别有用。

3.3.3 正则中的常见陷阱

陷阱 错误写法 正确写法
匹配浮点数 [0-9]+.[0-9]+ [0-9]+\.[0-9]+
匹配转义字符 "\n" \\n
匹配空白 " " [ \t]+

为什么浮点数那个容易错?因为 . 在正则里是「匹配任意字符」。不加反斜杠,123x456 也会被匹配上。我刚开始学的时候就被这个坑过。

注意:Flex 的正则语法和 PCRE 不完全一样。比如不支持 \d\w 这些简写。你得用 [0-9][a-zA-Z0-9_]

3.4 实战:一个简易计算器的词法分析

光说不练假把式。我们写一个能识别整数、浮点数、加减乘除和括号的词法分析器。

%{
#include <stdio.h>
#include "calc.tab.h"  /* 语法分析器生成的 token 定义 */
%}

%%
[0-9]+          { yylval.num = atoi(yytext); return NUMBER; }
[0-9]+\.[0-9]+  { yylval.fnum = atof(yytext); return FNUMBER; }
"+"             { return PLUS; }
"-"             { return MINUS; }
"*"             { return TIMES; }
"/"             { return DIVIDE; }
"("             { return LPAREN; }
")"             { return RPAREN; }
[ \t]           { /* 忽略空白 */ }
\n              { return NEWLINE; }
.               { printf("非法字符: %s\n", yytext); }
%%

这里有个细节:整数规则要写在浮点数前面吗? 不用。因为 Flex 会优先匹配最长的。输入 3.14 时,[0-9]+\.[0-9]+ 匹配 4 个字符,而 [0-9]+ 只匹配 1 个。所以浮点数规则会自动胜出。

小技巧:如果你想让整数规则优先,可以加一个「后面不是小数点」的预查。但 Flex 不支持预查。所以老老实实让最长匹配机制来工作就好。

3.5 知识体系图

下面这张图,是我对本章核心逻辑的总结。你看一眼就能明白 Flex 的工作流程。

Flex 词法分析器工作流程 源代码字符流 .l 文件:正则规则 + 动作代码 Flex 生成 lex.yy.c Token 序列 → 语法分析器 核心要点 • 最长匹配优先 • 相同长度靠前优先 • 开始状态处理上下文 • 正则中 . 要转义 • 关键字规则放前面 • 空白字符要显式忽略 • yytext 保存当前匹配 • yylval 传递语义值

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 我曾经忘记在规则里处理换行符,结果整个文件被当成一行处理。调试了半天才发现。
  • 我曾经. 规则放在最前面,结果所有字符都被它匹配了,后面的规则全失效。记住:. 是万能匹配,要放在最后。
  • 我曾经在动作代码里忘记 return token,导致语法分析器永远收不到结束信号。嗯,这种低级错误犯过一次就不会再犯了。

Flex 是个好工具,但前提是你得尊重它的规则。写正则时多想想「如果输入是边界情况会怎样」,能省去很多调试时间。