/
ini.go
183 lines (153 loc) · 5.21 KB
/
ini.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// Copyright (c) 2016 OpenM++
// This code is licensed under the MIT license (see LICENSE.txt for details)
package config
import (
"errors"
"strconv"
"strings"
"github.com/openmpp/go/ompp/helper"
)
/*
NewIni read ini-file content into map of (section.key)=>value.
It is very light and able to parse:
dsn = "DSN='server'; UID='user'; PWD='pas#word';" ; comments are # here
Section and key are trimed and cannot contain comments ; or # chars inside.
Key and values trimed and "unquoted".
Key or value escaped with "double" or 'single' quotes can include spaces or ; or # chars
Multi-line values are NOT supported, no line continuation.
Example:
; comments can start from ; or
# from # and empty lines are skipped
[section test] ; section comment
val = no comment
rem = ; comment only and empty value
nul =
dsn = "DSN='server'; UID='user'; PWD='pas#word';" ; quoted value
t w = the "# quick #" brown 'fox ; jumps' over ; escaped: ; and # chars
" key "" 'quoted' here " = some value
qts = " allow ' unbalanced quotes ; with comment
*/
func NewIni(iniPath string, encodingName string) (map[string]string, error) {
if iniPath == "" {
return nil, nil // no ini-file
}
// read ini-file and convert to utf-8
s, err := helper.FileToUtf8(iniPath, encodingName)
if err != nil {
return nil, errors.New("reading ini-file to utf-8 failed: " + err.Error())
}
// parse ini-file into strings map of (section.key)=>value
kvIni, err := loadIni(s)
if err != nil {
return nil, errors.New("reading ini-file failed: " + err.Error())
}
return kvIni, nil
}
// iniKey return ini-file key as concatenation: section.key
func iniKey(section, key string) string { return section + "." + key }
// Parse ini-file content into strings map of (section.key)=>value
func loadIni(iniContent string) (map[string]string, error) {
kvIni := make(map[string]string)
var section, key, val string
for nLine, nStart := 0, 0; nStart < len(iniContent); {
// get current line and move to next
nextPos := strings.IndexAny(iniContent[nStart:], "\r\n")
if nextPos < 0 {
nextPos = len(iniContent)
}
nextPos += 1 + nStart
if nextPos > len(iniContent) {
nextPos = len(iniContent)
}
line := strings.TrimSpace(iniContent[nStart:nextPos])
nStart = nextPos
nLine++
// skip empty lines and ; comments and # Linux comments
// empty line: at least k= or [] section expected, ignore shorter lines
if len(line) < 1 || line[0] == ';' || line[0] == '#' {
continue
}
// error if line too short: at least k= or [] section expected
// error if no [section] found: only comments or empty lines can be before first section
if len(line) < 2 {
return nil, errors.New("line " + strconv.Itoa(nLine) + " too short")
}
if section == "" && line[0] != '[' {
return nil, errors.New("line " + strconv.Itoa(nLine) + ": only comments or empty lines can be before first section")
}
// check if this is [section] with optional ; comments
if line[0] == '[' {
nEnd := strings.IndexRune(line, ']')
nRem := strings.IndexAny(line, ";#")
if nEnd < 2 || nRem > 0 && nRem < nEnd {
return nil, errors.New("line " + strconv.Itoa(nLine) + ": invalid section name")
}
section = strings.TrimSpace(line[1:nEnd])
continue // done with section header
}
if section == "" { // if no [section] found then skip until first section
continue
}
// get key: find first = outside of "quote" or 'single quote'
isQuote := false
var cQuote rune
nEq := 0
for k, c := range line {
if !isQuote && (c == '"' || c == '\'') || isQuote && c == cQuote { // open or close quotes
isQuote = !isQuote
if isQuote {
cQuote = c // opening quote
} else {
cQuote = 0 // quote closed
}
continue
}
if !isQuote && c == '=' { // if outside of quote: check key=
nEq = k
break // found end of key=
}
if !isQuote && (c == ';' || c == '#') { // comment outside of quotes
break
}
}
if nEq < 1 || nEq >= len(line) {
return nil, errors.New("line " + strconv.Itoa(nLine) + ": expected key=...")
}
// split key = and value ; with comment
key = helper.UnQuote(line[:nEq])
val = line[nEq+1:]
// split value and ; optional # comment
isQuote = false
cQuote = 0
nQuote := 0
nRem := 0
for k, c := range val {
if c == ';' || c == '#' { // potential comment started
nRem = k
if !isQuote {
break // comment outside of quotes
}
// else comment inside of quotes or after unbalanced quote started
}
if !isQuote && (c == '"' || c == '\'') || isQuote && c == cQuote { // open or close quotes
isQuote = !isQuote
nQuote = k
if isQuote {
cQuote = c // opening quote
} else {
cQuote = 0 // quote closed
}
continue
}
}
if nRem > nQuote { // if comment after 'value' or after "unbalanced quotes then remove comment
val = val[:nRem]
}
// append result to the map, unquote "value" if quotes balanced
if section != "" && key != "" {
kvIni[iniKey(section, key)] = helper.UnQuote(val)
}
key, val = "", "" // reset state
}
return kvIni, nil
}