在介紹 PEG.js 以前,咱們先來講下咱們爲何須要它。javascript
假若有這樣一個場景:用戶輸入一句簡單的 sql 查詢語句 select name from user
, 咱們的任務是獲取到裏面的列名,表名。在現有工具下,咱們第一個想到的就是正則表達式,熟悉的同窗應該能很快寫出這樣的正則表達式:java
/^select\s+([A-Za-z_]*)\s+from\s+([A-Za-z_]*)$/.exec("select name from user")
//["select t from", "name", "user"]
複製代碼
完成了以後發現正則表達式稍微有點長,可是總體結構仍是蠻簡單的。不過仔細測試發現,咱們的正則表達式考慮的還不是很周全,例如: select * from user
, select user.name from user
這兩種狀況咱們都沒考慮到,因此咱們還須要完善一下正則表達式node
/^select\s+([A-Za-z_]*|\*|[A-Za-z_]*.[A-Za-z_]*)\s+from\s+([A-Za-z_]*)$/.exec("select user.name from user")
//["select user.name from user", "user.name", "user"]
複製代碼
很明顯,正則表達式已經比較長了,看過去會有一點眼花,但是,這條正則表達式其實並不完善,例如沒有考慮到多列名的狀況 select name, age from user
,和多表的狀況 select user.name, class.name from user,class
,假如繼續擴展下去,咱們的正則表達式將會變得十分龐大,而且當別人來修改本身的代碼的時候,也會變得十分困難,長此以往,就會變得難以維護。正則表達式
咱們仔細思考一下上面的流程,其實正則表達式主要幫助咱們作了一件事,那就是將一條 sql 語句轉化爲結構化的數據 ["select t from", "name", "user"]
,從而咱們根據結構化的數據來獲取本身想要的信息。sql
首先,咱們將 PEG.js 安裝到全局下npm
npm install -g pegjs
複製代碼
安裝好了能夠驗證一下數組
$ pegjs -v
PEG.js 0.10.0
複製代碼
而後咱們在新建一個目錄用來存放咱們代碼。本文的結構以下bash
peg
文件夾用來存放咱們的語法文件,
dist
文件夾用來存放咱們生成的 parser。建立完項目以後,咱們下面就開始來正式使用 PEG.js 來解析 sql。
首先,咱們先肯定一下咱們要實現的 select 語句,本文爲了便捷,不會編寫完善的語法文件,因此這裏以最簡單的 select 語句爲例子。ide
select
column_name | *
from
table_name;
複製代碼
在 PEG.js 文件的開頭,咱們來寫下第一個規則 ,PEG.js 默認從第一個規則開始解析。工具
start
= selectStatement
複製代碼
這裏咱們定義了一個 start
規則,而這個 start
的解析表達式由一個 selectStatement
規則組成,而這個 selectStatement
就是咱們剛剛定義的 select 語句了。
那麼按照咱們以前的定義,selectStatement
又由什麼組成呢?
通過一些思考,咱們應該能寫下如下規則
selectStatement
= select colunm_clause from table_name ';'
複製代碼
selectStatement
的解析表達式由5個部分組成,其中,';'
表明一個分號字符串,來表明一條 sql 語句的結尾,而其餘4個則表明 selectStatement
解析表達式的四個組成部分。因此,咱們只要從 selectStatement
開始自頂向下 ,爲每個規則添加解析表達式就行了。
selectStatement
= select colunm_clause from table_name ';'
colunm_clause
= column_name
/ '*'
column_name
= ident_part+
table_name
= ident_part+
ident_part
= [A-Za-z0-9]
select
= 'select'i
from
= 'from'i
複製代碼
這裏 colunm_clause
的解析表達式有一個 /
,它表明 或
的意思,意思是 colunm_clause
是由 column_name
或者 一個 *
組成。 除此以外,還能夠發現,有不少語法和 JS 的正則表達式是類似的,好比
[A-Za-z0-9]
ident_part+
'from'i
分別表明的意思是
ident_part
規則from
的大小寫編寫完了以後咱們嘗試將這個語法文件編譯成 JS 可使用的 parser
pegjs -o dist/selectParser.js peg/select.peg
複製代碼
咱們這裏指定了一個編譯輸出文件 dist/selectParser.js
和待編譯的語法文件 peg/select.peg
,運行結束後,咱們能夠在 dist 文件夾中看見看見咱們的 parser 文件 selectParser.js
。
生成 parser 文件以後就是使用它了
const selectParser=require("./dist/selectParser.js");
console.log(selectParser.parse("select name from user;"))
複製代碼
這裏調用了 parser 文件的 parse 方法來開始解析,不過,咱們卻獲得了一個錯誤
> node index.js
SyntaxError: Expected "*" or [A-Za-z0-9] but " " found.
複製代碼
這裏的意思是,parser但願收到 "*"
或 [A-Za-z0-9]
,不過卻獲得了一個空字符串。回到咱們的語法解析文件開頭
selectStatement
= select colunm_clause from table_name ';'
複製代碼
只有 colunm_clause
是由 "*"
和 [A-Za-z0-9]
組成的,那看來是 parser 把咱們的輸入 select name from user;
中 select
和 name
中間的空格看成 colunm_clause
來解析了,致使報錯,因此咱們須要再完善一下原來的語法文件,加入空格的解析。
selectStatement
= select _ colunm_clause _ from _ table_name __ ';'
__
= whitespace*
_
= whitespace+
whitespace
= [ \t\r\n];
複製代碼
如今咱們再次編譯運行,咱們獲得了正確的輸出
> node index.js
[ 'select',
[ ' ' ],
[ 'n', 'a', 'm', 'e' ],
[ ' ' ],
'from',
[ ' ' ],
[ 'u', 's', 'e', 'r' ],
[],
';'
]
複製代碼
如今結構是解析出來了,不過,展現上卻不是很是的美觀,好比 name
和 user
被拆分紅了字符數組,這是由於咱們在 table_name
和 column_name
中使用了 +
,因此輸出的時候,也會變成數組輸出;還有,咱們其實並不須要空白字符,可是結果裏卻也包含了它 [ ' ' ]
。所以,咱們須要再處理一下匹配的數據。
selectStatement
= select _ colunm_clause:colunm_clause _ from _ table_name:table_name __ ';'{ return `column_name=${colunm_clause}, table_name=${table_name}`}
column_name
= name:ident_part+ {return name.join("")}
table_name
= name:ident_part+ {return name.join("")}
複製代碼
這裏咱們用到了 PEG.js 的 action ,咱們能夠在 aciton 裏面寫 JS 代碼,它會在規則匹配成功的時候執行,同時,咱們也給規則取了名字(例如 name:ident_part+
中的 name
),以便於咱們在 action 中使用。
如今,再次運行編譯執行過程,不出意外,咱們能夠獲得如下輸出
> node index.js
column_name=name, table_name=user
複製代碼
這樣,一個最最基礎的 select 語句解析就完成了~咱們能夠錦上添花,讓它支持多條 sql 語句。完整代碼以下:
PEG
start
= selectStatements:selectStatement*
{
return selectStatements.join("\n")
}
selectStatement
= select _ colunm_clause:colunm_clause _ from _ table_name:table_name __ ';'{ return `column_name=${colunm_clause}, table_name=${table_name}`}
colunm_clause
= column_name
/ '*'
column_name
= name:ident_part+ {return name.join("")}
table_name
= name:ident_part+ {return name.join("")}
ident_part
= [A-Za-z0-9]
select
= 'select'i
from
= 'from'i
__ = whitespace*
_ = whitespace+
whitespace
= [ \t\r\n];
複製代碼
JS
const addParser=require("./dist/addParser.js");
const selectParser=require("./dist/selectParser.js");
const sqls=[
'select * from user;',
'select name from user;',
'select id from user;'
].join("")
console.log(selectParser.parse(sqls))
複製代碼
運行
> node index.js
column_name=*, table_name=user
column_name=name, table_name=user
column_name=id, table_name=user
複製代碼
回過頭來,咱們發現,藉助 PEG.js 編寫的 parser 很容易維護,整個語法的描述都是有結構的,而且藉助於 action ,咱們能夠很方便的返回咱們所須要的結構。
和正則表達式相比,惟一的缺點就是 PEG.js 生成的 parser 更佔空間,加載上相對慢一些,不過,咱們開發效率和代碼的可維護性也有了比較大的提高,綜合比較下, PEG.js 仍是一個很不錯的解決方案。