结构体
解析器负责启动词法器和从 channel 读取 Token 的组件。接收到 Token 后,解析器需要知道当前 Token 状态,然后将其解析到对应结构中。我们要做的第一件事就是,定义表示 INI 内容的结构体。将主要涉及三个结构体。
第一个表示 Key/Value 的结构体,名称为 IniKeyValue,如下。
model/ini/IniKeyValue.go
package ini
type IniKeyValue struct {
Key string `json:"key"`
Value string `json:"value"`
}
复制代码
第二个表示 Section 的结构体,名称为 IniSection,如下:
/model/ini/IniSection.go
package ini
type IniSection struct {
Name string `json:"name"`
KeyValuePairs []IniKeyValue `json:"keyValuePairs"`
}
复制代码
我们知道,Section 是由 Key/Value 组成的,其中 KeyValuePairs 即是属于这个 Section 的 Key/Value。如果 Key/Value 是不属于任何 Section,将会属于 Name 为空的 Section 中。
最后一个表示整个文件的结构体,名称为 IniFile,如下:
/model/ini/IniFile.go
package ini
type IniFile struct {
FileName string `json:"fileName"`
Sections []IniSection `json:"sections"`
}
复制代码
IniFile 有两个成员字段组成,分别 FileName 文件名称和一系列 Section。
解析器的编写,我们要做的第一件事是,创建一个用于存放解析结构的变量,即一个 IniFile 结构体类型变量。如下:
output := ini.IniFile{
FileName: fileName,
Sections: make([]ini.IniSection, 0),
}
复制代码
现在,我们还需要一些变量追踪 Token、 Token 的值,还有解析器当前的状态。例如,当我们在获取 Key/Value 时,我们需要知道当前处于哪个 Section 中。接下来,就可以启动词法器了。
var token lexertoken.Token
var tokenValue string
/* State variables */
key := ""
log.Println("Starting lexer and parser for file", fileName, "...")
l := lexer.BeginLexing(filename, input)
复制代码
到此,相关变量已经定义完成,并且词法器也成功启动。接下来开始从 channel 接收 Token,如果 Token 类型不是 TOKEN_VALUE,执行 Trim 空格。
for {
token = l.NextToken()
if token.Type != lexertoken.TOKEN_VALUE {
tokenValue = strings.TrimSpace(token.Value)
} else {
tokenValue = token.Value
}
...
复制代码
我们知道,如果词法器遍历到文件结尾,将返回类型为 EOF 的 Token。此时,需要将 Section 和 Key/Value 记录下来,并退出循环。
if isEOF(token) {
output.Sections = append(output.Sections, section)
break
}
复制代码
parser 函数的最后实现具体 Token 的处理,主要有三个 Token 需要关注。
第一个是 Section Token,如果遇到 Section Token,我们首先检查 section 变量中是否已存在 Key/Value,有则将它记录到 output.Sections 中。然后重置 section 变量追踪当前 Section 和接下来 Key/Value。
接着是 Key/Value。如果遇到 TOKEN_KEY,变量 key 用于记录 TOKEN_KEY 的值。遇到 TOKEN_VALUE,我们就可以变量 key 对应的 value,并将其 append 到 section.KeyValuePairs 中。
示例代码如下:
switch token.Type {
case lexertoken.TOKEN_SECTION:
/*
* Reset tracking variables
*/
if len(section.KeyValuePairs) > 0 {
output.Sections = append(output.Sections, section)
}
key = ""
section.Name = tokenValue
section.KeyValuePairs = make([]ini.IniKeyValue, 0)
case lexertoken.TOKEN_KEY:
key = tokenValue
case lexertoken.TOKEN_VALUE:
section.KeyValuePairs = append(section.KeyValuePairs, ini.IniKeyValue{
Key: key,
Value: tokenValue,
})
key = ""
}
复制代码
开发工作已经完成,下面进入测试阶段。Github 上有相应的测试代码,go get 下载好代码,在你的 GOPATH 的 src/github.com/adampresley/sample-ini-parser 目录下的 sampleIniParser.go 即是测试代码。
代码如下:
sampleInput := `
key=abcdefg
[User]
userName=adampresley
keyFile=~/path/to/keyfile
[Servers]
server1=localhost:8080
`
parsedINIFile := parser.Parse("sample.ini", sampleInput)
prettyJSON, err := json.MarshalIndent(parsedINIFile, "", " ")
if err != nil {
log.Println("Error marshalling JSON:", err.Error())
return
}
log.Println(string(prettyJSON))
复制代码
直接通过 go run sampleIniParser.go 执行即可。执行输出如下:
2019/08/01 00:06:33 Starting lexer and parser for file sample.ini ...
2019/08/01 00:06:33 Parser has been shutdown
2019/08/01 00:06:33 {
"fileName": "sample.ini",
"sections": [
{
"name": "",
"keyValuePairs": [
{
"key": "key",
"value": "abcdefg"
}
]
},
{
"name": "User",
"keyValuePairs": [
{
"key": "userName",
"value": "adampresley"
},
{
"key": "keyFile",
"value": "~/path/to/keyfile"
}
]
},
{
"name": "Servers",
"keyValuePairs": [
{
"key": "server1",
"value": "localhost:8080"
}
]
}
]
}
复制代码
这个系列文章是非常有挑战性,但也非常有趣。词法分析与解析是一个非常复杂的话题,有太多内容需要学习。我们可以看到,即使像上面 INI 文件解析这样简单的工作,我们也需要花费一些精力才能完成。