1. 程式人生 > >flex&bison 學習筆記

flex&bison 學習筆記

最近在學習開源資料庫 PostgreSQL 的解析器部分,需要用到 flex 和 bison,花了一些時間學習了一下,把目前已經搞清楚的地方和大家交流交流。如果有不對的地方,歡迎指正大笑

1. flex 和 bison 是什麼東西?用來幹什麼的?

flex和 bison 是兩個用來生成程式的工具(這兩個工具的用途很廣泛,這裡只說它在 PostgreSQL 中的使用)。在 PostgreSQL 中,它們被用來生成 SQL 語句的詞法和語法分析器。注意,flex 和 bison 本身不是詞法或語法分析器,利用它們生成的程式才是。即:



2. 詞法和語法分析器是怎麼工作的?

任何一種語言,都是有一定的語法規則的,不管是人類的語言,還是計算機語言(如 C/C++ 程式語言等),因此,可以利用這些已知的規則,來對對應的語言進行分析。舉個例子,漢語中的一個句子,基本的格式是:主(名詞/代詞)+ 謂(動詞)+ 賓(名詞/代詞),當你在說一句話的時候,我們把你說的話(輸入)先拆分成一個個有意義的字(或詞),然後對照該語法,看詞性及組合,是否符合既定的語法規範,如果符合,則可以知道你說的話是符合規範的。比如,你說“我吃飯”,輸入會被依次拆成“我”“吃”“飯”,他們分別是代詞、動詞、名詞,因此符合上面的語法規則,因此這句話是 ok 的。而如果說“我飯吃”,則會發現與上面的規範不符合(也沒有其他符合的規範),因此這句話語法上是有問題的。flex 和 bison 生成的詞法和語法分析器就是乾的這兩件事:flex 生成的詞法分析器將輸入拆分成一個一個的記號(token),bison 生成的語法分析器根據已有的規則,分析這些 token 的組合,是否是符合語法規範的。


3. flex/bison 原始檔格式

上面已經提到,flex/bison是通過處理其原始檔來生詞法和語法分析器的,flex 原始檔的副檔名為 .l,bison 原始檔的副檔名為 .y。它們的格式比較類似,分為三個段:

         /* 定義段 */

%{

%}

%%

         /* 規則段 */

%%

         /* 使用者子程式段 */

三個段用 %% 進行分離,

(1)定義段,這一部分一般是一些宣告及選項設定等。C 語言的註釋、標頭檔案包含等一般就放在 %{ %} 之間,這一部分的內容會被直接複製到輸出檔案的開頭部分,選項後面再說;

(2)規則段為一系列匹配模式和動作,模式一般使用正則表示式書寫,動作部分為 C 程式碼:

模式1            {動作1(C 程式碼) }

          在輸入和模式 1 匹配的時候,執行動作部分的程式碼;

(3)使用者子程式段,這裡為 C 程式碼,會被原樣複製到輸出檔案中,一般這裡定義一些輔助函式等,如動作程式碼中使用到的輔助函式。

詞法分析器所做的,就是在輸入中尋找字元的模式(pattern)。在詞法分析器中,我們要給定我們需要識別的模式,因此需要使用一種方式來描述模式,這就是常用的正則表示式。正則表示式在這裡不多講,如果完全不瞭解,可以問問谷歌或者度娘,花個十幾二十分鐘看看最基本的部分,基本就能應付現在的需要了。來看一個簡單地例子,實際地認識認識(強烈建議自己動手敲程式碼實踐哦!)。

4. flex 小試牛刀

編寫以下 flex 原始檔(flex_1.l,注意字尾名是 l (字母 L)哦!):

%{

/* 這裡的部分會被直接拷貝到生成的 .c 檔案的開始部分,在這裡可以包含需要使用的標頭檔案,如 stdio.h

*/
#include <stdio.h>

%}

         /* 下面這個 %% 就表示定義段結束了,規則段開始了 */

%%

/* 規則段開始了,下面定義了四條規則,前面的部分就是模式,處於一行的開始位置,後面的部分就是動作,也就是,輸入中匹配到了這個模式的時候,對應的進行什麼動作,就像你蹲在戰壕裡,前面出來一個人,你通過匹配知道是敵人,你就開槍一樣。第一個模式也就是匹配連續的一個到多個字元,匹配到之後就將其打印出來。注意到 yytext,在輸入匹配到該模式的時候,匹配的部分就儲存在這個 yytext 裡面啦!這裡把它作為字串直接輸出就可以了;第二條規則的模式部分,就是匹配連續的一個或者多個數字,匹配到了之後,也是以字串的形式輸出(對於輸入,就是一連串的字元,什麼轉換之類的,就得我們自己操心了);第三條規則的模式部分,就是匹配一個換行符了,並且匹配到之後就列印一個新行的資訊;第四條規則的模式部分,是一個點。正則表示式裡面這個也就是匹配任何出了 \n 之外的字元。因此,下面的遊戲規則就是,匹配到英語單詞(當然,不一定是正確的單詞,比如 niubi,暫時先不計較這個),則將該單詞輸出,匹配到連續數字,將其輸出;匹配到換行符,列印一條資訊;匹配到任何其他的字元,則直接忽略({} 也就是動作是空的,就是什麼都不做了。什麼都不做了那程式該幹啥呢?當然是繼續識別後面的輸入啦!) */

