使用lex和yacc进行格式验证

时间:2014-08-05 09:53:14

标签: bison yacc flex-lexer lex

假设我有一个包含这样字符串的文件:

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

这是正确的方向吗?我的代码当然没有用。 :)

2 个答案:

答案 0 :(得分:2)

如果你能够在词法分析器中完全预测状态的顺序,那么使用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添加到开始条件列表中,但由于开始条件是包含的,因此无需为该条件明确标记任何规则。

答案 1 :(得分:0)

对于这种简单的检查(因为你没有显示语法),你不需要'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的“编译技术简介”之后是一个合理的实践,虽然是古老的介绍。