PEGとは

Perl6のsyntax parserです。

正規表現の代用

PEGで出来ることの99%は正規表現でも出来て、正規表現で出来ることの(略)PEGの実装は小さく、大抵Cのラッパなので速度もほぼ変わりませんが、どちらが上とかの話はしません。

Lua.jpg

Luaの正規表現は特殊で、{}や|が無いせいで、ロジックまで変わってしまい、PCREに移植不能になることがあります。PEGならどこにでも持って行けるので、LuaだけはPEGへの全置換を検討しても良いでしょう。

Perlは全域で正規表現を一切使わなければバイナリが小さくなるので、Luaも速度的恩恵があるかも。ないかも。

ini parser

要lpeg

sudo luarocks install lpeg

実装

値をダブルクオートで括る作法があるそうですが考慮していません。クオートがあればクオートも含めて値と見なします。その他、想定していない記述があるかもしれませんが、githubに幾つかあるLuaのini parserの中では一番マシだと思います。

function toLines(str)
    local lines = {}
    for val in str:gmatch("(.-)\r?\n") do
        table.insert(lines, val)
    end
    return lines
end

function parseIni(str)
    local lpeg = require "lpeg"
    local sp = lpeg.P(" ") ^ 0 --0文字以上の空白
    local bb = lpeg.P("[")      -- [
    local eb = lpeg.P("]")      -- ]
    local eq = lpeg.P("=")      -- =
    local cr = lpeg.P("\r")     -- <CR>
    local lf = lpeg.P("\n")     -- <LF>
    local sc = lpeg.P(";")      -- ;
    local key   = (lpeg.P(1) - lpeg.P(" ") - eq) ^1
    local value = lpeg.P(1) ^0
    local ptComment = sp * sc
    local ptSubject = sp * bb * lpeg.C((lpeg.P(1) - eb) ^1) * eb
    local ptBody    = sp * lpeg.C(key) * sp * eq * sp * lpeg.C(value)

    local struct = {}
    local parentName = nil
    local lines = toLines(str)
    local matched
    for i,line in ipairs(lines) do
        if not lpeg.match(ptComment, line) then
            matched = lpeg.match(ptSubject, line)
            if matched then
                parentName = matched
                if not struct[parentName] then struct[parentName] = {} end
            else
                ckey,val = lpeg.match(ptBody, line)
                if ckey then
                    if parentName then
                        struct[parentName][ckey] = val
                    else
                        struct[ckey] = val
                    end
                end
            end
        end
    end
    return struct
end

parseIniに下のようなString型を食わせるとテーブルを返します。

PARENTNAME=parent

[api]
hostname = 192.168.1.111
port = 8080
authUri = /auth

[auth]
username = root
passwprd = rpwd

簡単な解説

リンク張って済ませるとこですが、イマイチ実装向きの解説がないので、PEGについて超簡単に解説します。

意味 正規表現
* 連結
+ または []
- 除外 [^]
^ 連続 {}

今回使ったのはこのぐらい。

(lpeg.P(1) - eb) ^1

の意味は以下です。

任意の1文字 ただし]以外 の1文字以上の連続

先頭から探索

正規表現で

"abc" =~ /b/

はマッチしますが、PEGでは以下のように解釈されてマッチしません。

"abc" =~ /^b/

日本語(UTF8)

lpegでは日本語は単なる3文字(一部4文字)と見なされます。即ちlpeg.P(3)または(4)で日本語1文字です。

再利用性

PEGではなるべく再利用可能な記述でパターンを組み立てるのがスタンダードです。PCREで <h1>foo</h1> のfooを取り出す時

<h1>(.+?)</h1>

などとキャプチャしがちですが、PEGでは以下のようにタグを組み立てた方が後々幸せです。

lt = p("<")
gt = p(">")
h1 = p("h1")
openh1 = lt * h1 * gt
...

Work Plan

  • Lua+PEGでYAMLをparseしたい。
  • Lua+PEGでJSON5をparseしたい。
  • Lua+PEGでlua-templateのDSL作りたい。
  • <% now() | isNotPast ? "OK" : "ISPAST!!" %>