【问题标题】:Format validation with lex and yacc使用 lex 和 yacc 进行格式验证
【发布时间】:2014-08-05 09:53:13
【问题描述】:

假设我有一个包含如下字符串的文件:

qwerty01234xy+-/
rtweqq22222xx+++

[A-Z] 的前 6 个字符,然后是 [0-9] 的 5 个,然后是 [A-Z] 的 2 个和 [+-/] 的最后 3 个。我想编写一个格式检查器,它会产生语法错误。 到目前为止,我正在做的事情是这样的:

lex 文件:

<code>
...
/*states*/
%x WORD1_STATE
%x NUMBER_STATE
%x WORD22_STATE
%x ETC_STATE
%%
...
yy_push_state(ETC_STATE)
yy_push_state(WORD22_STATE)
yy_push_state(NUMBER_STATE)
yy_push_state(WORD1_STATE)
...
 /*rules*/
<WORD1_STATE>^[A-Z]{6}    yy_pop_state(); yylval.string=strdup(yytext); return WORD1;
<NUMBER_STATE>[0-9]{5}    yy_pop_state(); yylval.string=strdup(yytext); return NUMBER;
<WORD22_STATE>[A-Z]{2}    yy_pop_state(); yylval.string=strdup(yytext); return WORD2;
<ETC_STATE>[+-/]{3}    yy_pop_state(); yylval.string=strdup(yytext); return ETC;

\n        /*do nothing*/
<*>.      fprintf(stderr, "Bad character at line %d column %d: \"%s\"\n", yylloc.first_line, yylloc.first_column, yytext); yy_pop_state();
</code>

yacc 规则:

<code>
 entries :
         | entry
         | error
         ;
 entry : WORD1 NUMBER WORD2 ETC;
</code>

我的目标如下:如果这个检查器看到这样的一行:

aaaaaa01W56ss--1

它会产生以下错误:

Bad character in NUMBER at line x at column 9
Bad character in ETC at line x at column 16

这是正确的方向吗?我的代码当然不起作用。 :)

【问题讨论】:

  • 您不需要 lex 或 yacc。只是一个正则表达式。

标签: bison yacc flex-lexer lex


【解决方案1】:

如果你可以在你的词法分析器中完美地预测状态序列,那么使用yacc 真的没有意义;它在这里确实没有提供任何有用的设施。 (请参阅下面的词法分析器中的错误恢复策略。)另一方面,如果语法比简单的模式序列更复杂,您可能需要yacc;在这种情况下,您应该提供一个更准确的示例。

无论如何,将状态压入堆栈并不是处理进程的一种非常有效的机制。使用BEGIN 宏通常更容易构建简单的状态机。

这是您示例的基本词法分析器:

%s NUMBER WORD2 ETC

%%
    /* Any indented text before the first rule is inserted
     * at the top of the yylex function.
     */
    int error_count = 0;
<INITIAL>[A-Z]{6} BEGIN(NUMBER);
<NUMBER>[0-9]{5}  BEGIN(WORD2);
<WORD2>[A-Z]{2}   BEGIN(ETC);
<ETC>[+-/]{3}     BEGIN(EOL);
<EOL>" "*\n       BEGIN(INITIAL);
<RECOVER>.*       BEGIN(EOL);
.|\n              signal_error(); ++error_count; BEGIN(RECOVER);
<<EOF>>           return error_count != 0;

(在 (f)lex 中,模式 . 不匹配换行符。在 RECOVER 开始条件中使用这一事实应该在下面变得明显。)

只要换行符不是模式的一部分,就很容易跟踪行和列信息,就像您的示例一样。所以让我们补充一下:

%s NUMBER WORD2 ETC

%%
    int error_count = 0;
    int line = 1, column = 1;
<INITIAL>[A-Z]{6} BEGIN(NUMBER);  column += yyleng;
<NUMBER>[0-9]{5}  BEGIN(WORD2);   column += yyleng;
<WORD2>[A-Z]{2}   BEGIN(ETC);     column += yyleng;
<ETC>[+-/]{3}     BEGIN(EOL);     column += yyleng;
<EOL>" "*\n       BEGIN(INITIAL); ++line; column = 0;
<RECOVER>.*       BEGIN(EOL);
.|\n              signal_error(); ++error_count; yyless(0); BEGIN(RECOVER);
<<EOF>>           return error_count;

