用于对第一个和第二个字符串(分别)进行词法分析的正则表达式

Regex for lexing first and second string (separately) in a pair

我正在尝试编写一个词法分析器来解析如下所示的文件:

one.html /two/
one/two/ /three
three/four http://five.com

每行有两个字符串,由 space 分隔。我需要创建两个正则表达式模式:一个匹配第一个字符串,另一个匹配第二个字符串。

这是我对词法分析器正则表达式的尝试(一个名为 lexer.l 的文件被 flex 运行):

%%
(\S+)(?:\s+\S+)   { printf("FIRST %s\n", yytext); }
(?:\S+\s+)(\S+)   { printf("SECOND %s\n", yytext); }
.                 { printf("Mystery character %s\n", yytext); }
%%

我在 Regex101 测试仪中测试了 (\S+)(?:\s+\S+)(?:\S+\s+)(\S+),它们似乎都工作正常:https://regex101.com/r/FQTO15/1

但是,当我尝试通过 运行ning flex lexer.l 构建词法分析器时,出现错误:

lexer.l:3: warning, rule cannot be matched

这是指我的第二条规则。如果我试图颠倒规则的顺序,我会再次在第二个规则上遇到错误。如果我只留下其中一条规则,它就可以正常工作。

我认为这个问题与两个正则表达式相似且长度相同的事实有关,所以 flex 认为它是模棱两可的,即使两个正则表达式捕获不同的东西(但它们匹配一样的东西?)。

有什么我可以用正则表达式做的,以便它 capture/match 我想要的而不相互冲突吗?

编辑:更多测试示例

one.html /two/
one/two.html /three/four/
one /two
one/two/ /three
one_two/ /three
one%20two/ /three
one/two/ /three/four
one/two /three/four/five/
one/two.html http://three.four.com/
one/two/index.html http://three.example.com/four/
one http://two.example.com/three
one/two.pdf https://example.com
one/two?query=string /three/four/
go.example.com https://example.com

编辑

事实证明,flex 使用的正则表达式引擎相当有限。它不能进行分组,而且它似乎也没有为 spaces 使用 \s

所以这行不通:

^.*\s.*$

但是这样做:

^.*" ".*$

感谢@fossil 的帮助。

尽管有多种方法可以解决您所说的问题,但我认为您最好了解 (f)lex 的预期用途,并找到与其处理模型一致的解决方案。

(F)lex 旨在将输入拆分为单个标记。每个令牌都有一个类型,并且预计可以通过查看令牌(而不是其上下文)来确定令牌的类型。令牌类型的经典模型是计算机程序中的对象,例如,我们有 identifiersnumbers、某些关键字和各种运营商。给定一组适当的规则,(f)lex 扫描器将接受像

这样的输入
a = b*7 + 2;

并生成令牌流:

标识符 = 标识符 * + ;

这些标记中的每一个都有一个关联的 "semantic value"(并非所有标记都需要),因此两个 identifier 标记和两个 number 不仅仅是匿名 blob。

注意上面一行中的ab有不同的作用。 a 被分配给,而 b 被引用。但这与他们的形式无关,从他们的形式上看不出来。它们只是代币。弄清楚它们的含义以及它们之间的关系是 parser 的作用,它是解析模型的一个独立部分。 two-phase scan/parse 范式的目的是通过抽象化复杂性来简化这两项任务:扫描器对上下文或意义一无所知,而解析器可以推断出输入的逻辑结构而不用关心自己混乱的表示细节和不相关的空白。

在很多方面,您的问题有点超出此范例,部分原因是您拥有的两种标记类型不能仅根据它们的外观来区分。但是,如果它们没有有用的内部结构,那么您可以接受您的输入包含

  • "paths",不包含空格,
  • 换行符。

然后您可以结合使用词法分析器和解析器将输入分成几行:

文件splitter.l

%{
#include "splitter.tab.h"
%}
%option noinput nounput noyywrap nodefault
%%
\n             { return '\n'; }
[^[:space:]]+  { yylval = strdup(yytext); return PATH; }
[[:space:]]    /* Ignore whitespace other than newlines */

文件splitter.y

%code { 
#include <stdio.h>
#include <stdlib.h>

int yylex();
void yyerror(const char* msg);
}

%code requires {
#define YYSTYPE char*
}

%token PATH

%%

lines: %empty
     | lines line '\n'

line : %empty
     | PATH PATH       { printf("Map '%s' to '%s'\n", , );
                         free(); free();
                       }

%%
void yyerror(const char* msg) {
  fprintf(stderr, "%s\n", msg);
}

int main(int argc, char** argv) {
  return yyparse();
}

上面有不少是boiler-plate;只关注语法和标记模式是值得的。

语法很简单:

lines: %empty
     | lines line '\n'

line : %empty
     | PATH PATH       { printf("Map '%s' to '%s'\n", , );
                         free(); free();
                       }

有趣的是最后一行,它说一个 line 由两个 PATH 组成。它通过打印出来处理每一行,尽管您可能想要做一些不同的事情。正是这一行理解一行中的第一个单词和同一行中的第二个单词具有不同的功能。请注意,它不需要词法分析器将这两个词标记为 "FIRST" 和 "SECOND",因为它可以自己看到所有这些:)

free的两次调用释放了词法分析器中strdup分配的内存,从而避免了内存泄漏。在实际应用程序中,您需要确保在不再需要它们之前不要释放这些字符串。

词法分析器模式也非常简单:

\n             { return '\n'; }
[^[:space:]]+  { yylval = strdup(yytext); return PATH; }
[[:space:]]    /* Ignore whitespace other than newlines */

第一个 returns 一个特殊的 single-character 标记,换行符,用于 end-of-line 标记。第二个匹配任何 non-whitespace 字符的字符串。 ((F)lex 不了解 GNU 正则表达式扩展,因此它没有 \s 和朋友。但是,它确实具有更具可读性的 Posix 字符 类,在 flex manual, among other places 中列出。第三个模式跳过任何空格。由于 \n 已由第一个模式处理,因此无法在此处匹配(这就是为什么此模式是单个空白字符而不是重复。)

在第二种模式中,我们给yylval赋值,这是token的语义值。 (我们不会在其他地方这样做,因为换行符不需要语义值。)yylval 始终具有类型 YYSTYPE,我们已将其安排为 char* by a #define。这里,我们只是从yytext开始设置,也就是(f)lex刚刚匹配到的字符串。复制这个字符串很重要,因为 yytext 是词法分析器内部结构的一部分,它的值会在没有警告的情况下改变。复制了字符串后,我们必须确保最终释放内存。

试用此程序:

bison -o splitter.tab.c -d splitter.y
flex -o  splitter.lex.c splitter.l
gcc -Wall -O2 -o splitter splitter.tab.c splitter.lex.c