用lpeg解析文本语法
所谓语法解析,就是将文本中符合既定规则的子串提取出来。欲解析,先要找出文本的既定规则;欲写出代码,先要将规则从抽象域转为自然语言的形式域,就像人们把数学概念用符号固化下来。这种符号,先辈们早已给出了方案,最常用的是BNF。lpeg便是自然地契合BNF的,这也是它与正则表达式等模式匹配库最大的不同,也是它最大的优势,它把每个模式对象作为lua的第一类对象,也就是可以存储于变量中,模式对象间可以相互运算,用BNF来看,每个模式对象便是非终结符,其定义便是终结符。如匹配一个email:
- 用正则表达式可以为:
^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$
- 用lpeg可以为:
local lpeg = require "lpeg"
local R = lpeg.R
local C = lpeg.C
local alpha = R"az" + R"AZ" + R"_"
local num = R"09"
local world = alpha + num
local email = world^1 * "@" * world^1 * "." * world^1
lpeg看起来不更清晰,更接近自然语言些吗?
下面用一个复杂些的例子来说明lpeg的运用,来解析C语言的结构体定义,然后将提取的类型定义保存在一张表中。这张表为一个数组,每个元素的结构如下:
--[[
struct student{
char *name;
int age;
};
--]]
{
name = student
fields = {
[1] = {"char","*","name"},
[2] = {"int","age"}
}
}
我们用的文本如下:
//学生
struct student{
char *name;
int age;
};
//账号
struct account{
char *user;
char pwd[32];//md5
};
第一步,把文本的规则形式化,用EBNF描述出来。
struct ::= banks name "{" banks {field} banks"};" banks
space ::= " " | "\t"
newline ::= "\r\n" | "\n"
name ::= alpha | "" {world}
world = alpha | num | ""
alpha ::= lower | upper
lower ::= "a"|"b"|"c"|"d"|"e"|"f"|"g"|"h"|"i"|"j"|"k"|"l"|"m"|"n"|"o"|"p"|"q"|"r"|"s"|"t"|"u"|"v"|"w"|"x"|"y"|"z"
num ::= "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"
upper ::= 同lower的大写字母形式
bank ::= newline | comment | space
banks ::= {bank}
comment ::= "//" {.}
field ::= type space | {space} ptr {space} name [array] ; banks"
space ::= " " | "\t"
type ::= name
ptr ::= {"*"}
array ::= "[" {num} "]"
第二步,把EBNF转为lpeg模式
local lpeg = require "lpeg"
local P = lpeg.P
local S = lpeg.S
local R = lpeg.R
local C = lpeg.C
local Ct = lpeg.Ct
local Cg = lpeg.Cg
local Cc = lpeg.Cc
local V = lpeg.V
local Carg = lpeg.Carg
local Cmt = lpeg.Cmt
local alpha = R"az" + R"AZ" + "_"
local num = R"09"
local space = P" " + P"\t"
local newline = P"\r\n" + "\n"
local comment = P"//" * (1 - newline)^0
local bank = space + newline + comment
local banks = bank^0
local world = alpha + num + "_"
local name = C(alpha * world^1)
local dtype = name
local array = C(P"[" * num^0 * "]")
local ptr = C(P"*")
local function multipat(...)
local pat = P" "^0
local pp = {...}
for _,v in ipairs(pp) do
pat = pat * v
end
return Ct(pat)
end
local field = multipat(banks * dtype * space^0 * ptr^0 * space^0 * name * array^0 * ";" * banks)
local struct = multipat(banks * "struct" * space^0 * Cg(name,"name") * "{" * banks * Cg(Ct(field^0),"fields") * banks * "};" * banks)
local typedef = Ct(struct^0)
--解析
local r = typedef:match(text)
用lpeg,要以BNF为指导,然后根据保存形式选择合适的捕获。它虽然很容易提取符合语法的子串,但无法检测子串的合法性,如字段的数据类型是否有效,其合法性就需要另外检测了。