LuaでTOMLのパースプログラムを書く

今日はlua言語でTOMLフォーマットを読み込んで見ます。
luaってあまり聞かない言語ですが、使ってみるとすごい便利だったりします。
なにが良いかって、文字列が非常に扱いやすいこと、そして軽量。 C言語ライクにプログラムが書ける謎のスクリプト言語って感じです。    基本的なところは以下のサイトで勉強しました。 http://starcode.web.fc2.com

TOMLとは?

JSONのように設定ファイルを記述するフォーマットです。 まだ仕様が固まりきっていないみたいなのですが、人間が読みやすく、かつ、記述しやすいように作られています。 最近はやりのGolangでもよく使われていて、GoBGPなどで設定ファイルとして採用しています。

例えば、以下のように記述すると、

[hoge]
a = "HOGE"
[hoge.foo]
b = 1
[bar]
[[bar.barbar]]
c = 2
[[bar.barbar]]
d = 3

JSONてきにはこんな感じになります。

{"hoge":{"a" : "HOGE"}, {"foo" : {"b": 1 } } }
{"bar" : 
    { "barbar" : [
        { "c" = 2 },
        { "d" = 3 }
        ]
    }
}

二重括弧でくくるだけで配列のような構造ができるので、とても記述しやすいし、いちいち括弧の数を数えなくても良いので、とても読みやすく書きやすいです。

Luaで文字列操作

Luaでの文字列操作は、正規表現を使うことが多くなります。
逆に言えば、正規表現で指定した文字列を簡単に取り出したり、配列に入れたりができるので、使いやすいものです。 よく使う、3つくらいを紹介します。

line = string.gmatch(str,"[^\n]+")

これは、文字列中に含まれる改行コードとその前の文字列をを全て抜き取ってline配列に入れています。
(一行ずづ抜き出しています。)

x = string.split("hello world", /\s+/)

これは、文字列をスペースに区切って配列に入れます。

x = string.sub("ABCDE", 2, 4)

これはABCDE→ BCD に削ります。

Luaの配列みたいなの

LuaではPythonでいうリストのような形で、Tableという型が使われる。 これが面白く、

hoge = {} 
hoge["foo"] = 2
print(hoge.foo)

--[[ 
結果
2
]]--

こんな感じで、入れ子のTableにした場合、要素をドットでつなげることで参照できる。上記プログラム中の"foo"の部分を何も指定しなかった場合、1からインクリメントされる数字が勝手に入ってくるのでご注意。
この辺0からじゃなくて1からってのが、Luaが気持ち悪いって言われる所以なのかもしれません。
確かに気持ち悪いです。

LuaでTOMLを読み込み

ここまでくるとなんとなく、TOMLの読み込み方がわかってきました。
TOMLは行毎に意味を持つので、行毎に読み込んで、構造化し、LuaのTableに入れてるプログラムを書きます。

[Global]
    [Global.GlobalConfig]
    As = 1
    HoldTime = 180
    RouterId = "172.17.0.1"
[Neighbors]
    [[Neighbors.NeighborList]]
        NeighborAddress = "10.0.1.2"
    [[Neighbors.NeighborList]]
        NeighborAddress = "10.0.1.3"

上記のようなTOMLファイル(BGPの設定ファイルっぽいもの)を読み込むようなプログラムを作って見ます。

function split(line,delim)
    local ret = {}
    local pat = "[^" .. delim .. "]+"
    local i = 1
    for group in string.gmatch(line, pat) do
        ret[i] = group
        i = i + 1
    end
    return ret
end

function loadstr(str)
    InLabel = {}
    InLabelArray = {}
    ret = {}
    Label = ret
    ArrayTable = 0
    count = 0
    for line in string.gmatch(str,"[^\n]+") do
        line = string.format( "%s", line:match( "^%s*(.-)%s*$" ))
        if string.sub(line,1,1) == "[" then
            if string.sub(line,1,2) == "[[" then
                ArrayTable = 1
                line = string.sub(line,3,#line -2)
                if InLabelArray[line] == nil then
                    InLabelArray[line] = true
                    count = 0
                end
            else
                line = string.sub(line,2,#line -1)
                ArrayTable = 0
            end
            Label = ret
            arrays = split(line,".")
            for i, value in pairs(arrays) do
                if Label[value] ~= nil then --if in the value
                    if i == table.maxn(arrays) then
                        if InLabel[value] ~= nil then
                            InLabel[value] = nil
                        else
                            print("double")
                        end
                    end
                else
                    if i ~= table.maxn(arrays) then
                        InLabel[value] = true
                    end
                    Label[value] = {}
                end
                Label = Label[value]
            end
            if ArrayTable == 1 then
                count = count + 1
                Label[count] = {}
            end
        elseif line:match("=") ~= nil then
            formula = split(line,"=")
            formula[1] = string.format( "%s", formula[1]:match( "^%s*(.-)%s*$" ))
            formula[2] = string.format( "%s", formula[2]:match( "^%s*(.-)%s*$" ))
            if ArrayTable == 1 then
                Label[count][formula[1]]=formula[2]
            else
                Label[formula[1]]=formula[2]
            end
        end
    end
    return ret
end

以下のように呼び出すことができます。
このプログラムではパラメータを全て文字列で呼び出していますが、このあたりはまだ改変する必要があります。

file = io.open("bgp.conf", "r")
string = file:read("*all")
file:close()

test = loadstr(string)

print(test.Global.GlobalConfig.As)
print(test.Neighbors.NeighborList[1].NeighborAddress)
print(test.Neighbors.NeighborList[2].NeighborAddress)
--[[
実行結果
1
"10.0.1.2"
"10.0.1.3"
]]--