(注意在默认规则中使用yyless(0)。这会导致错误字符返回到输入源,以便在新的开始条件下重新扫描它,这避免了一些围绕换行符和获取的混乱逻辑行和列计数器正确。此外,我们将所有换行处理集中在EOL 开始条件的规则中,以防我们以后需要修改它。)

现在只需要编写错误报告器,我们需要将状态映射到字符串和 main 驱动程序,并添加必要的内容以避免编译器警告:

%{
#  include <stdio.h>

void signal_error(int state, int line, int column);
%}
%option noyywrap nounput noinput

%s NUMBER WORD2 ETC EOL RECOVER

%%
    int error_count = 0;
    int line=1, column=1;
<INITIAL>[A-Z]{6} BEGIN(NUMBER);  column += yyleng;
<NUMBER>[0-9]{5}  BEGIN(WORD2);   column += yyleng;
<WORD2>[A-Z]{2}   BEGIN(ETC);     column += yyleng;
<ETC>[+-/]{3}     BEGIN(EOL);     column += yyleng;
<EOL>" "*\n       BEGIN(INITIAL); ++line; column = 1;
<RECOVER>.*       BEGIN(EOL);
.|\n              { signal_error(YY_START, line, column);
                    ++error_count; yyless(0); BEGIN(RECOVER);
                  }
<<EOF>>           return error_count != 0;
%%

typedef struct { int state; const char* name; } StateToName;
const StateToName state_to_name[] = {
  { INITIAL, "in WORD1" },
  { NUMBER,  "in NUMBER"},
  { WORD2,   "in WORD2" },
  { ETC,     "in ETC"   },
  { EOL,     "at end of line"},
  { -1,      NULL}
};

const char* find_name(int state) {
  for (const StateToName* ent = state_to_name; ent->name; ++ent)
    if (state == ent->state) return ent->name;
  return "in unknown state";
}

void signal_error(int state, int line, int column) {
  fprintf(stderr, "Bad character %s at line %d, column %d\n",
                  find_name(state), line, column);
}

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

这仍然不是很理想,因为报告的列计数是“令牌”的开始,而不是带有无效字符的实际列。不幸的是,flex 没有提供写“匹配此模式的初始前缀”的方法。在这种情况下,模式计算可以手动完成,但总的来说很烦人:

<INITIAL>[A-Z]{6}   BEGIN(NUMBER);  column += yyleng;
<INITIAL>[A-Z]{0,5} BEGIN(ERROR);   column += yyleng;
<NUMBER>[0-9]{5}    BEGIN(WORD2);   column += yyleng;
<NUMBER>[0-9]{0,4}  BEGIN(ERROR);   column += yyleng;
<WORD2>[A-Z]{2}     BEGIN(ETC);     column += yyleng;
<WORD2>[A-Z]{0,1}   BEGIN(ERROR);   column += yyleng;
<ETC>[+-/]{3}       BEGIN(EOL);     column += yyleng;
<ETC>[+-/]{0,2}     BEGIN(ERROR);   column += yyleng;

根据上述情况,需要将ERROR 添加到开始条件列表中,但由于开始条件包含在内,因此无需为该条件显式标记任何规则。

【讨论】:

  • 非常感谢您的精彩解释。我还想拥有一件事:必须做什么才能在一行中报告多个错误?目前,如果发现一个错误,则不会“报告”当前行中的下一个错误。
【解决方案2】:

对于这样简单的检查(因为您没有显示语法),您不需要“yacc”。如果您并不真正关心精确的错误消息,您可以编写一个“grep”单行来过滤掉错误的行。如果您的动机是提供错误消息,'awk' 是一个很好的解决方案:

#!/usr/bin/awk -f

/[[:alpha:]]{6}[[:digit:]]{5}[[:alpha:]]{2}(+|-|\/)/ { next; }

{
    if (substr($0,1,6) !~ /[[:alpha:]]{6}/)
    print "first six chars in " NR, substr($0,1,6);
    # check for other mistakes
}

如果是 lex/yacc 知识,那么您在 JP Bennett 的“编译技术介绍”之后是一个合理的动手实践的介绍,虽然很古老。

【讨论】:

  • 问题是,我展示的代码只是一个简化,我必须每行处理更多的标记,并且由于其他功能(例如错误消息,正如你提到的),“仅正则表达式" 解决方案不合适。无论如何,谢谢你的建议。
猜你喜欢
  • 1970-01-01
  • 2011-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多