First!
lex程序的結構是這樣的!
定義
%%
規則
%%
用戶代碼
一個 Lex 程序分為三個段:第一段是 C 和 Lex 的全局聲明,第二段包括模式(C 代碼),第三段是補充的 C 函數。 這些段以%%來分界。 下面是一個行數與字數的統計工具。
int num_lines = 0, num_chars = 0;
%%
\n ++num_lines; ++num_chars;
. ++num_chars;
%%
main()
{
yylex();
printf( "# of lines = %d, # of chars = %d\n",
num_lines, num_chars );
}
Second!
對First內容的回顧
C 和 Lex 的全局聲明
這一段中我們可以增加 C 變量聲明。這里我們將為字數統計程序聲明一個整型變量,來保存程序統計出來的字數。我們還將進行 Lex 的標記聲明。
字數統計程序的聲明
%{
int wordCount = 0;
%}
chars [A-za-z\_\'\.\"]
numbers ([0-9])+
delim [" "\n\t]
whitespace {delim}+
words {chars}+
%%
兩個百分號標記指出了 Lex 程序中這一段的結束和三段中第二段的開始。
Lex 的模式匹配規則
讓我們看一下 Lex 描述我們所要匹配的標記的規則。(我們將使用 C 來定義標記匹配后的動作。)繼續看我們的字數統計程序,下面是標記匹配的規則。
字數統計程序中的 Lex 規則
{words} { wordCount++; /*
increase the word count by one*/ }
{whitespace} { /* do
nothing*/ }
{numbers} { /* one may
want to add some processing here*/ }
%%
C 代碼
Lex 編程的第三段,也就是最后一段覆蓋了 C 的函數聲明(有時是主函數)。注意這一段必須包括 yywrap() 函數。 Lex 有一套可供使用的函數和變量。 其中之一就是 yywrap。一般來說,yywrap() 的定義如下例。我們將在 高級 Lex 中探討這一問題。
字數統計程序的 C 代碼段
void main()
{
yylex(); /* start the
analysis*/
printf(" No of words:
%d\n", wordCount);
}
int yywrap()
{
return 1;
}
Lex 編程的基本元素就這樣搞定了,它將幫助你編寫簡單的詞法分析程序。
Third
高級Lex
Lex 有幾個函數和變量提供了不同的信息,可以用來編譯實現復雜函數的程序。下表中列出了一些變量和函數,以及它們的使用。 詳盡的列表請參考 Lex 手冊。
Lex 變量
yyin FILE* 類型。 它指向 lexer 正在解析的當前文件。
yyout FILE* 類型。 它指向記錄 lexer 輸出的位置。 缺省情況下,yyin 和 yyout 都指向標準輸入和輸出。
yytext 匹配模式的文本存儲在這一變量中(char*)。
yyleng 給出匹配模式的長度。
yylineno 提供當前的行數信息。(lexer不一定支持。)
Lex 函數
yylex() 這一函數開始分析。 它由 Lex 自動生成。
yywrap() 這一函數在文件(或輸入)的末尾調用。如果函數的返回值是1,就停止解析。 因此它可以用來解析多個文件。代碼可以寫在第三段,這就能夠解析多個文件。 方法是使用 yyin 文件指針(見上表)指向不同的文件,直到所有的文件都被解析。最后,yywrap() 可以返回 1 來表示解析的結束。
yyless(int n) 這一函數可以用來送回除了前 n? 個字符外的所有讀出標記。
yymore() 這一函數告訴 Lexer 將下一個標記附加到當前標記后。
到此為止,可能你看到lex程序還會范暈,沒關系,下面我們接著來,分析一個類pascal語法的極簡析器!
/* 這個就是注釋了*/
/* scanner for a toy Pascal-like language */
申明部分開始
%{ 內的東西會原封不動地出現在輸出文件中 }%
%{
/* need this for the call to atof() below */
#include <math.h>
%}
DIGIT [0-9]
ID [a-z][a-z0-9]*
%%
模式部分開始
{DIGIT}+ {
printf( "An integer: %s (%d)\n", yytext,
atoi( yytext ) );
}
{DIGIT}+"."{DIGIT}* {
printf( "A float: %s (%g)\n", yytext,
atof( yytext ) );
}
if|then|begin|end|procedure|function {
printf( "A keyword: %s\n", yytext );
}
{ID} printf( "An identifier: %s\n", yytext );
"+"|"-"|"*"|"/" printf( "An operator: %s\n", yytext );
"{"[^}\n]*"}" /* eat up one-line comments */
[ \t\n]+ /* eat up whitespace */
. printf( "Unrecognized character: %s\n", yytext );
%%
補充部分開始
main( argc, argv )
int argc;
char **argv;
{
++argv, --argc; /* skip over program name */
if ( argc > 0 )
yyin = fopen( argv[0], "r" );
else
yyin = stdin;
yylex();
}
想要真正了解lex, [[正則表達式]] 是關鍵!
Four
yytext 匹配模式的文本存儲變量, 可以通過在申明階段使用%pointer或%array來控制是一個字符指針還是一個字符數組。指針模式與數組模式各有特點,導致在yytex申明上也不一樣,具體請參考lex手冊!
在模式階段中
模式 動作
[ \t]+ putchar( ' ' );
[ \t]+$ /* ignore this token */
模式部分是正則表達式,動作部分是處理方法,動作部分如果時{開頭,那么,動作將會持續到},如果動作中出現了括號{},開始采用 %{ %}來表示動作去區段。動作部分如果時 |,就表示與下一條規則執行相同的動作。
好的,我們來看一個更為實用一點的lex程序。
我們先定義三個動作:
ECHO 將yytext輸出
BEGIN 開始一個條件處理塊
REJECT 指示簡析器對當前規則不做處理,而是采用第二匹配規則。
int word_count = 0;
%%
frob special(); REJECT;
[^ \t\n]+ ++word_count;
如果frob沒有REJECT動作,frob將不會被計數,因為解析器在通常情況下,每個被匹配的對象只會對一個動作生效,多個REJECT也是允許的,會尋找下一個最配的規則來做處理。所以,下面的規則會把輸入的"abcd"處理后輸出"abcdabcaba".
%%
a |
ab |
abc |
abcd ECHO; REJECT;
.|\n /* eat up any unmatched character */
`yymore()' 告訴解析器下一次匹配的規則,滿足的部分將會添加到當前yytext值得后面而不是替換它。 例如,指定的輸入"mega-kludge"經過下面的程序處理后將會輸出"mega-mega-kludge"。
%%
mega- ECHO; yymore();
kludge ECHO;
第一個 "mega-" 被滿足并且輸出. 然后 "kludge" 滿足, 但是并沒有替換之前的"mega-"而是"kludge"附加到他的后面,然后輸出的其實是"mega-kludge".
yymore()需要兩件事情需要注意。第一,yymnore()依賴于表現當前匹配項的長度yyleng的值,所以使用yymore不允許改變yyleng的值。第二,yymore()的使用會使解析器付出一點點性能的代價。
有yymore()就有yyless()
yyless(n) 返回當前匹配項除了開始的n個字符內的所有的內容到輸入緩存區,解析器處理下一個匹配時,它們將會被重新解析。yyless將會導致yytext與yyleng的調整。(yyleng將會等于=n) 如輸入"foobar"被下面的程序處理后,將會輸出"boobarbar". 因為前n=3個字符foo外的字符bar被重新返回到輸入緩存區了。
%%
foobar ECHO; yyless(3);
[a-z]+ ECHO;
參數0對于yyless將會導致整個當前匹配將會被重新解析。除非你改變了解析器本來的處理流程(如使用begin),這將會導致循環結束。需要注意的是,yyless是一個宏,并且在flex輸入文件中使用,不能在其他源文件中使用。
unput(c) 將字符c放回到輸入流中,該字符可以重新被解析。下面的動作將當前的匹配值附上括號后重新進行匹配。
{
int i;
/* Copy yytext because unput() trashes yytext */
char *yycopy = strdup( yytext );
unput( ')' );
for ( i = yyleng - 1; i >= 0; --i )
unput( yycopy[i] );
unput( '(' );
free( yycopy );
}
注意: 由于每次unput()將指定的字符添加到輸入源的開頭,所以將字符串添加到輸入源開頭必須從后道前處理。一個比較重要的潛在問題是使用unput()的時候,如果采用了%pointer指針模式保存yytext,unput會破壞yytext的內容,從最右邊的字符開始將會破壞左邊的一個字符。如果在unput()后要用到yytext,你首先必須復制一份yytext,或者用%array模式來保存yytext. 最后你不能放一個EOF去試圖標志輸入流的結束。
input 從輸入源中讀取下一個字符。例如,下面有的例子將會吃掉C語言注釋
%%
"/*" {
register int c;
for ( ; ; )
{
while ( (c = input()) != '*' &&
c != EOF )
; /* eat up text of comment */
if ( c == '*' )
{
while ( (c = input()) == '*' )
;
if ( c == '/' )
break; /* found the end */
}
if ( c == EOF )
{
error( "EOF in comment" );
break;
}
}
}
注意: 如果簡析器采用用C++編譯,input()被yyinput()的替代,因為input()與C++中的流名稱input沖突。
YY_FLUSH_BUFFER 刷新解析器內部緩存以便于下一次的匹配工作,首先它會使用YY_INPUT填充緩存區。這是通用yy_flush_buffer()的一個特例,將會在多輸入緩存中描述。
yyterminate()可以在動作內部返回描述區域中使用,它將終止解析器并返回0給解析器調用者,表示操作完成。缺省情況下,到達文件結束位置也會被調用,它是一個宏,并且可能重定義。
Lex進階
模式
模式在第一階段或第二個階段使用,也就是在申明或規則階段中出現,模式定義了匹配的目標,目標被匹配后將會執行動作。
對于模式不想做太多說明,使用正則表達式定義,可以參看 regex 或 pcre.
開始條件
lex提供了根據條件激活規則的機制。在<sc>前綴的規則將會在解析器在"sc"的開始條件下被匹配。
<STRING>[^"]* { /* eat up the string body ... */ ... }
將會在啟動條件"STRING"的情況下被激活。
<INITIAL,STRING,QUOTE>\. { /* handle an escape ... */ ... }
將會在 "INITIAL", "STRING", "QUOTE"三者之一的條件下被激活。
開始條件在輸入源的定義(第一)部分被申明,在‘%s' 或 ’%x'后跟隨著名字列表。 %s申明了包含的開始條件,%x申明了排他的開始條件。開始條件被BEGIN動作激活。直到下一個BEGIN動作,滿足開始條件名稱的規則將會被規則,不滿足啟動條件的規則將不會被執行。
如果是包含條件,沒有開始條件的規則也會被激活執行,如果時排他條件,只有滿足開始條件的規則才會被執行。
具有相同排他條件的規則的集合可以使解析器獨立于其他的規則。因此,排他條件可以容易地創建微型解析器處理輸入源中的獨立與其他部分的一部分(如,注釋)。如果對于包含與排他條件還有混淆,可以看下面的例子。
%s example%%<example>foo do_something();bar something_else();
等同于
%x example%%<example>foo do_something();<INITIAL,example>bar something_else();
上面的程序中如果沒有<INITIAL,example>,在example條件下bar規則將永遠不會被激活。如果使用<example>,將會導致只能在exmaple開始條件下激活,而INITIAL條件下不會被激活。而第一個程序中在任何條件下bar都被會激活。因為第一個程序用example時%s,時包含條件。頁可以通過特殊開始條件<*>來配置任何開始條件,上面的程序還可以寫為:
%x example%%<example>foo do_something();<*>bar something_else();
缺省規則(顯示任何未被匹配的字符)在開始條件下仍然生效。等同于:
<*>.|\\n ECHO;
‘BEGIN(0)’在無開始條件的規則激活條件下返回原始狀態,這個狀態同于開始條件下的'INITIAL',所以‘BEGIN(INITIAL)'等同于’BEGIN(0)'。
BEGIN行為在規則部分的開頭是默認的代碼(BEGIN actions can also be given as indented code at the beginning of the rules section.請翻譯)例如,下面的代碼將會僅需SPECIAL開始條件,不管合適yylex()被調用并且全局變量enter_special是true。
int enter_special;%x SPECIAL%% if ( enter_special ) BEGIN(SPECIAL);<SPECIAL>blahblahblah...more rules follow...
為了說明開始條件,我們用兩種方法處理"123.456".缺省將會被解析為 '123','.','456'三個標記,如果expect-floats后面將會被解析為浮點數 123.456
%{#include <math.h>%}%s expect%%expect-floats BEGIN(expect);<expect>[0-9]+"."[0-9]+ { printf( "found a float, = %f\n", atof( yytext ) ); }<expect>\n { /* that's the end of the line, so * we need another "expect-number" * before we'll recognize any more * numbers */ BEGIN(INITIAL); }[0-9]+ { printf( "found an integer, = %d\n", atoi( yytext ) ); }"." printf( "found a dot\n" );
下面的代碼能夠是被C語言注釋并且統計行數。
%x comment%% int line_num = 1;"/*" BEGIN(comment);<comment>[^*\n]* /* eat anything that's not a '*' */<comment>"*"+[^*/\n]* /* eat up '*'s not followed by '/'s */<comment>\n ++line_num;<comment>"*"+"/" BEGIN(INITIAL);
實際上,編寫高速解析程序的辦法時在每個規則中做盡可能多的匹配。
This scanner goes to a bit of trouble to match as much text as possible with each rule. In general, when attempting to write a high-speed scanner try to match as much possible in each rule, as it's a big win.
注意: 開始條件的名字實際上時一個整形值并且能夠被保存,所以,上面的代碼可以擴展為:
%x comment foo%% int line_num = 1; int comment_caller;"/*" { comment_caller = INITIAL; BEGIN(comment); }...<foo>"/*" { comment_caller = foo; BEGIN(comment); }<comment>[^*\n]* /* eat anything that's not a '*' */<comment>"*"+[^*/\n]* /* eat up '*'s not followed by '/'s */<comment>\n ++line_num;<comment>"*"+"/" BEGIN(comment_caller);
而且,可能易使用YY_START宏來訪問當前的開始條件。如上面的賦值條件可以改寫為
comment_caller = YY_START
YYSTATE是YY_START的別名(因為AT&T lex使用了YYSTATE)。
注意 開始條件沒有他們的名字空間; %s 與 %x 申明與 #define形式一樣。
到這里,時一個使用排他開始條件如何匹配C風格的引用字符串的處理。包含的擴展的轉義,但不包括檢查,因為代碼太長。
%x str%% char string_buf[MAX_STR_CONST]; char *string_buf_ptr;\" string_buf_ptr = string_buf; BEGIN(str);<str>\" { /* saw closing quote - all done */ BEGIN(INITIAL); *string_buf_ptr = '\0'; /* return string constant token type and * value to parser */ }<str>\n { /* error - unterminated string constant */ /* generate error message */ }<str>\\[0-7]{1,3} { /* octal escape sequence */ int result; (void) sscanf( yytext + 1, "%o", &result ); if ( result > 0xff ) /* error, constant is out-of-bounds */ *string_buf_ptr++ = result; }<str>\\[0-9]+ { /* generate error - bad escape sequence; something * like '\48' or '\0777777' */ }<str>\\n *string_buf_ptr++ = '\n';<str>\\t *string_buf_ptr++ = '\t';<str>\\r *string_buf_ptr++ = '\r';<str>\\b *string_buf_ptr++ = '\b';<str>\\f *string_buf_ptr++ = '\f';<str>\\(.|\n) *string_buf_ptr++ = yytext[1];<str>[^\\\n\"]+ { char *yptr = yytext; while ( *yptr ) *string_buf_ptr++ = *yptr++; }
通常,如上面的例子中所看到你,會有許多相同開始條件的處理。開始條件范圍可以簡化重復操作。
<SCs>{}
SCs 是一個或開始條件的列表。在這個開始條件范圍內,每個規則將會自動具有前綴 `<SCs>' 直到 `}' 與開始的 `{' 匹配. 例如
<ESC>{ "\\n" return '\n'; "\\r" return '\r'; "\\f" return '\f'; "\\0" return '\0';}
等價于
<ESC>"\\n" return '\n';<ESC>"\\r" return '\r';<ESC>"\\f" return '\f';<ESC>"\\0" return '\0';
開始條件頁可以嵌套,下面時三個管理開始條件堆棧的參數。
`void yy_push_state(int new_state)'
將當前的開始條件壓棧,切換到 new_state 與使用 `BEGIN new_state'類似。
`void yy_pop_state()'
從棧頂彈出,類似于 BEGIN.
`int yy_top_state()'
返回棧頂值,不改變棧內容。
開始條件棧動態增長,沒有固定限制,如果內容用盡,程序竟會終止。
為了使用開始條件棧,需要使用 `%option stack' 指令。
多輸入緩存區
一些允許include文件解析器的解析器要求從幾個輸入流中讀取內容。YY_INPUT只在結束緩存時被調用,碰到 include 后需要切換輸入源,而解析一個描述也許需要很長時間。為了解決此類問題,解析器提供了創建并在多個輸入緩存中創建的機制。輸入緩存可以通過下面的方式創建:
YY_BUFFER_STATE yy_create_buffer( FILE *file, int size )
參數為與緩存關聯的輸入文件指針,以及足夠的可維持size字符(如果不確定,size可以使用YY_BUF_SIZE)。返回一個YY_BUFFER_STATE,可以傳遞到其他的處理過程。YY_BUFFER_STATE是一個不可見結構yy_buffer_state的指針,所以可以安全地使用`((YY_BUFFER_STATE) 0)'來初始化YY_BUFFER_STATE,如果你愿意,你可以在解析器之外的源程序中引用這個不透明結構來正確的申明輸入緩存。可以通過下面的參數來選擇一個緩存區。
void yy_switch_to_buffer( YY_BUFFER_STATE new_buffer )
切換解析器的輸入緩存將會導致記接下來的匹配項來自于新的緩存中。yy_switch_to_buffer可能出現在yywrap中為繼續解析做準備,替換打開一個新的文件并執行yyin. 通過yy_switch_to_buffer 或 yywrap切換輸入源不改變開始條件。
void yy_delete_buffer( YY_BUFFER_STATE buffer )
用于收回與緩存關聯的空間。你可以使用下面的函數清空當前內容:
void yy_flush_buffer( YY_BUFFER_STATE buffer )
此函數廢棄緩存內容,下一個解析器試圖匹配一個內容時將會使用YY_INPUT來更新緩存區。
`yy_new_buffer()' 是 `yy_create_buffer()' 的一個別名,用于提供C++使用new 與 delete操作創建與銷毀動態對象的兼容性。
最后, YY_CURRENT_BUFFER 宏返回 YY_BUFFER_STATE 指針,表示當前的緩存。
這里是一個擴展include使用的一個解析器 (`<<EOF>>' 特性將會在以后討論):
/* "incl" 狀態用于獲取include的文件名 */
%x incl
%{
#define MAX_INCLUDE_DEPTH 10
YY_BUFFER_STATE include_stack[MAX_INCLUDE_DEPTH];
int include_stack_ptr = 0;
%}
%%
include BEGIN(incl);
[a-z]+ ECHO;
[^a-z\n]*\n? ECHO;
<incl>[ \t]* /* eat the whitespace */
<incl>[^ \t\n]+ { /* got the include file name */
if ( include_stack_ptr >= MAX_INCLUDE_DEPTH )
{
fprintf( stderr, "Includes nested too deeply" );
exit( 1 );
}
include_stack[include_stack_ptr++] =
YY_CURRENT_BUFFER;
yyin = fopen( yytext, "r" );
if ( ! yyin )
error( ... );
yy_switch_to_buffer(
yy_create_buffer( yyin, YY_BUF_SIZE ) );
BEGIN(INITIAL);
}
<<EOF>> {
if ( --include_stack_ptr < 0 )
{
yyterminate();
}
else
{
yy_delete_buffer( YY_CURRENT_BUFFER );
yy_switch_to_buffer(
include_stack[include_stack_ptr] );
}
}
提供三個過程來實現內存字符串而不是文件輸入緩存的解析。它們都要創建一個輸入緩存來解析字符串,并且返回YY_BUFFER_STATE (可以在完成解析后用 `yy_delete_buffer()' 刪除).,也可以通過`yy_switch_to_buffer()'來切換, 下一次調用`yylex()' 將會解析字符串。
`yy_scan_string(const char *str)' 解析0結尾字符串。
`yy_scan_bytes(const char *bytes, int len)' 解析bytes開始的len個字符(可能包含 0 字符)
注意,上面的兩個函數會創建字符串或字節串的副本。(這也許時期望的,因為`yylex()' 會修改被解析緩存的內容) 可以使用下面的方式來拒絕使用副本:
`yy_scan_buffer(char *base, yy_size_t size)'
將會從base開始解析,包含size個字節, 最后的兩個字節必須是 YY_END_OF_BUFFER_CHAR (ASCII NUL)。他們不會被解析, 解析范圍從 `base[0]' 到 `base[size-2]'(包含)。如果你沒能按照這種規定使用base(如,忘記了最后的兩個YY_END_OF_BUFFER_CHAR字節), `yy_scan_buffer()' 將會返回空指針而不創建YY_BUFFER_STATE。yy_size_t類型是個整型,可以轉化為整數來反映buffer的長度。
文件結束規則
特殊規則 "<<EOF>>" 只是規則在文件結束位置發生且yywrap()返回非0值。(如,沒有更多的文件要處理). 這個動作必須完成下面四件事情之一:
賦值給yyin一個新的文件 (早期版本的flex, 此操作后必須調用特殊動作 YY_NEW_FILE; 這個操作已經不需要了);
執行一個返回申明;
執行一個特殊的`yyterminate()' 動作;
或者使用`yy_switch_to_buffer()' 切換到一個新的輸入緩存區.
<<EOF>> 不能與其他模式一起使用;它也許僅在開始條件列表申明。如果指定了不合法 <<EOF>> 規則, 它將會應用到所有的開始條件而不僅是 <<EOF>> 動作. 指定 <<EOF>> 規則僅在 initial 開始條件下匹配,就是用:
<INITIAL><<EOF>>
下面的規則可以發現象不關閉的注釋類的問題。
%x quote
%%
...other rules for dealing with quotes...
<quote><<EOF>> {
error( "unterminated quote" );
yyterminate();
}
<<EOF>> {
if ( *++filelist )
yyin = fopen( *filelist, "r" );
else
yyterminate();
}