TOML的由来

配置文件的使用由来已久,从.ini、XML、JSON、YAML再到TOML,语言的表达能力越来越强,同时书写便捷性也在不断提升。 TOML是前GitHub CEO, Tom Preston-Werner,于2013年创建的语言,其目标是成为一个小规模的易于使用的语义化配置文件格式。TOML被设计为可以无二义性的转换为一个哈希表(Hash table)。

完全版解析TOML格式

解析TOML格式的第三包使用方法,请使用度娘或谷哥搜索。这方面的资料还是相当丰富的。
比如: https://github.com/BurntSushi/toml
安装方法: go get github.com/BurntSushi/toml


简陋版实现解析TOML

很多时候我们只是把TOML格式用在配置文件中。配置文件并不复杂,可能总共就那么十几行数据。为了解析这十几行数据亲自下手解析TOML格式可就有点纠结了;直接引用第三方包吧似乎又有点轻率。那么接下来的这款轮子正适合你。总共200多行代码纯手工打造原味解析TOML格式。

package toml

import (
    "bufio"
    "errors"
    //"fmt"
    "io"
    //"os"
    "regexp"
    "strconv"
    "strings"
    "time"
)

//ValueType --
type ValueType int

//List --
//Object --
const (
    TABLE ValueType = iota
    LEAF
    KNOT ///
)

//Tree --
type Tree struct {
    Value interface{}
    Vtype ValueType
}

func (t *Tree) object() Object {
    if t.Vtype == KNOT || t.Vtype == LEAF {
        return t.Value.(Object)
    } else if t.Vtype == TABLE {
        tb := t.Value.(Table)
        return tb[len(tb)-1]
    }
    return nil
}

func (t *Tree) table() Table {
    if t.Vtype == TABLE {
        return t.Value.(Table)
    }
    return nil
}

//Object --
type Object map[string]interface{}

//GetString --
func (o Object) GetString(key string) string {
    if cc, ok := o[key]; ok {
        if str, ok := cc.(string); ok {
            return str
        }
    }
    return ""
}

//GetFloat --
func (o Object) GetFloat(key string) (float64, error) {
    str := o.GetString(key)
    if len(str) > 0 {
        return strconv.ParseFloat(str, 32)
    }
    return 0, nil
}

func (o Object) GetDateTime(key string, timeLayout string) (time.Time, error) {
    str := o.GetString(key)
    if len(str) > 0 {
        return time.Parse(timeLayout, str)
    }
    return time.Time{}, errors.New("bad value for field")
}

//GetOject --
func (o Object) GetOject(path ...string) (child Object) {

    child = o
    for _, p := range path {
        if cc, ok := child[p]; ok {
            if nd, ok := cc.(*Tree); ok {
                child = nd.object()
            } else {
                return nil
            }
        } else {
            return nil
        }
    }
    return
}

//GetTable --
func (o Object) GetTable(path ...string) (child Table) {
    if len(path) == 0 {
        return nil
    }

    p := o
    for i, s := range path {
        if cc, ok := p[s]; ok {
            if nd, ok := cc.(*Tree); ok {
                if len(path)-1 == i {
                    return nd.table()
                } else {
                    p = nd.object()
                }
            } else {
                return nil
            }
        } else {
            return nil
        }
    }
    return
}

//GetStringArray --
func (o Object) GetStringArray(key string) []string {

    str := o.GetString(key)
    if len(str) > 0 && str[0] == '[' && str[len(str)-1] == ']' {
        list := strings.Split(str[1:len(str)-2], ",")
        for i, str := range list {
            list[i] = strings.Trim(str, "\" ")
        }
        return list
    }
    return nil
}

//GetFloatArray --
func (o Object) GetFloatArray(key string) []float64 {

    v := o.GetString(key)
    if len(v) > 0 && v[0] == '[' && v[len(v)-1] == ']' {
        strs := strings.Split(v[1:len(v)-2], ",")
        list := []float64{}
        for _, str := range strs {
            str = strings.TrimSpace(str)
            if len(str) > 0 {
                if f, err := strconv.ParseInt(str, 10, 64); err == nil {
                    list = append(list, float64(f))
                } else if f, err := strconv.ParseFloat(str, 32); err == nil {
                    list = append(list, f)
                }
            }
        }
        return list
    }
    return nil
}

