--- tags: Coding --- # lex Lex 是一種工具,用來生成詞法分析器(lexer),將輸入的字符流分割成 "tokens",這在構建編譯器和解釋器中非常重要。Lex 使用基於規則的語法來描述如何將輸入轉換為 tokens。 ## Lex 語法結構 Lex 的語法分為三個主要部分: 1. **宣告區(Declarations)** 2. **規則區(Rules)** 3. **副程式區(User Subroutines)** 在 Lex 文件中,這三個部分以 `%%` 作為分隔。 ### 基本格式 ```c %{ // C 語言代碼部分,用來包含必要的標頭文件或變數聲明 %} %% // 規則部分,每個規則定義一個正則表達式和相應的動作 正則表達式 { 動作 } %% // C 語言的副程式部分,用來實現特定功能或支持程式的運行 ``` ### Lex 的用法 1. 宣告區 這部分用 %{ ... %} 包裹,包含 C 語言代碼,如標頭文件、全局變數和宏定義。 例子 ```c= %{ #include <stdio.h> int num_lines = 0; %} ``` 2. 規則區 這是 Lex 文件的核心部分,用來定義正則表達式及其對應的動作。 正則表達式:用來匹配輸入的模式。 動作:當正則表達式被匹配時執行的 C 語言代碼。 例子 ```c= %% [0-9]+ { printf("找到一個數字:%s\n", yytext); } \n { num_lines++; } . { /* 忽略其他字符 */ } %% [0-9]+:匹配一個或多個數字,並執行 printf 動作。 \n:匹配換行符並計算行數。 .:匹配任何單個字符並忽略它。 ``` 注意: \^放在\[ \]裡面是否定的意思 ex: \[\^A-Z\] => A-Z以外 \^放在\[\]前面是作為每一行開頭讀取的意思 ex: \^\[A-Z\] => 第一個是A-Z ![截圖 2024-10-27 下午3.36.30](https://hackmd.io/_uploads/B1GiTvoxye.png) 3. 副程式區 這部分主要用來放置 C 語言的副程式,如 main() 函數或其他輔助函數。 例子 ```lex= int main() { yylex(); // 呼叫詞法分析器開始分析 printf("總共行數:%d\n", num_lines); return 0; } ``` ### yywrap 在 Lex 生成的詞法分析器中,yywrap() 是一個內建函數,用來在輸入結束時決定是否需要進行進一步的處理。 預設情況下,Lex 期望這個函數在詞法分析器完成所有輸入處理後被調用: 如果 yywrap() 返回 0,則表示還有更多輸入需要處理; 如果返回 1,則表示輸入結束。 ![image](https://hackmd.io/_uploads/HkzE3Pjg1l.png) 假設你有以下 Lex 文件,但未加入 %option noyywrap: ```c= %{ #include <stdio.h> %} %% [0-9]+ { printf("找到數字:%s\n", yytext); } %% int main() { yylex(); return 0; } ``` 在這個範例中,若沒有 yywrap() 函數的定義,編譯時會出現錯誤。 為了解決這個問題,你需要加上 yywrap() 定義: ```c= %{ #include <stdio.h> int yywrap() { return 1; } %} ``` ### 生成與運行詞法分析器 編寫 Lex 文件(如 example.l)。 使用 lex 命令生成詞法分析代碼: ```bash= flex example.l ``` 使用 C 編譯器(如 gcc)編譯生成的詞法分析代碼: ```bash= gcc lex.yy.c -ll -o example ``` 執行生成的可執行文件: ```bash= ./example < input.txt ``` ![image](https://hackmd.io/_uploads/H1o99Dsxye.png) ## 例子 ![image](https://hackmd.io/_uploads/HJaUZ22xye.png) ![image](https://hackmd.io/_uploads/S1qD-33eJl.png) ### 測資 ```md= 123; 12jj12 -54564 12 9- 0.00002 -222.1111; A_XX2; ABD_UU_X ABD_UU_X~ .12345 1.0.1.0; CACHE_ ; A A A~ 1 1 3 4 A A ~ab ~df aa _ABX 123ABC; ~ . 23; ``` ### 程式 ```c= %{ #include <stdio.h> #include <string.h> int int_count = 0; int float_count = 0; int id_count = 0; int error_count = 0; %} %option noyywrap %% [a-zA-Z][a-zA-Z0-9_]* { printf("Identifier(ID) : %s\n", yytext); id_count++; } [a-zA-Z][a-zA-Z0-9_]*[^\x00-\x1Fa-zA-Z0-9_\n]+.* {printf("Error : %s\n", yytext);error_count++;/*只要不符合上述就會報錯*/} [-]?[0-9]+ { printf("Integer : %s\n", yytext); int_count++; } [-]?[0-9]+[^0-9.\x00-\x1F\n]+.* { printf("Error : %s\n", yytext); error_count++; } ^[-]?[0-9]+\.[0-9]+ { printf("Float : %s\n", yytext); float_count++; } ^[-]?[0-9]+\. { printf("Float : %s\n", yytext); float_count++; } ^[-]?[0-9]+\.[^.\x00-\x1F0-9\n]+.* {printf("Error : %s\n", yytext);error_count++;} ^[-]?[0-9]+\.[0-9]+[^.\x00-\x1F0-9\n]+.* {printf("Error : %s\n", yytext);error_count++;} [0-9]+\.[0-9]+\..* {printf("Error : %s\n", yytext);error_count++;} ^[^ \t\n]+[ \t]+[^ \t\n]+.* { printf("Error : %s\n", yytext); error_count++; } \n { /* 忽略换行符 */ } [ \t] {} ^[^ \-a-zA-Z0-9].* { if(yytext[0]>=' ')printf("Error : %s\n", yytext);//因為會把控制符號也一起掃進去 error_count++; } %% int main() { yylex(); printf("Total_Integer : %d\n", int_count); printf("Total_Float : %d\n", float_count); printf("Total_Identifier(ID) : %d\n", id_count); printf("Total_Error : %d\n", error_count); return 0; } ``` ### 解釋 **讀取ID** \\x00-\\x1F排除掉空白前面的控制字串 \[\^\\x00-\\x1Fa-zA-Z0-9_\\n\]排除非英文、數字、底線、控制字符。也就是特殊字元 \^放在\[ \]裡面是否定的意思 \^放在\[\]前面是作為第一行讀取的意思 ```c= [a-zA-Z][a-zA-Z0-9_]* { printf("Identifier(ID) : %s\n", yytext); id_count++; } [a-zA-Z][a-zA-Z0-9_]*[^\x00-\x1Fa-zA-Z0-9_\n]+.* {printf("Error : %s\n", yytext);error_count++;/*只要不符合上述就會報錯*/} ``` **讀取整數** \?表示0個或1個 .*表示遇到特殊字元後 後面的東西不管是什麼都要讀掉 ```c= [-]?[0-9]+ { printf("Integer : %s\n", yytext); int_count++; } [-]?[0-9]+[^0-9.\x00-\x1F\n]+.* {printf("Error : %s\n", yytext);error_count++;} ``` **讀取小數點** ![未命名的筆記本](https://hackmd.io/_uploads/ry5jpcneJx.jpg) 上面四種可能性 都有分別對應方法 ```= ^[-]?[0-9]+\.[0-9]+ 正確:1.1 ^[-]?[0-9]+\. 錯誤:9. ^[-]?[0-9]+\.[^.\x00-\x1F0-9\n]+.* 錯誤:9.~ ^[-]?[0-9]+\.[0-9]+[^.\x00-\x1F0-9\n]+.* 錯誤:9.9~ [0-9]+\.[0-9]+\..* 錯誤:9.9.0 ``` ```c= ^[-]?[0-9]+\.[0-9]+ {printf("Float : %s\n", yytext); float_count++; } ^[-]?[0-9]+\.{printf("Float : %s\n", yytext); float_count++; } ^[-]?[0-9]+\.[^.\x00-\x1F0-9\n]+.* {printf("Error : %s\n", yytext);error_count++;} ^[-]?[0-9]+\.[0-9]+[^.\x00-\x1F0-9\n]+.* {printf("Error : %s\n", yytext);error_count++;} [0-9]+\.[0-9]+\..* {printf("Error : %s\n", yytext);error_count++;} ``` **讀取空白以及其他** ```c= ^[^ \t\n]+[ \t]+[^ \t\n]+.* { printf("Error : %s\n", yytext); error_count++; } \n { /* 忽略换行符 */ } [ \t] { /* 忽略空白與tab */ } ^[^ \-a-zA-Z0-9].* { //只要開頭是特殊字元就跳過直到換行 if(yytext[0]>=' ')printf("Error : %s\n", yytext);//因為會把控制符號也一起掃進去 error_count++; } ```