[a-zA-Z]+                    { printf(“get word: %s”, yytext); }

[0-9]+                         {printf(“get number:%s”, yytext); }

\n                                {printf(“New line\n”); }

.                                   { }

%%

/* 規則段之後就是使用者子程式段了。這裡現在我們就空著,先重點關注一下規則段的再說。其實上面這第二個%% 也是可以去掉的哦!不妨動手試試吧! */

好了,flex 原始檔寫好了,你一定迫不及待想看看跑起來的樣子吧?現在就開始(找個有 flex 的環境,linux 一般都自帶,如果你實在不想用 Linux,那就在 windows 上裝一個吧!個人建議直接在 windows 上裝一個 cygwin,很多 GNU 的工具都可以使用):

tom@linux-oqq5:~/mycode/flex_bison>flex flex_1.l

tom@linux-oqq5:~/mycode/flex_bison>ll

total 48

-rw-r--r-- 1 tom users   170 May 17 21:53 flex_1.l

-rw-r--r-- 1 tom users 41617 May 1722:08 lex.yy.c

可以看到,生成了一個名字為lex.yy.c 的 C 原始碼檔案,如果有興趣的話,可以開啟該 C 檔案看看。這一步,也就是 flex 通過 flex 原始碼檔案,生成了一個詞法分析器的 C 原始碼檔案了。那麼下一步做什麼呢?當然就是編譯這個詞法分析器的原始碼檔案,生成詞法分析器程式啦(暫且就叫它 scanner 吧,注意要帶上-lfl連結flex的庫,否則會報一些函式找不到)!

tom@linux-oqq5:~/mycode/flex_bison> gcc -o scanner lex.yy.c -lfl

tom@linux-oqq5:~/mycode/flex_bison> ll

total 68

-rw-r--r-- 1 tomusers   170 May 17 21:53 flex_1.l

-rw-r--r-- 1 tom users41617 May 17 22:08 lex.yy.c

-rwxr-xr-x 1 tom users17447 May 17 22:16 scanner

注意到我們在編譯的時候,連結了一個庫 fl,這個是 flex 提供的一個庫,生成的詞法分析器裡面會用到這個庫裡面的一些東西。這個裡面也包括內建的 main 函式,回頭看看,上面的 flex 原始檔中,是不是沒有 main 函式的?最後的入口 main,就是來自這個庫啦!當然,也可以自己寫 main 函式,就在使用者子程式段,不過得呼叫詞法分析器的入口程式才行哦!這個入口函式就是 yylex()。一會兒再看看最後的那個例子吧!現在可以看到,生成了一個可執行程式 scanner,趕緊試試吧!

1 tom@linux-oqq5:~/mycode/flex_bison> ./scanner

2 Hello, scanner! This isa test string: hi, 123

3 get word: Hello

4 get word: scanner

5 get word: This

6 get word: is

7 get word: a

8 get word: test

9 get word: string

10 get word: hi

11 get number: 123

12 New line

為了方便說明,我在上面的各行添加了行號。程式啟動後,等待你的輸入,我們輸入第二行所示的內容,然後敲回車鍵,則輸出如3-12 行所示。從上面的結果可以很清晰地看到,所有的英文單詞都被識別了並輸出了出來(3-10行),所有數字被識別了出來(11行),最後敲的換行符也被識別出來了(12行),而其他的所有符號,如,!:和空格等,都被忽略掉了,從輸出結果中,我們找不到它們的一點蹤跡。

通過上面這個例子,應該知道flex的大概的工作原理了吧!當然,我們這裡舉的例子很簡單,動作部分也就是簡單地列印。那麼,在真實的情況下,又是怎麼樣的呢?其實,道理是一樣的。一般詞法分析器和語法分析器會一起使用,語法分析器會呼叫詞法分析器來讀取輸入,詞法分析器匹配到特定的模式後,就向語法分析器報告(型別和值之類的)。也就是說,詞法分析器的工作,就是讀取、報告、再讀、再報告,而語法分析器的工作,就是呼叫詞法分析器讀取,以及根據詞法分析器的反饋,看輸入是否和語法規則匹配。這些,到後面講到的時候再具體說吧!

再貼一個例子,就不細說了,自己敲敲程式碼玩玩去體會吧!

下面的 flex 原始檔最終生成一個統計輸入行數、單詞數以及字元個數的程式,類似 Linux 上的 wc 命令,注意要結束輸入得到統計結果的時候,按 ctrl + d:

%{

int chars = 0;

int words = 0;

int lines = 0;

%}

%%

[a-zA-Z]+{words++; chars += strlen(yytext); }

\n              { chars++; lines++; }

.               { chars++; }

%%

main(int argc, char **argv)

{

    yylex();

   printf("%8d%8d%8d\n", lines, words, chars);

}

哦,說了這半天,忘了推薦書籍了,呵呵~~我主要就看一本書《flex & bison》,這是曾經的傳說《lex & yacc》的經典延續,上面這個統計的例子就是這本書裡面的。手頭不缺這兩個錢的童鞋,需要的話建議可以留一本在案頭,可隨時查閱。