//Table --
type Table []Object

//NewTOMLTree --
func NewTOMLTree(rd io.Reader) (root Object) {

    root = make(Object)
    current := root

    var s = bufio.NewScanner(rd)
    s.Split(bufio.ScanLines)
    for s.Scan() {
        line := strings.TrimSpace(s.Text())
        if len(line) > 0 && line[0] != '#' {

            //[Leaf]
            reg := regexp.MustCompile(`^\[\s*(\S+)\s*\]$`)
            group := reg.FindStringSubmatch(line)
            if len(group) == 2 {
                p := root
                gi := strings.Split(group[1], ".")
                for i, g := range gi {
                    pv := p

                    if v, ok := pv[g]; !ok {
                        tmp := &Tree{make(Object), KNOT}
                        if len(gi)-1 == i {
                            tmp.Vtype = LEAF
                        }
                        pv[g] = tmp
                        p = tmp.object()
                    } else {
                        if len(gi)-1 == i {
                            if me, ok := v.(*Tree); ok {

                                if me.Vtype == KNOT {
                                    me.Vtype = LEAF
                                } else if me.Vtype == LEAF {
                                    me.Vtype = TABLE
                                    me.Value = Table{me.Value.(Object), make(Object)}
                                } else if me.Vtype == TABLE {
                                    me.Value = append(me.Value.(Table), make(Object))
                                }
                            } else {
                                panic("节点名存在冲突")
                            }
                        }
                        p = v.(*Tree).object()
                    }

                    current = p
                }
                continue
            }

            //Key = Value
            reg = regexp.MustCompile(`^(.+)=(.*)$`)
            kv := reg.FindStringSubmatch(line)
            if len(kv) == 3 {
                //key
                key := strings.TrimSpace(kv[1])
                value := strings.Trim(strings.TrimSpace(strings.Split(kv[2], "#")[0]), "\"")
                obj := current
                obj[key] = value
            }
        }
    }
    return
}

如何使用?

首先需要说明的是这款实现存在着大量不足

*因为解析方式是按行解析,所以不支持多行数组数据。形如:
hosts = [
“alpha”,
“omega”
]
*由于解析数据值并未按照TOML的语法来实现,因此不支持各种复杂或非主流写法的数据值。形如:
data = [ [“gamma”, “delta”], [1, 2] ]
*如果出现无法解析或解析不正确的数据值,看官可以亲手实现数据值的解析函数。毕竟除了我的电脑不能开源以外,解析实现的代码都在上面了。

 
以下是测试代码,同时也是如何使用的例子代码

//
func main() {
    file, _ := os.Open("d:/toml")
    //接受实现io.Reader接口的参数
    tree := NewTOMLTree(file)
    //最基础获取键值对
    fmt.Println(tree.GetString("title"))
    //单层节点下的获取键值对
    owner := tree.GetOject("owner")
    fmt.Println(owner.GetString("bio"))
    //多层节点下的获取键值对
    one:=tree.GetOject("owner","user","one")
    fmt.Println(one.GetString("name"))
    //键值为对象的获取方式
    one = owner.GetOject("user", "one")
    fmt.Println(one.GetString("duration"))

    //键值为表格数据的获取方式,通过索引号指定各行数据  
    db := tree.GetTable("database")
    //获取数组数据
    fmt.Println(db[2].GetStringArray("ports"))
    dc := db[0].GetOject("alpha")
    fmt.Println(dc.GetString("ip"))
    dc = db[2].GetOject("alpha")
    fmt.Println(dc.GetString("ip"))

}

output:
TOML Example
GitHub Cofounder & CEO\nLikes tater tots and beer.
Thunder Road
4m49s
[6001 6001 6002]
10.0.0.1
11.1.1.1

用到的测试数据如下:

# This is a TOML document. Boom.

title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?

[owner.user.one]
name = "Thunder Road"
duration = "4m49s"

[database.alpha]
  ip = "10.0.0.1"
  dc = "eqdc10"

[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true

[database]
server = "192.168.1.2"
ports = [ 7001, 7001, 7002 ]
connection_max = 5000
enabled = true

[database]
server = "192.168.1.3"
ports = [ 6001, 6001, 6002 ]
connection_max = 5000
enabled = true

[database.alpha]
  ip = "11.1.1.1"
  dc = "eqdc11"