Lex是由美國Bell實驗室M.Lesk等人用C語言開發的一種詞法分析器自動生成工具,它提供一種供開發者編寫詞法規則(正規式等)的語言(Lex語言)以及這種語言的翻譯器(這種翻譯器將Lex語言編寫的規則翻譯成為C語言程序)。
Lex是linux下的工具,本實驗使用的編譯工具是cygwin(cygwin在windows下模擬一個linux環境)下的flex,它與lex的使用方法基本相同,只有很少的差別。
1.Lex的基本原理和使用方法
Lex的基本工作原理為:由正規式生成NFA,將NFA變換成DFA,DFA經化簡后,模擬生成詞法分析器。
其中正規式由開發者使用Lex語言編寫,其余部分由Lex翻譯器完成.翻譯器將Lex源程序翻譯成一個名為lex.yy.c的C語言源文件,此文件含有兩部分內容:一部分是根據正規式所構造的DFA狀態轉移表,另一部分是用來驅動該表的總控程序yylex()。當主程序需要從輸入字符流中識別一個記號時,只需要調用一次yylex()就可以了。為了使用Lex所生成的詞法分析器,我們需要將lex.yy.c程序用C編譯器進行編譯,並將相關支持庫函數連入目標代碼。Lex的使用步驟可如下圖所示:
2.lex源程序的寫法
Lex源程序必須按照Lex語言的規范來寫,其核心是一組詞法規則(正規式)。一般而言,一個Lex源程序分為三部分,三部分之間以符號%%分隔。
[第一部分:定義段]
%%
第二部分:詞法規則段
[%%
第三部分:輔助函數段]
其中,第一部分及第三部分和第三部分之上的%%都可以省略(即上述方括號括起的部分可以省略)。以%開頭的符號和關鍵字,或者是詞法規則段的各個規則一般頂着行首來寫,前面沒有空格。
Lex源程序中可以有注釋,注釋由/*和*/括起,但是請注意,注釋的行首需要有前導空白。
1)第一部分定義段的寫法:
定義段可以分為兩部分:
第一部分以符號%{和%}包裹,里面為以C語法寫的一些定義和聲明:例如,文件包含,宏定義,常數定義,全局變量及外部變量定義,函數聲明等。這一部分被Lex翻譯器處理后會全部拷貝到文件lex.yy.c中。注意,特殊括號%{和%}都必須頂着行首寫。例如:
%{ #define LT 1 intyylval; %}
第二部分是一組正規定義和狀態定義。正規定義是為了簡化后面的詞法規則而給部分正規式定義了名字。每條正規定義也都要頂着行首寫。例如下面這組正規定義分別定義了letter,digit和id所表示的正規式:
letter [A-Za-z] digit [0-9] id {letter}({letter}|{digit})*
注意:上面正規定義中出現的小括號表示分組,而不是被匹配的字符。而大括號括起的部分表示正規定義名。
狀態定義也叫環境定義,它定義了匹配正規式時所處的狀態的名字。狀態定義以%s開始,后跟所定義的狀態的名字,注意%s也要頂行首寫,例如下面一行就定義了一個名為COMMENT的狀態和一個名為BAD的狀態,狀態名之間用空白分隔:
%s COMMENT BAD
2) 第二部分詞法規則段的寫法:
詞法規則段列出的是詞法分析器需要匹配的正規式,以及匹配該正規式后需要進行的相關動作。其例子如下:
while {return (WHILE);} do {return (DO);} {id} {yylval = installID (); return (ID);}
每行都是一條規則,該規則的前一部分是正規式,需要頂行首寫,后一部分是匹配該正規式后需要進行的動作,這個動作是用C語法來寫的,被包裹在{}之內,被Lex翻譯器翻譯后會被直接拷貝進lex.yy.c。正規式和語義動作之間要有空白隔開。其中用{}擴住的正規式表示正規定義的名字。
也可以若干個正規式匹配同一條語義動作,此時正規式之間要用 | 分隔。
3) 第三部分輔助函數段的寫法:
輔助函數段用C語言語法來寫,輔助函數一般是在詞法規則段中用到的函數。這一部分一般會被直接拷貝到lex.yy.c中。
4) Lex源程序中詞法規則(即正規式)的相關規定:
元字符:元字符是lex語言中作特殊用途的一些字符,包括:* + ? | { } [ ] ( ). ^ $ “ \ - / < >。
正文字符:除元字符以外的其他字符,這些字符在正規式中可以被匹配。若單個正文字符c作為正規式,則可與字符c匹配,元字符無法被匹配,如果元字符想要被匹配,則需要通過“轉義”的方式,即用” ”包括住元字符,或在元字符前加\。例如”+”和\+都表示加號。C語言中的一些轉義字符也可以出現在正規式中,例如\t \n \b等。
部分元字符在lex語言中的特殊含義:
^表示補集:[^…]表示補集,即匹配除^之后所列字符以外的任何字符。如[^0-9]表示匹配除數字字符0-9以外的任意字符。除^ - \以外,任何元字符在方括號內失去其特殊含義。如果要在方括號內表示負號-,則要將其至於方括號內的第一個字符位置或者最后一個字符位置,例如[-+0-9][+0-9-]都匹配數字及+ - 號。
. ^$ /:
點運算符 . 匹配除換行之外的任何字符,一般可作為最后一條翻譯規則。
^匹配行首字符。如:^begin匹配出現在行首的begin
$匹配行末字符。如:end$ 匹配出現在行末的end
R1/R2(R1和R2是正規式)表示超前搜索:若要匹配R1,則必須先看緊跟其后的超前搜索部分是否與R2匹配。
如:DO/{alnum}*={alnum}*,表示如果想匹配DO,則必須先在DO后面找到形式為{alnum}*={alnum}*的串,才能確定匹配DO。
5) Lex源程序中常用到的變量及函數:
yyin和yyout:這是Lex中本身已定義的輸入和輸出文件指針。這兩個變量指明了lex生成的詞法分析器從哪里獲得輸入和輸出到哪里。默認:鍵盤輸入,屏幕輸出。
yytext和yyleng:這也是lex中已定義的變量,直接用就可以了。
yytext:指向當前識別的詞法單元(詞文)的指針
yyleng:當前詞法單元的長度。
ECHO:Lex中預定義的宏,可以出現在動作中,相當於fprintf(yyout, “%s”,yytext),即輸出當前匹配的詞法單元。
yylex():詞法分析器驅動程序,用Lex翻譯器生成的lex.yy.c內必然含有這個函數。
yywrap():詞法分析器遇到文件結尾時會調用yywrap()來決定下一步怎么做:
若yywrap()返回0,則繼續掃描
若返回1,則返回報告文件結尾的0標記。
由於詞法分析器總會調用yywrap,因此輔助函數中最好提供yywrap,如果不提供,則在用C編譯器編譯lex.yy.c時,需要鏈接相應的庫,庫中會給出標准的yywrap函數(標准函數返回1)。
6) 詞法分析器的狀態(環境):
詞法分析器在匹配正規式時,可以在不同狀態(或環境)下進行。我們可以規定在不同的狀態下有不同的匹配方式。每個詞法分析器都至少有一個狀態,這個狀態叫做初始狀態,可以用INITIAL或0來表示,如果還需要使用其他狀態,可以在定義段用%s 來定義。
使用狀態時,可以用如下方式寫詞法規則:
<state1, state2> p0 {action0;} <state1> p1 {action1;}
這兩行詞法規則表示:在狀態state1和state2下,匹配正規式p0后執行動作action0,而只有在狀態state1下,才可以匹配正規式p1后執行動作action1。如果不指明狀態,默認情況下處於初始狀態INITIAL。
要想進入某個特定狀態,可以在動作中寫上這樣一句: BEGINstate; 執行這個動作后,就進入狀態state。
下面是一段處理C語言注釋的例子,里面用到了狀態的轉換,在這個例子里,使用不同的狀態,可以讓詞法分析器在處於注釋中和處於注釋外時使用不同的匹配規則:
… %s c_comment … %% <INITIAL>“/*” {BEGIN c_comment;} … <c_comment>“*/” {BEGIN 0;} <c_comment>. {;}
7) Lex的匹配策略:
(1) 按最長匹配原則確定被選中的單詞。
(2) 如果一個字符串能被若干正規式匹配,則先匹配排在前面的正規式。
3.Lex生成的詞法分析器如何使用
lex常常與語法分析器的生成工具yacc(第三章會講到)同時使用。此時,一般來說,語法分析器每次都調用一次 yylex()獲取一個記號。如果想自己寫一個程序使用lex生成的詞法分析器,則只需要在自己的程序中按需要調用yylex()函數即可。
請注意:yylex()調用結束后,輸入緩沖區並不會被重置,而是仍然停留在剛才讀到的地方。並且,詞法分析器當前所處的狀態(%s定義的那些狀態)也不會改變。
完整的Lex源程序例子請見exam1.l和exam2.l。
exam1.l

