Flex 查找子字符串直到字符
Flex find substring until character
这是我的 lexer.l
文件:
%{
#include "../h/Tokens.h"
%}
%option yylineno
%%
[+-]?([1-9]*\.[0-9]+)([eE][+-]?[0-9])? return FLOAT;
[+-]?[1-9]([eE][+-]?[0-9])? return INTEGER;
\"(\\|\\"|[^\"])*\" return STRING;
(true|false) return BOOLEAN;
(func|val|if|else|while|for)* return KEYWORD;
[A-Za-z_][A-Za-z_0-9]* return IDENTIFIER;
"+" return PLUS;
"-" return MINUS;
"*" return MULTI;
"." return DOT;
"," return COMMA;
":" return COLON;
";" return SEMICOLON;
. printf("Unexpected or invalid token: '%s'\n", yytext);
%%
int yywrap(void)
{
return 1;
}
现在,如果我的词法分析器发现意外标记,它会为每个字符发送一个错误。我希望它为每个子字符串发送一条错误消息,直到出现空格或运算符。
示例:
Input:
foo bar baz
~±`≥ hello
Output:
Identifier.
Identifier.
Identifier.
Unexpected or invalid token: '~±`≥'
Identifier.
有没有办法使用正则表达式模式来做到这一点?
谢谢。
当然可以使用正则表达式。但是您不能使用独立于其他令牌规则的正则表达式来做到这一点。找到正确的正则表达式可能并非易事。
不过,在这个相当简单的例子中,它相当简单,尽管有一个极端情况。由于没有多字符运算符,因此字符不能作为标记的起始字符,除非它是字母、数字、运算符之一 (-+*.,:;
) 或双引号。因此,此类字符的任何序列都是无效序列。另外,我认为你真的想忽略空白字符(基于示例输出),即使你的问题没有显示任何匹配空白的规则。所以假设你只是遗漏了空白规则,就像
[[:space:]]+ { /* Ignore whitespace */ }
匹配非法字符序列的正则表达式是
[^-+*.,:;[:alnum:][:space:]]+ { fprintf(stderr, "Invalid sequence %s\m", yytext); }
极端情况是一个未终止的字符串文字;也就是说,以 "
开头但不包含匹配的结束引号的标记。这样的标记必须延伸到输入的末尾,并且可以使用您的字符串模式轻松匹配它,省去最后的 "
。 (这是有效的,因为 (f)lex 总是使用最长的匹配模式,所以如果有终止 "
正确的字符串文字将被匹配。)
您的模式中存在一些错误:
在数字文字的开头匹配 +-
几乎总是一个坏主意。如果你这样做,那么 x+2
将不会被正确分析;您的词法分析器将 return 两个标记,一个 IDENTIFIER
和一个 INTEGER
,而不是正确的三个标记(IDENTIFIER
、PLUS
、INTEGER
) .
您的 FLOAT
模式不接受小数点前包含 0 的数字,因此 0.5
和 10.3
都将失败。此外,您强制指数为一位数,因此 1.3E11
也不会匹配。而你强制用户在小数点后放一个数字;大多数语言接受 3.
等同于 3.0
。 (最后一个不一定是错误,但它是非常规的。)
您的 INTEGER
模式不接受包含 0 的数字,例如 10
。但是它会接受科学记数法,这有点奇怪;在大多数语言中 3E10
是浮点常量,而不是整数。
您的 KEYWORD
模式接受由一系列串联的单词组成的关键字,例如 forwhilefuncif
。您可能不打算在模式末尾放置 *
。
您的字符串文字模式允许 "
以外的任何字符序列,这意味着反斜杠 \
将被允许作为单个字符进行匹配,即使它是后跟引号或反斜杠。这将导致一些字符串文字没有被正确终止。例如,给定字符串文字
"\"
(这是一个包含单个反斜杠的字符串文字),正则表达式将匹配初始的 "
,然后是 \
作为单个字符,然后是 \"
序列, 然后是字符串文字后面的任何内容,直到遇到另一个引号。
错误是 flex 要求 \
在方括号表达式内转义的结果,不像 Posix 正则表达式,其中 \
在方括号内失去特殊意义。
所以这会给你留下这样的东西:
%{
#include "../h/Tokens.h"
%}
%option yylineno noyywrap
%%
[[:space:]]+ /* Ignore whitespace */
(\.[0-9]+|[0-9]+\.[0-9]*)([eE][+-]?[0-9]+)? {
return FLOAT;
}
0|[1-9][0-9]* return INTEGER;
true|false return BOOLEAN;
func|val|if|else|while|for return KEYWORD;
[A-Za-z_][A-Za-z_0-9]* return IDENTIFIER;
"+" return PLUS;
"-" return MINUS;
"*" return MULTI;
"." return DOT;
"," return COMMA;
":" return COLON;
";" return SEMICOLON;
\"(\\|\\"|[^\"])*\" return STRING;
\"(\\|\\"|[^\"])* { fprintf(stderr,
"Unterminated string literal\n"); }
[^-+*.,:;[:alnum:][:space:]]+ { fprintf(stderr,
"Invalid sequence %s\m", yytext); }
(如果这些模式中的任何一个看起来很神秘,您可能需要查看 the description of flex patterns in the flex manual。)
但我觉得您正在寻找不同的东西:一种无需过多分析即可神奇地适应代币模式的任何变化的方法。
这也是可能的,但我不知道如何在不重复代码的情况下做到这一点。基本思想很简单:当我们遇到一个不匹配的字符时,我们只是将它附加到错误标记的末尾,当我们找到一个有效的标记时,我们发出错误消息并清除错误标记。
问题出在“当我们找到有效标记时”部分,因为这意味着我们需要在除错误规则之外的每个规则的开头插入一个操作。最简单的方法是使用宏,这样至少可以避免为每个动作都写出代码。
(F)lex 确实为我们提供了一些有用的工具,我们可以在此基础上构建它。我们将使用 (f)lex 的特殊操作之一,yymore()
,它会导致当前匹配附加到正在构建的标记,这对于构建错误标记很有用。
为了知道错误标记的长度(从而知道是否有错误标记),我们需要一个额外的变量。幸运的是,(f)lex 允许我们 define our own local variables inside the scanner。然后我们定义宏 E_
(其名称被选择为短,以避免混淆规则操作),它打印错误消息,将 yytext
移动到错误标记上,并重置错误计数。
放在一起:
%{
#include "../h/Tokens.h"
%}
%option yylineno noyywrap
%%
int nerrors = 0; /* To keep track of the length of the error token */
/* This macro must be inserted at the beginning of every rule,
* except the fallback error rule.
*/
#define E_ \
if (nerrors > 0) { \
fprintf(stderr, "Invalid sequence %.*s\n", nerrors, yytext); \
yytext += nerrors; yyleng -= nerrors; nerrors = 0; \
} else /* Absorb the following semicolon */
[[:space:]]+ { E_; /* Ignore whitespace */ }
(\.[0-9]+|[0-9]+\.[0-9]*)([eE][+-]?[0-9]+)? { E_; return FLOAT; }
0|[1-9][0-9]* { E_; return INTEGER; }
true|false { E_; return BOOLEAN; }
func|val|if|else|while|for { E_; return KEYWORD; }
[A-Za-z_][A-Za-z_0-9]* { E_; return IDENTIFIER; }
"+" { E_; return PLUS; }
"-" { E_; return MINUS; }
"*" { E_; return MULTI; }
"." { E_; return DOT; }
"," { E_; return COMMA; }
":" { E_; return COLON; }
";" { E_; return SEMICOLON; }
\"(\\|\\"|[^\"])*\" { E_; return STRING; }
\"(\\|\\"|[^\"])* { E_;
fprintf(stderr,
"Unterminated string literal\n"); }
. { yymore(); ++nerror; }
这一切都假设我们乐于在扫描仪中生成一条错误消息,否则忽略错误字符。但实际 return 错误指示并让调用者决定如何处理错误可能会更好。这引入了一个额外的问题,因为它要求我们在一个动作中 return 两个标记。
对于一个简单的解决方案,我们使用另一个 (f)lex 特性,yyless()
,它允许我们重新扫描部分或全部当前标记。我们可以使用它从当前标记中删除错误标记,而不是调整 yytext 和 yyleng。 (yyless
将为我们进行调整。)这意味着在出现错误后,下一个正确的令牌将被扫描两次。这可能看起来效率低下,但它可能是可以接受的,因为:
- 大多数令牌都很短,
- 针对错误进行优化并没有多大意义。优化正确输入的处理更有用。
为此,我们只需要对 E_
宏做一点小改动:
#define E_ \
if (nerrors > 0) { \
yyless(nerrors); \
fprintf(stderr, "Invalid sequence %s\n", yytext); \
nerrors = 0; \
return BAD_INPUT; \
} else /* Absorb the following semicolon */
这是我的 lexer.l
文件:
%{
#include "../h/Tokens.h"
%}
%option yylineno
%%
[+-]?([1-9]*\.[0-9]+)([eE][+-]?[0-9])? return FLOAT;
[+-]?[1-9]([eE][+-]?[0-9])? return INTEGER;
\"(\\|\\"|[^\"])*\" return STRING;
(true|false) return BOOLEAN;
(func|val|if|else|while|for)* return KEYWORD;
[A-Za-z_][A-Za-z_0-9]* return IDENTIFIER;
"+" return PLUS;
"-" return MINUS;
"*" return MULTI;
"." return DOT;
"," return COMMA;
":" return COLON;
";" return SEMICOLON;
. printf("Unexpected or invalid token: '%s'\n", yytext);
%%
int yywrap(void)
{
return 1;
}
现在,如果我的词法分析器发现意外标记,它会为每个字符发送一个错误。我希望它为每个子字符串发送一条错误消息,直到出现空格或运算符。
示例:
Input:
foo bar baz
~±`≥ hello
Output:
Identifier.
Identifier.
Identifier.
Unexpected or invalid token: '~±`≥'
Identifier.
有没有办法使用正则表达式模式来做到这一点?
谢谢。
当然可以使用正则表达式。但是您不能使用独立于其他令牌规则的正则表达式来做到这一点。找到正确的正则表达式可能并非易事。
不过,在这个相当简单的例子中,它相当简单,尽管有一个极端情况。由于没有多字符运算符,因此字符不能作为标记的起始字符,除非它是字母、数字、运算符之一 (-+*.,:;
) 或双引号。因此,此类字符的任何序列都是无效序列。另外,我认为你真的想忽略空白字符(基于示例输出),即使你的问题没有显示任何匹配空白的规则。所以假设你只是遗漏了空白规则,就像
[[:space:]]+ { /* Ignore whitespace */ }
匹配非法字符序列的正则表达式是
[^-+*.,:;[:alnum:][:space:]]+ { fprintf(stderr, "Invalid sequence %s\m", yytext); }
极端情况是一个未终止的字符串文字;也就是说,以 "
开头但不包含匹配的结束引号的标记。这样的标记必须延伸到输入的末尾,并且可以使用您的字符串模式轻松匹配它,省去最后的 "
。 (这是有效的,因为 (f)lex 总是使用最长的匹配模式,所以如果有终止 "
正确的字符串文字将被匹配。)
您的模式中存在一些错误:
在数字文字的开头匹配
+-
几乎总是一个坏主意。如果你这样做,那么x+2
将不会被正确分析;您的词法分析器将 return 两个标记,一个IDENTIFIER
和一个INTEGER
,而不是正确的三个标记(IDENTIFIER
、PLUS
、INTEGER
) .您的
FLOAT
模式不接受小数点前包含 0 的数字,因此0.5
和10.3
都将失败。此外,您强制指数为一位数,因此1.3E11
也不会匹配。而你强制用户在小数点后放一个数字;大多数语言接受3.
等同于3.0
。 (最后一个不一定是错误,但它是非常规的。)您的
INTEGER
模式不接受包含 0 的数字,例如10
。但是它会接受科学记数法,这有点奇怪;在大多数语言中3E10
是浮点常量,而不是整数。您的
KEYWORD
模式接受由一系列串联的单词组成的关键字,例如forwhilefuncif
。您可能不打算在模式末尾放置*
。您的字符串文字模式允许
"
以外的任何字符序列,这意味着反斜杠\
将被允许作为单个字符进行匹配,即使它是后跟引号或反斜杠。这将导致一些字符串文字没有被正确终止。例如,给定字符串文字"\"
(这是一个包含单个反斜杠的字符串文字),正则表达式将匹配初始的
"
,然后是\
作为单个字符,然后是\"
序列, 然后是字符串文字后面的任何内容,直到遇到另一个引号。错误是 flex 要求
\
在方括号表达式内转义的结果,不像 Posix 正则表达式,其中\
在方括号内失去特殊意义。
所以这会给你留下这样的东西:
%{
#include "../h/Tokens.h"
%}
%option yylineno noyywrap
%%
[[:space:]]+ /* Ignore whitespace */
(\.[0-9]+|[0-9]+\.[0-9]*)([eE][+-]?[0-9]+)? {
return FLOAT;
}
0|[1-9][0-9]* return INTEGER;
true|false return BOOLEAN;
func|val|if|else|while|for return KEYWORD;
[A-Za-z_][A-Za-z_0-9]* return IDENTIFIER;
"+" return PLUS;
"-" return MINUS;
"*" return MULTI;
"." return DOT;
"," return COMMA;
":" return COLON;
";" return SEMICOLON;
\"(\\|\\"|[^\"])*\" return STRING;
\"(\\|\\"|[^\"])* { fprintf(stderr,
"Unterminated string literal\n"); }
[^-+*.,:;[:alnum:][:space:]]+ { fprintf(stderr,
"Invalid sequence %s\m", yytext); }
(如果这些模式中的任何一个看起来很神秘,您可能需要查看 the description of flex patterns in the flex manual。)
但我觉得您正在寻找不同的东西:一种无需过多分析即可神奇地适应代币模式的任何变化的方法。
这也是可能的,但我不知道如何在不重复代码的情况下做到这一点。基本思想很简单:当我们遇到一个不匹配的字符时,我们只是将它附加到错误标记的末尾,当我们找到一个有效的标记时,我们发出错误消息并清除错误标记。
问题出在“当我们找到有效标记时”部分,因为这意味着我们需要在除错误规则之外的每个规则的开头插入一个操作。最简单的方法是使用宏,这样至少可以避免为每个动作都写出代码。
(F)lex 确实为我们提供了一些有用的工具,我们可以在此基础上构建它。我们将使用 (f)lex 的特殊操作之一,yymore()
,它会导致当前匹配附加到正在构建的标记,这对于构建错误标记很有用。
为了知道错误标记的长度(从而知道是否有错误标记),我们需要一个额外的变量。幸运的是,(f)lex 允许我们 define our own local variables inside the scanner。然后我们定义宏 E_
(其名称被选择为短,以避免混淆规则操作),它打印错误消息,将 yytext
移动到错误标记上,并重置错误计数。
放在一起:
%{
#include "../h/Tokens.h"
%}
%option yylineno noyywrap
%%
int nerrors = 0; /* To keep track of the length of the error token */
/* This macro must be inserted at the beginning of every rule,
* except the fallback error rule.
*/
#define E_ \
if (nerrors > 0) { \
fprintf(stderr, "Invalid sequence %.*s\n", nerrors, yytext); \
yytext += nerrors; yyleng -= nerrors; nerrors = 0; \
} else /* Absorb the following semicolon */
[[:space:]]+ { E_; /* Ignore whitespace */ }
(\.[0-9]+|[0-9]+\.[0-9]*)([eE][+-]?[0-9]+)? { E_; return FLOAT; }
0|[1-9][0-9]* { E_; return INTEGER; }
true|false { E_; return BOOLEAN; }
func|val|if|else|while|for { E_; return KEYWORD; }
[A-Za-z_][A-Za-z_0-9]* { E_; return IDENTIFIER; }
"+" { E_; return PLUS; }
"-" { E_; return MINUS; }
"*" { E_; return MULTI; }
"." { E_; return DOT; }
"," { E_; return COMMA; }
":" { E_; return COLON; }
";" { E_; return SEMICOLON; }
\"(\\|\\"|[^\"])*\" { E_; return STRING; }
\"(\\|\\"|[^\"])* { E_;
fprintf(stderr,
"Unterminated string literal\n"); }
. { yymore(); ++nerror; }
这一切都假设我们乐于在扫描仪中生成一条错误消息,否则忽略错误字符。但实际 return 错误指示并让调用者决定如何处理错误可能会更好。这引入了一个额外的问题,因为它要求我们在一个动作中 return 两个标记。
对于一个简单的解决方案,我们使用另一个 (f)lex 特性,yyless()
,它允许我们重新扫描部分或全部当前标记。我们可以使用它从当前标记中删除错误标记,而不是调整 yytext 和 yyleng。 (yyless
将为我们进行调整。)这意味着在出现错误后,下一个正确的令牌将被扫描两次。这可能看起来效率低下,但它可能是可以接受的,因为:
- 大多数令牌都很短,
- 针对错误进行优化并没有多大意义。优化正确输入的处理更有用。
为此,我们只需要对 E_
宏做一点小改动:
#define E_ \
if (nerrors > 0) { \
yyless(nerrors); \
fprintf(stderr, "Invalid sequence %s\n", yytext); \
nerrors = 0; \
return BAD_INPUT; \
} else /* Absorb the following semicolon */