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 的工作流程。
3.6 避坑指南
最后,分享几个我踩过的坑:
- 我曾经忘记在规则里处理换行符,结果整个文件被当成一行处理。调试了半天才发现。
- 我曾经把
.规则放在最前面,结果所有字符都被它匹配了,后面的规则全失效。记住:.是万能匹配,要放在最后。 - 我曾经在动作代码里忘记
returntoken,导致语法分析器永远收不到结束信号。嗯,这种低级错误犯过一次就不会再犯了。
Flex 是个好工具,但前提是你得尊重它的规则。写正则时多想想「如果输入是边界情况会怎样」,能省去很多调试时间。