Flex如何仅在文件开头匹配规则 能否修改默认INITIAL起始条件
问题描述
正在开发脚本语言解释器,需要支持处理(忽略)shebang、UTF BOM这类仅可能出现在文件开头的内容。
核心需求:随着语法规则不断扩充,无法保证后续不会出现能匹配上述前缀内容的规则,为保证程序可靠性,需要实现这类特殊规则仅在文件起始位置生效匹配。
以shebang匹配为例的示例语法可复现当前问题:
词法规则代码(Flex实现):
%% #!.+ { printf("shebang: \"%s\"\n", yytext + 2); } [[:alnum:]_]+ { printf("id: %s\n", yytext); return 1; } #[^#]*# { printf("thingy: \"%s\"\n", yytext); return 2; } [[:space:]] ; . { printf("error: '%c'\n", yytext[0]); } %% int main() { while (yylex()); return 0; } int yywrap() { return 1; }
测试输入文件内容:
#!my-program # some multiline thingy # aaa bbb ccc#!not a shebang#ddd eee
预期输出结果:
shebang: "my-program" thingy: "# some multiline thingy #" id: aaa id: bbb id: ccc thingy: "#!not a shebang#" id: ddd id: eee
实际运行输出:
thingy: "#!my-program #" id: some id: multiline id: thingy thingy: "# aaa bbb ccc#" error: '!' id: not id: a id: shebang error: '#' id: ddd id: eee
现有方案缺陷
尝试使用*start conditions(起始条件)*实现需求,可正常运行的词法规则如下,但实现方式非常不优雅:
%s MAIN %% <INITIAL>#!.+ { printf("shebang: \"%s\"\n", yytext + 2); BEGIN(MAIN); } <INITIAL>""/(?s:.) { BEGIN(MAIN); } [[:alnum:]_]+ { printf("id: %s\n", yytext); return 1; } <MAIN>#[^#]*# { printf("thingy: \"%s\"\n", yytext); return 2; } [[:space:]] ; . { printf("error: '%c'\n", yytext[0]); } %% int main() { while (yylex()); return 0; } int yywrap() { return 1; }
该方案的问题:必须为#[^#]*#规则显式指定MAIN起始条件,否则会和shebang规则#!.+产生匹配冲突。但INITIAL属于包容性起始条件,所有未显式指定状态的规则默认都会在INITIAL状态下生效,必须手动将所有可能产生冲突的规则排除在INITIAL状态外,每次新增规则都需要做这一检查,极易因遗忘引发bug。
核心诉求:是否有方法可以将INITIAL设置为排他性起始条件,或者指定其他起始条件作为词法分析器的默认初始状态?
解决方案
不需要修改INITIAL状态的默认属性,使用排他性起始条件即可零成本解决问题:
- 用
%x定义排他性起始状态(该状态下只有显式标记对应状态的规则会生效,其余普通规则完全不参与匹配),专门用来处理文件开头的特殊内容(shebang、BOM等) - 在词法分析器入口处指定默认进入该特殊状态,匹配完所有开头特殊内容后,将当前读取到的非特殊字符回退到输入流,切换回默认
INITIAL状态即可,后续所有普通规则不需要加任何状态标记,完全不会和开头特殊规则产生冲突。
修正后可直接运行的代码如下:
/* 定义排他性起始状态,专门处理文件头特殊内容 */ %x HEADER_SCAN %option noyywrap %% /* 词法分析器启动时默认进入文件头扫描状态,兼容所有版本Flex/Lex */ BEGIN(HEADER_SCAN); /* 文件头特殊匹配规则,后续新增仅开头生效的规则直接加在这里即可 */ <HEADER_SCAN>^\xEF\xBB\xBF { /* 匹配UTF-8 BOM,直接忽略 */ } <HEADER_SCAN>^#!.+ { printf("shebang: \"%s\"\n", yytext + 2); } <HEADER_SCAN>.|[\n\r] { /* 匹配到任意非文件头特殊字符,回退字符流,切换到正常扫描状态 */ yyless(0); BEGIN(INITIAL); } /* 以下所有普通规则不需要加任何状态标记,仅在INITIAL状态生效,新增规则无需额外做冲突检查 */ [[:alnum:]_]+ { printf("id: %s\n", yytext); return 1; } #[^#]*# { printf("thingy: \"%s\"\n", yytext); return 2; } [[:space:]] ; . { printf("error: '%c'\n", yytext[0]); } %% int main() { while (yylex()); return 0; }
该方案的优势:
- 完全不改动原有普通词法规则的写法,新增规则时不需要额外检查是否和文件头规则冲突
- 文件头特殊规则统一维护,后续需要支持其他仅开头生效的匹配逻辑(比如UTF-16/UTF-32 BOM、自定义文件魔数)只需要在
HEADER_SCAN状态下新增规则即可 - 字符回退逻辑不会丢失输入内容,匹配逻辑和预期完全一致
内容的提问来源于stack exchange,提问作者Piotr Siupa
相关产品推荐
相关产品推荐