/* 這是注釋的形式,與C中的/*...* /注釋相同。 */ /* 第一部分是定義、聲明部分。這部分內容可以為空。*/ %{ /* 寫在 %{...%}這對特殊括號內的內容會被直接拷貝到C文件中。 * * 這部分通常進行一些頭文件聲明,變量(全局,外部)、常量 * 的定義,用C語法。 * * %{和%}兩個符號都必須位於行首 */ /* 下面定義了需要識別的記號名,如果和yacc聯合使用,這些記號名都應該在yacc中定義 */ #include <stdio.h> #define LT 1 #define LE 2 #define GT 3 #define GE 4 #define EQ 5 #define NE 6 #define WHILE 18 #define DO 19 #define ID 20 #define NUMBER 21 #define RELOP 22 #define NEWLINE 23 #define ERRORCHAR 24 int yylval; /* yylval 是yacc中定義的變量,用來保存記號的屬性值,默認是int類型。 * 在用lex實現的詞法分析器中可以使用這個變量將記號的屬性傳遞給用 * yacc實現的語法分析器。 * * 注意:該變量只有在聯合使用lex和yacc編寫詞法和語法分析器時才可在lex * 中使用,此時該變量不需要定義即可使用。 * 單獨使用lex時,編譯器找不到這個變量。這里定義該變量為了“欺騙”編譯器。 */ %} /* 這里進行正規定義和狀態定義。 * 下面就是正規定義,注意,正規定義和狀態定義都要頂着行首寫。 */ delim [ \t \n] /* \用來表示轉義,例如\t表示制表符,\n表示換行符。*/ ws {delim}+ letter [A-Za-z_] digit [0-9] id {letter}({letter}|{digit})* /* 注意:上面正規定義中出現的小括號表示分組,而不是被匹配的字符。 * 而大括號括起的部分表示正規定義名。 */ number {digit}+(\.{digit}+)?(E[+-]?{digit}+)? /* %%作為lex文件三個部分的分割符,必須位於行首 */ /* 下面這個%%不能省略 */ %% /* 第二部分是翻譯規則部分。 */ /* 寫在這一部分的注釋要有前導空格,否則lex編譯出錯。*/ /* 翻譯規則的形式是:正規式 {動作} * 其中,正規式要頂行首寫,動作要以C語法寫(動作會被拷貝到yylex()函數中,),\ * 正規式和動作之間要用空白分割。 */ {ws} {;/* 此時詞法分析器沒有動作,也不返回,而是繼續分析。 */} /* 正規式部分用大括號擴住的表示正規定義名,例如{ws}。 * 沒有擴住的直接表示正規式本身。 * 一些元字符沒辦法表示它本身,此時可以用轉義字符或 * 用雙引號括起來,例如"<" */ while {return (WHILE);} do {return (DO);} {id} {yylval = installID (); return (ID);} {number} {yylval = installNum (); return (NUMBER);} "<" {yylval = LT; return (RELOP);} "<=" {yylval = LE; return (RELOP);} "=" {yylval = EQ; return (RELOP);} "<>" {yylval = NE; return (RELOP);} ">" {yylval = GT; return (RELOP);} ">=" {yylval = GE; return (RELOP);} . {yylval = ERRORCHAR; return ERRORCHAR;} /*.匹配除換行之外的任何字符,一般可作為最后一條翻譯規則。*/ %% /* 第三部分是輔助函數部分,這部分內容以及前面的%%都可以省略 */ /* 輔助函數可以定義“動作”中使用的一些函數。這些函數 * 使用C語言編寫,並會直接被拷貝到lex.yy.c中。 */ int installID () { /* 把詞法單元裝入符號表並返回指針。*/ return ID; } int installNum () { /* 類似上面的過程,但詞法單元不是標識符而是數 */ return NUMBER; } /* yywrap這個輔助函數是詞法分析器遇到輸入文件結尾時會調用的,用來決定下一步怎么做: * 若yywrap返回0,則繼續掃描;返回1,則詞法分析器返回報告文件已結束的0。 * lex庫中的標准yywrap程序就是返回1,你也可以定義自己的yywrap。 */ int yywrap (){ return 1; } void writeout(int c){ switch(c){ case ERRORCHAR: fprintf(yyout, "(ERRORCHAR, \"%s\") ", yytext);break; case RELOP: fprintf(yyout, "(RELOP, \"%s\") ", yytext);break; case WHILE: fprintf(yyout, "(WHILE, \"%s\") ", yytext);break; case DO: fprintf(yyout, "(DO, \"%s\") ", yytext);break; case NUMBER: fprintf(yyout, "(NUM, \"%s\") ", yytext);break; case ID: fprintf(yyout, "(ID, \"%s\") ", yytext);break; case NEWLINE: fprintf(yyout, "\n");break; default:break; } return; } /* 輔助函數里可以使用yytext和yyleng這些外部定義的變量。 * yytext指向輸入緩沖區當前詞法單元(lexeme)的第一個字符, * yyleng給出該詞法單元的長度 */ /* 如果你的詞法分析器並不是作為語法分析器的子程序, * 而是有自己的輸入輸出,你可以在這里定義你的詞法 * 分析器的main函數,main函數里可以調用yylex() */ int main (int argc, char ** argv){ int c,j=0; if (argc>=2){ if ((yyin = fopen(argv[1], "r")) == NULL){ printf("Can't open file %s\n", argv[1]); return 1; } if (argc>=3){ yyout=fopen(argv[2], "w"); } } /* yyin和yyout是lex中定義的輸入輸出文件指針,它們指明了 * lex生成的詞法分析器從哪里獲得輸入和輸出到哪里。 * 默認:鍵盤輸入,屏幕輸出。 */ while (c = yylex()){ writeout(c); j++; if (j%5 == 0) writeout(NEWLINE); } if(argc>=2){ fclose(yyin); if (argc>=3) fclose(yyout); } return 0; }
exam2.l

/* 把注釋去掉 */ %{ #include <stdio.h> #define LT 1 #define LE 2 #define GT 3 #define GE 4 #define EQ 5 #define NE 6 #define LLK 7 #define RLK 8 #define LBK 9 #define RBK 10 #define IF 11 #define ELSE 12 #define EQU 13 #define SEM 14 #define WHILE 18 #define DO 19 #define ID 20 #define NUMBER 21 #define RELOP 22 #define NEWLINE 23 #define ERRORCHAR 24 #define ADD 25 #define DEC 26 #define MUL 27 #define DIV 28 %} delim [ \t \n] ws {delim}+ letter [A-Za-z_] digit [0-9] id {letter}({letter}|{digit})* number {digit}+(\.{digit}+)?(E[+-]?{digit}+)? /* 狀態(或條件)定義可以定義在這里 * INITIAL是一個默認的狀態,不需要定義 */ %s COMMENT %s COMMENT2 %% <INITIAL>"/*" {BEGIN COMMENT;} <COMMENT>"*/" {BEGIN INITIAL;} <COMMENT>.|\n {;} <INITIAL>"//" {BEGIN COMMENT2;} <COMMENT2>\n {BEGIN INITIAL;} <COMMENT2>. {;} /* ECHO是一個宏,相當於 fprintf(yyout, "%s", yytext)*/ <INITIAL>{ws} {;} <INITIAL>while {return (WHILE);} <INITIAL>do {return (DO);} <INITIAL>if {return (IF);} <INITIAL>else {return (ELSE);} <INITIAL>{id} {return (ID);} <INITIAL>{number} {return (NUMBER);} <INITIAL>"<" {return (RELOP);} <INITIAL>"<=" {return (RELOP);} <INITIAL>"=" {return (RELOP);} <INITIAL>"!=" {return (RELOP);} <INITIAL>">" {return (RELOP);} <INITIAL>">=" {return (RELOP);} <INITIAL>"(" {return (RELOP);} <INITIAL>")" {return (RELOP);} <INITIAL>"{" {return (RELOP);} <INITIAL>"}" {return (RELOP);} <INITIAL>"+" {return (RELOP);} <INITIAL>"-" {return (RELOP);} <INITIAL>"*" {return (RELOP);} <INITIAL>"/" {return (RELOP);} <INITIAL>";" {return (RELOP);} <INITIAL>. {return ERRORCHAR;} %% int yywrap (){ return 1; } void writeout(int c){ switch(c){ case ERRORCHAR: fprintf(yyout, "(ERRORCHAR, \"%s\") ", yytext);break; case RELOP: fprintf(yyout, "(RELOP, \"%s\") ", yytext);break; case WHILE: fprintf(yyout, "(WHILE, \"%s\") ", yytext);break; case DO: fprintf(yyout, "(DO, \"%s\") ", yytext);break; case IF: fprintf(yyout, "(IF, \"%s\") ", yytext);break; case ELSE: fprintf(yyout, "(ELSE, \"%s\") ", yytext);break; case NUMBER: fprintf(yyout, "(NUM, \"%s\") ", yytext);break; case ID: fprintf(yyout, "(ID, \"%s\") ", yytext);break; case NEWLINE: fprintf(yyout, "\n");break; default:break; } return; } int main (int argc, char ** argv){ int c,j=0; if (argc>=2){ if ((yyin = fopen(argv[1], "r")) == NULL){ printf("Can't open file %s\n", argv[1]); return 1; } if (argc>=3){ yyout=fopen(argv[2], "w"); } } while (c = yylex()){ writeout(c); j++; if (j%5 == 0) writeout(NEWLINE); } if(argc>=2){ fclose(yyin); if (argc>=3) fclose(yyout); } return 0; }
test1.p
while a >= -1.2E-2 do b<=2
test2.p
while a >= -1.2E-2 do b<=2 if else + - * / ; //的發生過 /* 請注意:測試文件的格式必須符合要求, 比如,該文件要求的格式是UNIX格式。*/
4.cygwin下編譯連接lex源程序的命令
1) 用lex翻譯器編譯lex源程序命令(假設filename.l是lex源程序名):
flex filename.l
2) 用gcc編譯器編譯lex翻譯器生成的c源程序(lex翻譯器生成的c源程序名固定為lex.yy.c):
gcc [-o outfile] lex.yy.c –lfl
其中,-lfl是鏈接flex的庫函數的,庫函數中可能包含類似yywrap一類的標准函數。-o outfile是可選編譯選項,該選項可將編譯生成的可執行程序命名為outfile,如果不寫該編譯選項,默認情況下生成的可執行程序名為a.exe(linux下實際為a.out)。
3) 調用詞法分析器yylex()的main函數可以寫在lex源程序的輔助函數部分,也可以寫在其他的c文件中。如果main函數寫在main.c中,則編譯時需要和lex.yy.c一起編譯鏈接,即編譯鏈接命令為:
gcc [-o outfile] lex.yy.cmain.c –lfl
4) 運行可執行文件a.exe的命令(假設a.exe處於當前目錄下,且忽略運行參數的情況):
./a.exe
其中,./表示當前目錄。如果a.exe處於其他路徑,則運行時請給出完整路徑名。
5.關於cygwin
1) 網址:www.cygwin.com 是cygwin的官方網站,可以從上面下載安裝cygwin。
2) 下載和安裝:從上述網址下載setup.exe運行,即可選擇從網絡上安裝cygwin,也可選擇下載到本地,然后再從本地安裝。
本地安裝的過程:
(1) 將setup.exe及安裝包下載到本地
(2) 運行setup.exe à 選擇Install fromLocal Directory à 選一個root directory(例如可以選D:\cygwin,不要選中文路徑名) à選一個Local Package Directory(選擇存放安裝包的那個目錄)àselectpackages(其它都可以default,重要的是選中Devel下的bison,flex,gcc-core,gcc-g++, make,vim)
3) 使用cygwin:
安裝完成后,檢查環境變量中有沒有HOME變量,如果有,先將HOME變量改名(方法:右鍵我的電腦à屬性à高級à環境變量,在你自己的用戶變量列表中找到HOME變量,改名)。
運行安裝目錄(root directory)下的cygwin.bat啟動cygwin。第一次運行cygwin會生成home目錄,如果home目錄創建不成功,則很可能是HOME環境變量的緣故,先將這個變量改名,再運行cygwin.bat。
第一次運行成功后,所在目錄應該是/home/your-user-name,請把你的文件存於該目錄下。其中home目錄實際上是在你選擇的root directory下。