New upstream release.
Debian Janitor
2 years ago
0 | --- | |
1 | name: Bug report | |
2 | about: Create a report to help us improve | |
3 | title: '' | |
4 | labels: '' | |
5 | assignees: '' | |
6 | ||
7 | --- | |
8 | ||
9 | **Describe the bug** | |
10 | A clear and concise description of what the bug is. | |
11 | ||
12 | **To Reproduce** | |
13 | A code snippet to reproduce the problem described above. | |
14 | ||
15 | **Expected behavior** | |
16 | A clear and concise description of what you expected to happen. | |
17 | ||
18 | **Screenshots** | |
19 | If applicable, add screenshots to help explain your problem. | |
20 | ||
21 | **Additional context** | |
22 | Add any other context about the problem here, or any suggestion to fix the problem. |
0 | --- | |
1 | name: Feature request | |
2 | about: Suggest an idea for this project | |
3 | title: '' | |
4 | labels: '' | |
5 | assignees: '' | |
6 | ||
7 | --- | |
8 | ||
9 | **Is your feature request related to a problem? Please describe.** | |
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] | |
11 | ||
12 | **Describe the solution you'd like** | |
13 | A clear and concise description of what you want to happen. | |
14 | ||
15 | **Describe alternatives you've considered** | |
16 | A clear and concise description of any alternative solutions or features you've considered. | |
17 | ||
18 | **Additional context** | |
19 | Add any other context or screenshots about the feature request here. |
0 | ### Please give general description of the problem | |
1 | ||
2 | ### Please provide code snippets to reproduce the problem described above | |
3 | ||
4 | ### Do you have any suggestion to fix the problem?⏎ |
0 | name: Go | |
1 | on: | |
2 | push: | |
3 | branches: [master] | |
4 | pull_request: | |
5 | env: | |
6 | GOPROXY: "https://proxy.golang.org" | |
7 | ||
8 | jobs: | |
9 | lint: | |
10 | name: Lint | |
11 | runs-on: ubuntu-latest | |
12 | steps: | |
13 | - uses: actions/checkout@v2 | |
14 | - name: Init Go modules | |
15 | run: go mod init gopkg.in/ini.v1 | |
16 | - name: Run golangci-lint | |
17 | uses: actions-contrib/golangci-lint@v1 | |
18 | ||
19 | test: | |
20 | name: Test | |
21 | strategy: | |
22 | matrix: | |
23 | go-version: [1.13.x, 1.14.x, 1.15.x] | |
24 | platform: [ubuntu-latest, macos-latest, windows-latest] | |
25 | runs-on: ${{ matrix.platform }} | |
26 | steps: | |
27 | - name: Install Go | |
28 | uses: actions/setup-go@v1 | |
29 | with: | |
30 | go-version: ${{ matrix.go-version }} | |
31 | - name: Checkout code | |
32 | uses: actions/checkout@v2 | |
33 | - name: Run unit tests | |
34 | run: | | |
35 | go mod init gopkg.in/ini.v1 | |
36 | go test -v -race -coverprofile=coverage -covermode=atomic ./... | |
37 | - name: Upload coverage report to Codecov | |
38 | uses: codecov/codecov-action@v1.0.6 | |
39 | with: | |
40 | file: ./coverage | |
41 | flags: unittests | |
42 | - name: Cache downloaded modules | |
43 | uses: actions/cache@v1 | |
44 | with: | |
45 | path: ~/go/pkg/mod | |
46 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} | |
47 | restore-keys: | | |
48 | ${{ runner.os }}-go- |
0 | name: LSIF | |
1 | on: [push] | |
2 | jobs: | |
3 | build: | |
4 | runs-on: ubuntu-latest | |
5 | steps: | |
6 | - uses: actions/checkout@v1 | |
7 | - name: Generate LSIF data | |
8 | uses: sourcegraph/lsif-go-action@master | |
9 | with: | |
10 | verbose: 'true' | |
11 | - name: Upload LSIF data | |
12 | uses: sourcegraph/lsif-upload-action@master | |
13 | continue-on-error: true | |
14 | with: | |
15 | endpoint: https://sourcegraph.com | |
16 | github_token: ${{ secrets.GITHUB_TOKEN }} |
0 | sudo: false | |
1 | language: go | |
2 | go: | |
3 | - 1.5.x | |
4 | - 1.6.x | |
5 | - 1.7.x | |
6 | - 1.8.x | |
7 | - 1.9.x | |
8 | ||
9 | script: | |
10 | - go get golang.org/x/tools/cmd/cover | |
11 | - go get github.com/smartystreets/goconvey | |
12 | - mkdir -p $HOME/gopath/src/gopkg.in | |
13 | - ln -s $HOME/gopath/src/github.com/go-ini/ini $HOME/gopath/src/gopkg.in/ini.v1 | |
14 | - go test -v -cover -race |
5 | 5 | go test -v -cover -race |
6 | 6 | |
7 | 7 | bench: |
8 | go test -v -cover -race -test.bench=. -test.benchmem | |
8 | go test -v -cover -test.bench=. -test.benchmem | |
9 | 9 | |
10 | 10 | vet: |
11 | 11 | go vet |
12 | 12 | |
13 | 13 | coverage: |
14 | go test -coverprofile=c.out && go tool cover -html=c.out && rm c.out⏎ | |
14 | go test -coverprofile=c.out && go tool cover -html=c.out && rm c.out |
0 | INI [![Build Status](https://travis-ci.org/go-ini/ini.svg?branch=master)](https://travis-ci.org/go-ini/ini) [![Sourcegraph](https://sourcegraph.com/github.com/go-ini/ini/-/badge.svg)](https://sourcegraph.com/github.com/go-ini/ini?badge) | |
1 | === | |
0 | # INI | |
1 | ||
2 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/go-ini/ini/Go?logo=github&style=for-the-badge)](https://github.com/go-ini/ini/actions?query=workflow%3AGo) | |
3 | [![codecov](https://img.shields.io/codecov/c/github/go-ini/ini/master?logo=codecov&style=for-the-badge)](https://codecov.io/gh/go-ini/ini) | |
4 | [![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue?style=for-the-badge&logo=go)](https://pkg.go.dev/github.com/go-ini/ini?tab=doc) | |
5 | [![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?style=for-the-badge&logo=sourcegraph)](https://sourcegraph.com/github.com/go-ini/ini) | |
2 | 6 | |
3 | 7 | ![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200) |
4 | 8 | |
5 | 9 | Package ini provides INI file read and write functionality in Go. |
6 | 10 | |
7 | [简体中文](README_ZH.md) | |
11 | ## Features | |
8 | 12 | |
9 | ## Feature | |
10 | ||
11 | - Load multiple data sources(`[]byte`, file and `io.ReadCloser`) with overwrites. | |
13 | - Load from multiple data sources(file, `[]byte`, `io.Reader` and `io.ReadCloser`) with overwrites. | |
12 | 14 | - Read with recursion values. |
13 | 15 | - Read with parent-child sections. |
14 | 16 | - Read with auto-increment key names. |
21 | 23 | |
22 | 24 | ## Installation |
23 | 25 | |
24 | To use a tagged revision: | |
26 | The minimum requirement of Go is **1.6**. | |
25 | 27 | |
26 | go get gopkg.in/ini.v1 | |
27 | ||
28 | To use with latest changes: | |
29 | ||
30 | go get github.com/go-ini/ini | |
28 | ```sh | |
29 | $ go get gopkg.in/ini.v1 | |
30 | ``` | |
31 | 31 | |
32 | 32 | Please add `-u` flag to update in the future. |
33 | 33 | |
34 | ### Testing | |
35 | ||
36 | If you want to test on your machine, please apply `-t` flag: | |
37 | ||
38 | go get -t gopkg.in/ini.v1 | |
39 | ||
40 | Please add `-u` flag to update in the future. | |
41 | ||
42 | ## Getting Started | |
43 | ||
44 | ### Loading from data sources | |
45 | ||
46 | A **Data Source** is either raw data in type `[]byte`, a file name with type `string` or `io.ReadCloser`. You can load **as many data sources as you want**. Passing other types will simply return an error. | |
47 | ||
48 | ```go | |
49 | cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) | |
50 | ``` | |
51 | ||
52 | Or start with an empty object: | |
53 | ||
54 | ```go | |
55 | cfg := ini.Empty() | |
56 | ``` | |
57 | ||
58 | When you cannot decide how many data sources to load at the beginning, you will still be able to **Append()** them later. | |
59 | ||
60 | ```go | |
61 | err := cfg.Append("other file", []byte("other raw data")) | |
62 | ``` | |
63 | ||
64 | If you have a list of files with possibilities that some of them may not available at the time, and you don't know exactly which ones, you can use `LooseLoad` to ignore nonexistent files without returning error. | |
65 | ||
66 | ```go | |
67 | cfg, err := ini.LooseLoad("filename", "filename_404") | |
68 | ``` | |
69 | ||
70 | The cool thing is, whenever the file is available to load while you're calling `Reload` method, it will be counted as usual. | |
71 | ||
72 | #### Ignore cases of key name | |
73 | ||
74 | When you do not care about cases of section and key names, you can use `InsensitiveLoad` to force all names to be lowercased while parsing. | |
75 | ||
76 | ```go | |
77 | cfg, err := ini.InsensitiveLoad("filename") | |
78 | //... | |
79 | ||
80 | // sec1 and sec2 are the exactly same section object | |
81 | sec1, err := cfg.GetSection("Section") | |
82 | sec2, err := cfg.GetSection("SecTIOn") | |
83 | ||
84 | // key1 and key2 are the exactly same key object | |
85 | key1, err := sec1.GetKey("Key") | |
86 | key2, err := sec2.GetKey("KeY") | |
87 | ``` | |
88 | ||
89 | #### MySQL-like boolean key | |
90 | ||
91 | MySQL's configuration allows a key without value as follows: | |
92 | ||
93 | ```ini | |
94 | [mysqld] | |
95 | ... | |
96 | skip-host-cache | |
97 | skip-name-resolve | |
98 | ``` | |
99 | ||
100 | By default, this is considered as missing value. But if you know you're going to deal with those cases, you can assign advanced load options: | |
101 | ||
102 | ```go | |
103 | cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true}, "my.cnf")) | |
104 | ``` | |
105 | ||
106 | The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read. | |
107 | ||
108 | To generate such keys in your program, you could use `NewBooleanKey`: | |
109 | ||
110 | ```go | |
111 | key, err := sec.NewBooleanKey("skip-host-cache") | |
112 | ``` | |
113 | ||
114 | #### Comment | |
115 | ||
116 | Take care that following format will be treated as comment: | |
117 | ||
118 | 1. Line begins with `#` or `;` | |
119 | 2. Words after `#` or `;` | |
120 | 3. Words after section name (i.e words after `[some section name]`) | |
121 | ||
122 | If you want to save a value with `#` or `;`, please quote them with ``` ` ``` or ``` """ ```. | |
123 | ||
124 | Alternatively, you can use following `LoadOptions` to completely ignore inline comments: | |
125 | ||
126 | ```go | |
127 | cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, "app.ini")) | |
128 | ``` | |
129 | ||
130 | ### Working with sections | |
131 | ||
132 | To get a section, you would need to: | |
133 | ||
134 | ```go | |
135 | section, err := cfg.GetSection("section name") | |
136 | ``` | |
137 | ||
138 | For a shortcut for default section, just give an empty string as name: | |
139 | ||
140 | ```go | |
141 | section, err := cfg.GetSection("") | |
142 | ``` | |
143 | ||
144 | When you're pretty sure the section exists, following code could make your life easier: | |
145 | ||
146 | ```go | |
147 | section := cfg.Section("section name") | |
148 | ``` | |
149 | ||
150 | What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you. | |
151 | ||
152 | To create a new section: | |
153 | ||
154 | ```go | |
155 | err := cfg.NewSection("new section") | |
156 | ``` | |
157 | ||
158 | To get a list of sections or section names: | |
159 | ||
160 | ```go | |
161 | sections := cfg.Sections() | |
162 | names := cfg.SectionStrings() | |
163 | ``` | |
164 | ||
165 | ### Working with keys | |
166 | ||
167 | To get a key under a section: | |
168 | ||
169 | ```go | |
170 | key, err := cfg.Section("").GetKey("key name") | |
171 | ``` | |
172 | ||
173 | Same rule applies to key operations: | |
174 | ||
175 | ```go | |
176 | key := cfg.Section("").Key("key name") | |
177 | ``` | |
178 | ||
179 | To check if a key exists: | |
180 | ||
181 | ```go | |
182 | yes := cfg.Section("").HasKey("key name") | |
183 | ``` | |
184 | ||
185 | To create a new key: | |
186 | ||
187 | ```go | |
188 | err := cfg.Section("").NewKey("name", "value") | |
189 | ``` | |
190 | ||
191 | To get a list of keys or key names: | |
192 | ||
193 | ```go | |
194 | keys := cfg.Section("").Keys() | |
195 | names := cfg.Section("").KeyStrings() | |
196 | ``` | |
197 | ||
198 | To get a clone hash of keys and corresponding values: | |
199 | ||
200 | ```go | |
201 | hash := cfg.Section("").KeysHash() | |
202 | ``` | |
203 | ||
204 | ### Working with values | |
205 | ||
206 | To get a string value: | |
207 | ||
208 | ```go | |
209 | val := cfg.Section("").Key("key name").String() | |
210 | ``` | |
211 | ||
212 | To validate key value on the fly: | |
213 | ||
214 | ```go | |
215 | val := cfg.Section("").Key("key name").Validate(func(in string) string { | |
216 | if len(in) == 0 { | |
217 | return "default" | |
218 | } | |
219 | return in | |
220 | }) | |
221 | ``` | |
222 | ||
223 | If you do not want any auto-transformation (such as recursive read) for the values, you can get raw value directly (this way you get much better performance): | |
224 | ||
225 | ```go | |
226 | val := cfg.Section("").Key("key name").Value() | |
227 | ``` | |
228 | ||
229 | To check if raw value exists: | |
230 | ||
231 | ```go | |
232 | yes := cfg.Section("").HasValue("test value") | |
233 | ``` | |
234 | ||
235 | To get value with types: | |
236 | ||
237 | ```go | |
238 | // For boolean values: | |
239 | // true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On | |
240 | // false when value is: 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off | |
241 | v, err = cfg.Section("").Key("BOOL").Bool() | |
242 | v, err = cfg.Section("").Key("FLOAT64").Float64() | |
243 | v, err = cfg.Section("").Key("INT").Int() | |
244 | v, err = cfg.Section("").Key("INT64").Int64() | |
245 | v, err = cfg.Section("").Key("UINT").Uint() | |
246 | v, err = cfg.Section("").Key("UINT64").Uint64() | |
247 | v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) | |
248 | v, err = cfg.Section("").Key("TIME").Time() // RFC3339 | |
249 | ||
250 | v = cfg.Section("").Key("BOOL").MustBool() | |
251 | v = cfg.Section("").Key("FLOAT64").MustFloat64() | |
252 | v = cfg.Section("").Key("INT").MustInt() | |
253 | v = cfg.Section("").Key("INT64").MustInt64() | |
254 | v = cfg.Section("").Key("UINT").MustUint() | |
255 | v = cfg.Section("").Key("UINT64").MustUint64() | |
256 | v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) | |
257 | v = cfg.Section("").Key("TIME").MustTime() // RFC3339 | |
258 | ||
259 | // Methods start with Must also accept one argument for default value | |
260 | // when key not found or fail to parse value to given type. | |
261 | // Except method MustString, which you have to pass a default value. | |
262 | ||
263 | v = cfg.Section("").Key("String").MustString("default") | |
264 | v = cfg.Section("").Key("BOOL").MustBool(true) | |
265 | v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) | |
266 | v = cfg.Section("").Key("INT").MustInt(10) | |
267 | v = cfg.Section("").Key("INT64").MustInt64(99) | |
268 | v = cfg.Section("").Key("UINT").MustUint(3) | |
269 | v = cfg.Section("").Key("UINT64").MustUint64(6) | |
270 | v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) | |
271 | v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 | |
272 | ``` | |
273 | ||
274 | What if my value is three-line long? | |
275 | ||
276 | ```ini | |
277 | [advance] | |
278 | ADDRESS = """404 road, | |
279 | NotFound, State, 5000 | |
280 | Earth""" | |
281 | ``` | |
282 | ||
283 | Not a problem! | |
284 | ||
285 | ```go | |
286 | cfg.Section("advance").Key("ADDRESS").String() | |
287 | ||
288 | /* --- start --- | |
289 | 404 road, | |
290 | NotFound, State, 5000 | |
291 | Earth | |
292 | ------ end --- */ | |
293 | ``` | |
294 | ||
295 | That's cool, how about continuation lines? | |
296 | ||
297 | ```ini | |
298 | [advance] | |
299 | two_lines = how about \ | |
300 | continuation lines? | |
301 | lots_of_lines = 1 \ | |
302 | 2 \ | |
303 | 3 \ | |
304 | 4 | |
305 | ``` | |
306 | ||
307 | Piece of cake! | |
308 | ||
309 | ```go | |
310 | cfg.Section("advance").Key("two_lines").String() // how about continuation lines? | |
311 | cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 | |
312 | ``` | |
313 | ||
314 | Well, I hate continuation lines, how do I disable that? | |
315 | ||
316 | ```go | |
317 | cfg, err := ini.LoadSources(ini.LoadOptions{ | |
318 | IgnoreContinuation: true, | |
319 | }, "filename") | |
320 | ``` | |
321 | ||
322 | Holy crap! | |
323 | ||
324 | Note that single quotes around values will be stripped: | |
325 | ||
326 | ```ini | |
327 | foo = "some value" // foo: some value | |
328 | bar = 'some value' // bar: some value | |
329 | ``` | |
330 | ||
331 | Sometimes you downloaded file from [Crowdin](https://crowdin.com/) has values like the following (value is surrounded by double quotes and quotes in the value are escaped): | |
332 | ||
333 | ```ini | |
334 | create_repo="created repository <a href=\"%s\">%s</a>" | |
335 | ``` | |
336 | ||
337 | How do you transform this to regular format automatically? | |
338 | ||
339 | ```go | |
340 | cfg, err := ini.LoadSources(ini.LoadOptions{UnescapeValueDoubleQuotes: true}, "en-US.ini")) | |
341 | cfg.Section("<name of your section>").Key("create_repo").String() | |
342 | // You got: created repository <a href="%s">%s</a> | |
343 | ``` | |
344 | ||
345 | That's all? Hmm, no. | |
346 | ||
347 | #### Helper methods of working with values | |
348 | ||
349 | To get value with given candidates: | |
350 | ||
351 | ```go | |
352 | v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) | |
353 | v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) | |
354 | v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) | |
355 | v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) | |
356 | v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) | |
357 | v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) | |
358 | v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) | |
359 | v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 | |
360 | ``` | |
361 | ||
362 | Default value will be presented if value of key is not in candidates you given, and default value does not need be one of candidates. | |
363 | ||
364 | To validate value in a given range: | |
365 | ||
366 | ```go | |
367 | vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) | |
368 | vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) | |
369 | vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) | |
370 | vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) | |
371 | vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) | |
372 | vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) | |
373 | vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 | |
374 | ``` | |
375 | ||
376 | ##### Auto-split values into a slice | |
377 | ||
378 | To use zero value of type for invalid inputs: | |
379 | ||
380 | ```go | |
381 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] | |
382 | // Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] | |
383 | vals = cfg.Section("").Key("STRINGS").Strings(",") | |
384 | vals = cfg.Section("").Key("FLOAT64S").Float64s(",") | |
385 | vals = cfg.Section("").Key("INTS").Ints(",") | |
386 | vals = cfg.Section("").Key("INT64S").Int64s(",") | |
387 | vals = cfg.Section("").Key("UINTS").Uints(",") | |
388 | vals = cfg.Section("").Key("UINT64S").Uint64s(",") | |
389 | vals = cfg.Section("").Key("TIMES").Times(",") | |
390 | ``` | |
391 | ||
392 | To exclude invalid values out of result slice: | |
393 | ||
394 | ```go | |
395 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] | |
396 | // Input: how, 2.2, are, you -> [2.2] | |
397 | vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") | |
398 | vals = cfg.Section("").Key("INTS").ValidInts(",") | |
399 | vals = cfg.Section("").Key("INT64S").ValidInt64s(",") | |
400 | vals = cfg.Section("").Key("UINTS").ValidUints(",") | |
401 | vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") | |
402 | vals = cfg.Section("").Key("TIMES").ValidTimes(",") | |
403 | ``` | |
404 | ||
405 | Or to return nothing but error when have invalid inputs: | |
406 | ||
407 | ```go | |
408 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] | |
409 | // Input: how, 2.2, are, you -> error | |
410 | vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") | |
411 | vals = cfg.Section("").Key("INTS").StrictInts(",") | |
412 | vals = cfg.Section("").Key("INT64S").StrictInt64s(",") | |
413 | vals = cfg.Section("").Key("UINTS").StrictUints(",") | |
414 | vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") | |
415 | vals = cfg.Section("").Key("TIMES").StrictTimes(",") | |
416 | ``` | |
417 | ||
418 | ### Save your configuration | |
419 | ||
420 | Finally, it's time to save your configuration to somewhere. | |
421 | ||
422 | A typical way to save configuration is writing it to a file: | |
423 | ||
424 | ```go | |
425 | // ... | |
426 | err = cfg.SaveTo("my.ini") | |
427 | err = cfg.SaveToIndent("my.ini", "\t") | |
428 | ``` | |
429 | ||
430 | Another way to save is writing to a `io.Writer` interface: | |
431 | ||
432 | ```go | |
433 | // ... | |
434 | cfg.WriteTo(writer) | |
435 | cfg.WriteToIndent(writer, "\t") | |
436 | ``` | |
437 | ||
438 | By default, spaces are used to align "=" sign between key and values, to disable that: | |
439 | ||
440 | ```go | |
441 | ini.PrettyFormat = false | |
442 | ``` | |
443 | ||
444 | ## Advanced Usage | |
445 | ||
446 | ### Recursive Values | |
447 | ||
448 | For all value of keys, there is a special syntax `%(<name>)s`, where `<name>` is the key name in same section or default section, and `%(<name>)s` will be replaced by corresponding value(empty string if key not found). You can use this syntax at most 99 level of recursions. | |
449 | ||
450 | ```ini | |
451 | NAME = ini | |
452 | ||
453 | [author] | |
454 | NAME = Unknwon | |
455 | GITHUB = https://github.com/%(NAME)s | |
456 | ||
457 | [package] | |
458 | FULL_NAME = github.com/go-ini/%(NAME)s | |
459 | ``` | |
460 | ||
461 | ```go | |
462 | cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon | |
463 | cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini | |
464 | ``` | |
465 | ||
466 | ### Parent-child Sections | |
467 | ||
468 | You can use `.` in section name to indicate parent-child relationship between two or more sections. If the key not found in the child section, library will try again on its parent section until there is no parent section. | |
469 | ||
470 | ```ini | |
471 | NAME = ini | |
472 | VERSION = v1 | |
473 | IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s | |
474 | ||
475 | [package] | |
476 | CLONE_URL = https://%(IMPORT_PATH)s | |
477 | ||
478 | [package.sub] | |
479 | ``` | |
480 | ||
481 | ```go | |
482 | cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 | |
483 | ``` | |
484 | ||
485 | #### Retrieve parent keys available to a child section | |
486 | ||
487 | ```go | |
488 | cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] | |
489 | ``` | |
490 | ||
491 | ### Unparseable Sections | |
492 | ||
493 | Sometimes, you have sections that do not contain key-value pairs but raw content, to handle such case, you can use `LoadOptions.UnparsableSections`: | |
494 | ||
495 | ```go | |
496 | cfg, err := ini.LoadSources(ini.LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] | |
497 | <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)) | |
498 | ||
499 | body := cfg.Section("COMMENTS").Body() | |
500 | ||
501 | /* --- start --- | |
502 | <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1> | |
503 | ------ end --- */ | |
504 | ``` | |
505 | ||
506 | ### Auto-increment Key Names | |
507 | ||
508 | If key name is `-` in data source, then it would be seen as special syntax for auto-increment key name start from 1, and every section is independent on counter. | |
509 | ||
510 | ```ini | |
511 | [features] | |
512 | -: Support read/write comments of keys and sections | |
513 | -: Support auto-increment of key names | |
514 | -: Support load multiple files to overwrite key values | |
515 | ``` | |
516 | ||
517 | ```go | |
518 | cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"} | |
519 | ``` | |
520 | ||
521 | ### Map To Struct | |
522 | ||
523 | Want more objective way to play with INI? Cool. | |
524 | ||
525 | ```ini | |
526 | Name = Unknwon | |
527 | age = 21 | |
528 | Male = true | |
529 | Born = 1993-01-01T20:17:05Z | |
530 | ||
531 | [Note] | |
532 | Content = Hi is a good man! | |
533 | Cities = HangZhou, Boston | |
534 | ``` | |
535 | ||
536 | ```go | |
537 | type Note struct { | |
538 | Content string | |
539 | Cities []string | |
540 | } | |
541 | ||
542 | type Person struct { | |
543 | Name string | |
544 | Age int `ini:"age"` | |
545 | Male bool | |
546 | Born time.Time | |
547 | Note | |
548 | Created time.Time `ini:"-"` | |
549 | } | |
550 | ||
551 | func main() { | |
552 | cfg, err := ini.Load("path/to/ini") | |
553 | // ... | |
554 | p := new(Person) | |
555 | err = cfg.MapTo(p) | |
556 | // ... | |
557 | ||
558 | // Things can be simpler. | |
559 | err = ini.MapTo(p, "path/to/ini") | |
560 | // ... | |
561 | ||
562 | // Just map a section? Fine. | |
563 | n := new(Note) | |
564 | err = cfg.Section("Note").MapTo(n) | |
565 | // ... | |
566 | } | |
567 | ``` | |
568 | ||
569 | Can I have default value for field? Absolutely. | |
570 | ||
571 | Assign it before you map to struct. It will keep the value as it is if the key is not presented or got wrong type. | |
572 | ||
573 | ```go | |
574 | // ... | |
575 | p := &Person{ | |
576 | Name: "Joe", | |
577 | } | |
578 | // ... | |
579 | ``` | |
580 | ||
581 | It's really cool, but what's the point if you can't give me my file back from struct? | |
582 | ||
583 | ### Reflect From Struct | |
584 | ||
585 | Why not? | |
586 | ||
587 | ```go | |
588 | type Embeded struct { | |
589 | Dates []time.Time `delim:"|" comment:"Time data"` | |
590 | Places []string `ini:"places,omitempty"` | |
591 | None []int `ini:",omitempty"` | |
592 | } | |
593 | ||
594 | type Author struct { | |
595 | Name string `ini:"NAME"` | |
596 | Male bool | |
597 | Age int `comment:"Author's age"` | |
598 | GPA float64 | |
599 | NeverMind string `ini:"-"` | |
600 | *Embeded `comment:"Embeded section"` | |
601 | } | |
602 | ||
603 | func main() { | |
604 | a := &Author{"Unknwon", true, 21, 2.8, "", | |
605 | &Embeded{ | |
606 | []time.Time{time.Now(), time.Now()}, | |
607 | []string{"HangZhou", "Boston"}, | |
608 | []int{}, | |
609 | }} | |
610 | cfg := ini.Empty() | |
611 | err = ini.ReflectFrom(cfg, a) | |
612 | // ... | |
613 | } | |
614 | ``` | |
615 | ||
616 | So, what do I get? | |
617 | ||
618 | ```ini | |
619 | NAME = Unknwon | |
620 | Male = true | |
621 | ; Author's age | |
622 | Age = 21 | |
623 | GPA = 2.8 | |
624 | ||
625 | ; Embeded section | |
626 | [Embeded] | |
627 | ; Time data | |
628 | Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 | |
629 | places = HangZhou,Boston | |
630 | ``` | |
631 | ||
632 | #### Name Mapper | |
633 | ||
634 | To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual section and key name. | |
635 | ||
636 | There are 2 built-in name mappers: | |
637 | ||
638 | - `AllCapsUnderscore`: it converts to format `ALL_CAPS_UNDERSCORE` then match section or key. | |
639 | - `TitleUnderscore`: it converts to format `title_underscore` then match section or key. | |
640 | ||
641 | To use them: | |
642 | ||
643 | ```go | |
644 | type Info struct { | |
645 | PackageName string | |
646 | } | |
647 | ||
648 | func main() { | |
649 | err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) | |
650 | // ... | |
651 | ||
652 | cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) | |
653 | // ... | |
654 | info := new(Info) | |
655 | cfg.NameMapper = ini.AllCapsUnderscore | |
656 | err = cfg.MapTo(info) | |
657 | // ... | |
658 | } | |
659 | ``` | |
660 | ||
661 | Same rules of name mapper apply to `ini.ReflectFromWithMapper` function. | |
662 | ||
663 | #### Value Mapper | |
664 | ||
665 | To expand values (e.g. from environment variables), you can use the `ValueMapper` to transform values: | |
666 | ||
667 | ```go | |
668 | type Env struct { | |
669 | Foo string `ini:"foo"` | |
670 | } | |
671 | ||
672 | func main() { | |
673 | cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") | |
674 | cfg.ValueMapper = os.ExpandEnv | |
675 | // ... | |
676 | env := &Env{} | |
677 | err = cfg.Section("env").MapTo(env) | |
678 | } | |
679 | ``` | |
680 | ||
681 | This would set the value of `env.Foo` to the value of the environment variable `MY_VAR`. | |
682 | ||
683 | #### Other Notes On Map/Reflect | |
684 | ||
685 | Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature: | |
686 | ||
687 | ```go | |
688 | type Child struct { | |
689 | Age string | |
690 | } | |
691 | ||
692 | type Parent struct { | |
693 | Name string | |
694 | Child | |
695 | } | |
696 | ||
697 | type Config struct { | |
698 | City string | |
699 | Parent | |
700 | } | |
701 | ``` | |
702 | ||
703 | Example configuration: | |
704 | ||
705 | ```ini | |
706 | City = Boston | |
707 | ||
708 | [Parent] | |
709 | Name = Unknwon | |
710 | ||
711 | [Child] | |
712 | Age = 21 | |
713 | ``` | |
714 | ||
715 | What if, yes, I'm paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome. | |
716 | ||
717 | ```go | |
718 | type Child struct { | |
719 | Age string | |
720 | } | |
721 | ||
722 | type Parent struct { | |
723 | Name string | |
724 | Child `ini:"Parent"` | |
725 | } | |
726 | ||
727 | type Config struct { | |
728 | City string | |
729 | Parent | |
730 | } | |
731 | ``` | |
732 | ||
733 | Example configuration: | |
734 | ||
735 | ```ini | |
736 | City = Boston | |
737 | ||
738 | [Parent] | |
739 | Name = Unknwon | |
740 | Age = 21 | |
741 | ``` | |
742 | ||
743 | 34 | ## Getting Help |
744 | 35 | |
36 | - [Getting Started](https://ini.unknwon.io/docs/intro/getting_started) | |
745 | 37 | - [API Documentation](https://gowalker.org/gopkg.in/ini.v1) |
746 | - [File An Issue](https://github.com/go-ini/ini/issues/new) | |
747 | ||
748 | ## FAQs | |
749 | ||
750 | ### What does `BlockMode` field do? | |
751 | ||
752 | By default, library lets you read and write values so we need a locker to make sure your data is safe. But in cases that you are very sure about only reading data through the library, you can set `cfg.BlockMode = false` to speed up read operations about **50-70%** faster. | |
753 | ||
754 | ### Why another INI library? | |
755 | ||
756 | Many people are using my another INI library [goconfig](https://github.com/Unknwon/goconfig), so the reason for this one is I would like to make more Go style code. Also when you set `cfg.BlockMode = false`, this one is about **10-30%** faster. | |
757 | ||
758 | To make those changes I have to confirm API broken, so it's safer to keep it in another place and start using `gopkg.in` to version my package at this time.(PS: shorter import path) | |
38 | - 中国大陆镜像:https://ini.unknwon.cn | |
759 | 39 | |
760 | 40 | ## License |
761 | 41 |
0 | 本包提供了 Go 语言中读写 INI 文件的功能。 | |
1 | ||
2 | ## 功能特性 | |
3 | ||
4 | - 支持覆盖加载多个数据源(`[]byte`、文件和 `io.ReadCloser`) | |
5 | - 支持递归读取键值 | |
6 | - 支持读取父子分区 | |
7 | - 支持读取自增键名 | |
8 | - 支持读取多行的键值 | |
9 | - 支持大量辅助方法 | |
10 | - 支持在读取时直接转换为 Go 语言类型 | |
11 | - 支持读取和 **写入** 分区和键的注释 | |
12 | - 轻松操作分区、键值和注释 | |
13 | - 在保存文件时分区和键值会保持原有的顺序 | |
14 | ||
15 | ## 下载安装 | |
16 | ||
17 | 使用一个特定版本: | |
18 | ||
19 | go get gopkg.in/ini.v1 | |
20 | ||
21 | 使用最新版: | |
22 | ||
23 | go get github.com/go-ini/ini | |
24 | ||
25 | 如需更新请添加 `-u` 选项。 | |
26 | ||
27 | ### 测试安装 | |
28 | ||
29 | 如果您想要在自己的机器上运行测试,请使用 `-t` 标记: | |
30 | ||
31 | go get -t gopkg.in/ini.v1 | |
32 | ||
33 | 如需更新请添加 `-u` 选项。 | |
34 | ||
35 | ## 开始使用 | |
36 | ||
37 | ### 从数据源加载 | |
38 | ||
39 | 一个 **数据源** 可以是 `[]byte` 类型的原始数据,`string` 类型的文件路径或 `io.ReadCloser`。您可以加载 **任意多个** 数据源。如果您传递其它类型的数据源,则会直接返回错误。 | |
40 | ||
41 | ```go | |
42 | cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) | |
43 | ``` | |
44 | ||
45 | 或者从一个空白的文件开始: | |
46 | ||
47 | ```go | |
48 | cfg := ini.Empty() | |
49 | ``` | |
50 | ||
51 | 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。 | |
52 | ||
53 | ```go | |
54 | err := cfg.Append("other file", []byte("other raw data")) | |
55 | ``` | |
56 | ||
57 | 当您想要加载一系列文件,但是不能够确定其中哪些文件是不存在的,可以通过调用函数 `LooseLoad` 来忽略它们(`Load` 会因为文件不存在而返回错误): | |
58 | ||
59 | ```go | |
60 | cfg, err := ini.LooseLoad("filename", "filename_404") | |
61 | ``` | |
62 | ||
63 | 更牛逼的是,当那些之前不存在的文件在重新调用 `Reload` 方法的时候突然出现了,那么它们会被正常加载。 | |
64 | ||
65 | #### 忽略键名的大小写 | |
66 | ||
67 | 有时候分区和键的名称大小写混合非常烦人,这个时候就可以通过 `InsensitiveLoad` 将所有分区和键名在读取里强制转换为小写: | |
68 | ||
69 | ```go | |
70 | cfg, err := ini.InsensitiveLoad("filename") | |
71 | //... | |
72 | ||
73 | // sec1 和 sec2 指向同一个分区对象 | |
74 | sec1, err := cfg.GetSection("Section") | |
75 | sec2, err := cfg.GetSection("SecTIOn") | |
76 | ||
77 | // key1 和 key2 指向同一个键对象 | |
78 | key1, err := sec1.GetKey("Key") | |
79 | key2, err := sec2.GetKey("KeY") | |
80 | ``` | |
81 | ||
82 | #### 类似 MySQL 配置中的布尔值键 | |
83 | ||
84 | MySQL 的配置文件中会出现没有具体值的布尔类型的键: | |
85 | ||
86 | ```ini | |
87 | [mysqld] | |
88 | ... | |
89 | skip-host-cache | |
90 | skip-name-resolve | |
91 | ``` | |
92 | ||
93 | 默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理: | |
94 | ||
95 | ```go | |
96 | cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true}, "my.cnf")) | |
97 | ``` | |
98 | ||
99 | 这些键的值永远为 `true`,且在保存到文件时也只会输出键名。 | |
100 | ||
101 | 如果您想要通过程序来生成此类键,则可以使用 `NewBooleanKey`: | |
102 | ||
103 | ```go | |
104 | key, err := sec.NewBooleanKey("skip-host-cache") | |
105 | ``` | |
106 | ||
107 | #### 关于注释 | |
108 | ||
109 | 下述几种情况的内容将被视为注释: | |
110 | ||
111 | 1. 所有以 `#` 或 `;` 开头的行 | |
112 | 2. 所有在 `#` 或 `;` 之后的内容 | |
113 | 3. 分区标签后的文字 (即 `[分区名]` 之后的内容) | |
114 | ||
115 | 如果你希望使用包含 `#` 或 `;` 的值,请使用 ``` ` ``` 或 ``` """ ``` 进行包覆。 | |
116 | ||
117 | 除此之外,您还可以通过 `LoadOptions` 完全忽略行内注释: | |
118 | ||
119 | ```go | |
120 | cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, "app.ini")) | |
121 | ``` | |
122 | ||
123 | ### 操作分区(Section) | |
124 | ||
125 | 获取指定分区: | |
126 | ||
127 | ```go | |
128 | section, err := cfg.GetSection("section name") | |
129 | ``` | |
130 | ||
131 | 如果您想要获取默认分区,则可以用空字符串代替分区名: | |
132 | ||
133 | ```go | |
134 | section, err := cfg.GetSection("") | |
135 | ``` | |
136 | ||
137 | 当您非常确定某个分区是存在的,可以使用以下简便方法: | |
138 | ||
139 | ```go | |
140 | section := cfg.Section("section name") | |
141 | ``` | |
142 | ||
143 | 如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。 | |
144 | ||
145 | 创建一个分区: | |
146 | ||
147 | ```go | |
148 | err := cfg.NewSection("new section") | |
149 | ``` | |
150 | ||
151 | 获取所有分区对象或名称: | |
152 | ||
153 | ```go | |
154 | sections := cfg.Sections() | |
155 | names := cfg.SectionStrings() | |
156 | ``` | |
157 | ||
158 | ### 操作键(Key) | |
159 | ||
160 | 获取某个分区下的键: | |
161 | ||
162 | ```go | |
163 | key, err := cfg.Section("").GetKey("key name") | |
164 | ``` | |
165 | ||
166 | 和分区一样,您也可以直接获取键而忽略错误处理: | |
167 | ||
168 | ```go | |
169 | key := cfg.Section("").Key("key name") | |
170 | ``` | |
171 | ||
172 | 判断某个键是否存在: | |
173 | ||
174 | ```go | |
175 | yes := cfg.Section("").HasKey("key name") | |
176 | ``` | |
177 | ||
178 | 创建一个新的键: | |
179 | ||
180 | ```go | |
181 | err := cfg.Section("").NewKey("name", "value") | |
182 | ``` | |
183 | ||
184 | 获取分区下的所有键或键名: | |
185 | ||
186 | ```go | |
187 | keys := cfg.Section("").Keys() | |
188 | names := cfg.Section("").KeyStrings() | |
189 | ``` | |
190 | ||
191 | 获取分区下的所有键值对的克隆: | |
192 | ||
193 | ```go | |
194 | hash := cfg.Section("").KeysHash() | |
195 | ``` | |
196 | ||
197 | ### 操作键值(Value) | |
198 | ||
199 | 获取一个类型为字符串(string)的值: | |
200 | ||
201 | ```go | |
202 | val := cfg.Section("").Key("key name").String() | |
203 | ``` | |
204 | ||
205 | 获取值的同时通过自定义函数进行处理验证: | |
206 | ||
207 | ```go | |
208 | val := cfg.Section("").Key("key name").Validate(func(in string) string { | |
209 | if len(in) == 0 { | |
210 | return "default" | |
211 | } | |
212 | return in | |
213 | }) | |
214 | ``` | |
215 | ||
216 | 如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳): | |
217 | ||
218 | ```go | |
219 | val := cfg.Section("").Key("key name").Value() | |
220 | ``` | |
221 | ||
222 | 判断某个原值是否存在: | |
223 | ||
224 | ```go | |
225 | yes := cfg.Section("").HasValue("test value") | |
226 | ``` | |
227 | ||
228 | 获取其它类型的值: | |
229 | ||
230 | ```go | |
231 | // 布尔值的规则: | |
232 | // true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On | |
233 | // false 当值为:0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off | |
234 | v, err = cfg.Section("").Key("BOOL").Bool() | |
235 | v, err = cfg.Section("").Key("FLOAT64").Float64() | |
236 | v, err = cfg.Section("").Key("INT").Int() | |
237 | v, err = cfg.Section("").Key("INT64").Int64() | |
238 | v, err = cfg.Section("").Key("UINT").Uint() | |
239 | v, err = cfg.Section("").Key("UINT64").Uint64() | |
240 | v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) | |
241 | v, err = cfg.Section("").Key("TIME").Time() // RFC3339 | |
242 | ||
243 | v = cfg.Section("").Key("BOOL").MustBool() | |
244 | v = cfg.Section("").Key("FLOAT64").MustFloat64() | |
245 | v = cfg.Section("").Key("INT").MustInt() | |
246 | v = cfg.Section("").Key("INT64").MustInt64() | |
247 | v = cfg.Section("").Key("UINT").MustUint() | |
248 | v = cfg.Section("").Key("UINT64").MustUint64() | |
249 | v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) | |
250 | v = cfg.Section("").Key("TIME").MustTime() // RFC3339 | |
251 | ||
252 | // 由 Must 开头的方法名允许接收一个相同类型的参数来作为默认值, | |
253 | // 当键不存在或者转换失败时,则会直接返回该默认值。 | |
254 | // 但是,MustString 方法必须传递一个默认值。 | |
255 | ||
256 | v = cfg.Seciont("").Key("String").MustString("default") | |
257 | v = cfg.Section("").Key("BOOL").MustBool(true) | |
258 | v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) | |
259 | v = cfg.Section("").Key("INT").MustInt(10) | |
260 | v = cfg.Section("").Key("INT64").MustInt64(99) | |
261 | v = cfg.Section("").Key("UINT").MustUint(3) | |
262 | v = cfg.Section("").Key("UINT64").MustUint64(6) | |
263 | v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) | |
264 | v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 | |
265 | ``` | |
266 | ||
267 | 如果我的值有好多行怎么办? | |
268 | ||
269 | ```ini | |
270 | [advance] | |
271 | ADDRESS = """404 road, | |
272 | NotFound, State, 5000 | |
273 | Earth""" | |
274 | ``` | |
275 | ||
276 | 嗯哼?小 case! | |
277 | ||
278 | ```go | |
279 | cfg.Section("advance").Key("ADDRESS").String() | |
280 | ||
281 | /* --- start --- | |
282 | 404 road, | |
283 | NotFound, State, 5000 | |
284 | Earth | |
285 | ------ end --- */ | |
286 | ``` | |
287 | ||
288 | 赞爆了!那要是我属于一行的内容写不下想要写到第二行怎么办? | |
289 | ||
290 | ```ini | |
291 | [advance] | |
292 | two_lines = how about \ | |
293 | continuation lines? | |
294 | lots_of_lines = 1 \ | |
295 | 2 \ | |
296 | 3 \ | |
297 | 4 | |
298 | ``` | |
299 | ||
300 | 简直是小菜一碟! | |
301 | ||
302 | ```go | |
303 | cfg.Section("advance").Key("two_lines").String() // how about continuation lines? | |
304 | cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 | |
305 | ``` | |
306 | ||
307 | 可是我有时候觉得两行连在一起特别没劲,怎么才能不自动连接两行呢? | |
308 | ||
309 | ```go | |
310 | cfg, err := ini.LoadSources(ini.LoadOptions{ | |
311 | IgnoreContinuation: true, | |
312 | }, "filename") | |
313 | ``` | |
314 | ||
315 | 哇靠给力啊! | |
316 | ||
317 | 需要注意的是,值两侧的单引号会被自动剔除: | |
318 | ||
319 | ```ini | |
320 | foo = "some value" // foo: some value | |
321 | bar = 'some value' // bar: some value | |
322 | ``` | |
323 | ||
324 | 有时您会获得像从 [Crowdin](https://crowdin.com/) 网站下载的文件那样具有特殊格式的值(值使用双引号括起来,内部的双引号被转义): | |
325 | ||
326 | ```ini | |
327 | create_repo="创建了仓库 <a href=\"%s\">%s</a>" | |
328 | ``` | |
329 | ||
330 | 那么,怎么自动地将这类值进行处理呢? | |
331 | ||
332 | ```go | |
333 | cfg, err := ini.LoadSources(ini.LoadOptions{UnescapeValueDoubleQuotes: true}, "en-US.ini")) | |
334 | cfg.Section("<name of your section>").Key("create_repo").String() | |
335 | // You got: 创建了仓库 <a href="%s">%s</a> | |
336 | ``` | |
337 | ||
338 | 这就是全部了?哈哈,当然不是。 | |
339 | ||
340 | #### 操作键值的辅助方法 | |
341 | ||
342 | 获取键值时设定候选值: | |
343 | ||
344 | ```go | |
345 | v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) | |
346 | v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) | |
347 | v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) | |
348 | v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) | |
349 | v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) | |
350 | v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) | |
351 | v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) | |
352 | v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 | |
353 | ``` | |
354 | ||
355 | 如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。 | |
356 | ||
357 | 验证获取的值是否在指定范围内: | |
358 | ||
359 | ```go | |
360 | vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) | |
361 | vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) | |
362 | vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) | |
363 | vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) | |
364 | vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) | |
365 | vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) | |
366 | vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 | |
367 | ``` | |
368 | ||
369 | ##### 自动分割键值到切片(slice) | |
370 | ||
371 | 当存在无效输入时,使用零值代替: | |
372 | ||
373 | ```go | |
374 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] | |
375 | // Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] | |
376 | vals = cfg.Section("").Key("STRINGS").Strings(",") | |
377 | vals = cfg.Section("").Key("FLOAT64S").Float64s(",") | |
378 | vals = cfg.Section("").Key("INTS").Ints(",") | |
379 | vals = cfg.Section("").Key("INT64S").Int64s(",") | |
380 | vals = cfg.Section("").Key("UINTS").Uints(",") | |
381 | vals = cfg.Section("").Key("UINT64S").Uint64s(",") | |
382 | vals = cfg.Section("").Key("TIMES").Times(",") | |
383 | ``` | |
384 | ||
385 | 从结果切片中剔除无效输入: | |
386 | ||
387 | ```go | |
388 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] | |
389 | // Input: how, 2.2, are, you -> [2.2] | |
390 | vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") | |
391 | vals = cfg.Section("").Key("INTS").ValidInts(",") | |
392 | vals = cfg.Section("").Key("INT64S").ValidInt64s(",") | |
393 | vals = cfg.Section("").Key("UINTS").ValidUints(",") | |
394 | vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") | |
395 | vals = cfg.Section("").Key("TIMES").ValidTimes(",") | |
396 | ``` | |
397 | ||
398 | 当存在无效输入时,直接返回错误: | |
399 | ||
400 | ```go | |
401 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] | |
402 | // Input: how, 2.2, are, you -> error | |
403 | vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") | |
404 | vals = cfg.Section("").Key("INTS").StrictInts(",") | |
405 | vals = cfg.Section("").Key("INT64S").StrictInt64s(",") | |
406 | vals = cfg.Section("").Key("UINTS").StrictUints(",") | |
407 | vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") | |
408 | vals = cfg.Section("").Key("TIMES").StrictTimes(",") | |
409 | ``` | |
410 | ||
411 | ### 保存配置 | |
412 | ||
413 | 终于到了这个时刻,是时候保存一下配置了。 | |
414 | ||
415 | 比较原始的做法是输出配置到某个文件: | |
416 | ||
417 | ```go | |
418 | // ... | |
419 | err = cfg.SaveTo("my.ini") | |
420 | err = cfg.SaveToIndent("my.ini", "\t") | |
421 | ``` | |
422 | ||
423 | 另一个比较高级的做法是写入到任何实现 `io.Writer` 接口的对象中: | |
424 | ||
425 | ```go | |
426 | // ... | |
427 | cfg.WriteTo(writer) | |
428 | cfg.WriteToIndent(writer, "\t") | |
429 | ``` | |
430 | ||
431 | 默认情况下,空格将被用于对齐键值之间的等号以美化输出结果,以下代码可以禁用该功能: | |
432 | ||
433 | ```go | |
434 | ini.PrettyFormat = false | |
435 | ``` | |
436 | ||
437 | ## 高级用法 | |
438 | ||
439 | ### 递归读取键值 | |
440 | ||
441 | 在获取所有键值的过程中,特殊语法 `%(<name>)s` 会被应用,其中 `<name>` 可以是相同分区或者默认分区下的键名。字符串 `%(<name>)s` 会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代。您可以最多使用 99 层的递归嵌套。 | |
442 | ||
443 | ```ini | |
444 | NAME = ini | |
445 | ||
446 | [author] | |
447 | NAME = Unknwon | |
448 | GITHUB = https://github.com/%(NAME)s | |
449 | ||
450 | [package] | |
451 | FULL_NAME = github.com/go-ini/%(NAME)s | |
452 | ``` | |
453 | ||
454 | ```go | |
455 | cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon | |
456 | cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini | |
457 | ``` | |
458 | ||
459 | ### 读取父子分区 | |
460 | ||
461 | 您可以在分区名称中使用 `.` 来表示两个或多个分区之间的父子关系。如果某个键在子分区中不存在,则会去它的父分区中再次寻找,直到没有父分区为止。 | |
462 | ||
463 | ```ini | |
464 | NAME = ini | |
465 | VERSION = v1 | |
466 | IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s | |
467 | ||
468 | [package] | |
469 | CLONE_URL = https://%(IMPORT_PATH)s | |
470 | ||
471 | [package.sub] | |
472 | ``` | |
473 | ||
474 | ```go | |
475 | cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 | |
476 | ``` | |
477 | ||
478 | #### 获取上级父分区下的所有键名 | |
479 | ||
480 | ```go | |
481 | cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] | |
482 | ``` | |
483 | ||
484 | ### 无法解析的分区 | |
485 | ||
486 | 如果遇到一些比较特殊的分区,它们不包含常见的键值对,而是没有固定格式的纯文本,则可以使用 `LoadOptions.UnparsableSections` 进行处理: | |
487 | ||
488 | ```go | |
489 | cfg, err := LoadSources(ini.LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] | |
490 | <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)) | |
491 | ||
492 | body := cfg.Section("COMMENTS").Body() | |
493 | ||
494 | /* --- start --- | |
495 | <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1> | |
496 | ------ end --- */ | |
497 | ``` | |
498 | ||
499 | ### 读取自增键名 | |
500 | ||
501 | 如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。 | |
502 | ||
503 | ```ini | |
504 | [features] | |
505 | -: Support read/write comments of keys and sections | |
506 | -: Support auto-increment of key names | |
507 | -: Support load multiple files to overwrite key values | |
508 | ``` | |
509 | ||
510 | ```go | |
511 | cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"} | |
512 | ``` | |
513 | ||
514 | ### 映射到结构 | |
515 | ||
516 | 想要使用更加面向对象的方式玩转 INI 吗?好主意。 | |
517 | ||
518 | ```ini | |
519 | Name = Unknwon | |
520 | age = 21 | |
521 | Male = true | |
522 | Born = 1993-01-01T20:17:05Z | |
523 | ||
524 | [Note] | |
525 | Content = Hi is a good man! | |
526 | Cities = HangZhou, Boston | |
527 | ``` | |
528 | ||
529 | ```go | |
530 | type Note struct { | |
531 | Content string | |
532 | Cities []string | |
533 | } | |
534 | ||
535 | type Person struct { | |
536 | Name string | |
537 | Age int `ini:"age"` | |
538 | Male bool | |
539 | Born time.Time | |
540 | Note | |
541 | Created time.Time `ini:"-"` | |
542 | } | |
543 | ||
544 | func main() { | |
545 | cfg, err := ini.Load("path/to/ini") | |
546 | // ... | |
547 | p := new(Person) | |
548 | err = cfg.MapTo(p) | |
549 | // ... | |
550 | ||
551 | // 一切竟可以如此的简单。 | |
552 | err = ini.MapTo(p, "path/to/ini") | |
553 | // ... | |
554 | ||
555 | // 嗯哼?只需要映射一个分区吗? | |
556 | n := new(Note) | |
557 | err = cfg.Section("Note").MapTo(n) | |
558 | // ... | |
559 | } | |
560 | ``` | |
561 | ||
562 | 结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。 | |
563 | ||
564 | ```go | |
565 | // ... | |
566 | p := &Person{ | |
567 | Name: "Joe", | |
568 | } | |
569 | // ... | |
570 | ``` | |
571 | ||
572 | 这样玩 INI 真的好酷啊!然而,如果不能还给我原来的配置文件,有什么卵用? | |
573 | ||
574 | ### 从结构反射 | |
575 | ||
576 | 可是,我有说不能吗? | |
577 | ||
578 | ```go | |
579 | type Embeded struct { | |
580 | Dates []time.Time `delim:"|" comment:"Time data"` | |
581 | Places []string `ini:"places,omitempty"` | |
582 | None []int `ini:",omitempty"` | |
583 | } | |
584 | ||
585 | type Author struct { | |
586 | Name string `ini:"NAME"` | |
587 | Male bool | |
588 | Age int `comment:"Author's age"` | |
589 | GPA float64 | |
590 | NeverMind string `ini:"-"` | |
591 | *Embeded `comment:"Embeded section"` | |
592 | } | |
593 | ||
594 | func main() { | |
595 | a := &Author{"Unknwon", true, 21, 2.8, "", | |
596 | &Embeded{ | |
597 | []time.Time{time.Now(), time.Now()}, | |
598 | []string{"HangZhou", "Boston"}, | |
599 | []int{}, | |
600 | }} | |
601 | cfg := ini.Empty() | |
602 | err = ini.ReflectFrom(cfg, a) | |
603 | // ... | |
604 | } | |
605 | ``` | |
606 | ||
607 | 瞧瞧,奇迹发生了。 | |
608 | ||
609 | ```ini | |
610 | NAME = Unknwon | |
611 | Male = true | |
612 | ; Author's age | |
613 | Age = 21 | |
614 | GPA = 2.8 | |
615 | ||
616 | ; Embeded section | |
617 | [Embeded] | |
618 | ; Time data | |
619 | Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 | |
620 | places = HangZhou,Boston | |
621 | ``` | |
622 | ||
623 | #### 名称映射器(Name Mapper) | |
624 | ||
625 | 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。 | |
626 | ||
627 | 目前有 2 款内置的映射器: | |
628 | ||
629 | - `AllCapsUnderscore`:该映射器将字段名转换至格式 `ALL_CAPS_UNDERSCORE` 后再去匹配分区名和键名。 | |
630 | - `TitleUnderscore`:该映射器将字段名转换至格式 `title_underscore` 后再去匹配分区名和键名。 | |
631 | ||
632 | 使用方法: | |
633 | ||
634 | ```go | |
635 | type Info struct{ | |
636 | PackageName string | |
637 | } | |
638 | ||
639 | func main() { | |
640 | err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) | |
641 | // ... | |
642 | ||
643 | cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) | |
644 | // ... | |
645 | info := new(Info) | |
646 | cfg.NameMapper = ini.AllCapsUnderscore | |
647 | err = cfg.MapTo(info) | |
648 | // ... | |
649 | } | |
650 | ``` | |
651 | ||
652 | 使用函数 `ini.ReflectFromWithMapper` 时也可应用相同的规则。 | |
653 | ||
654 | #### 值映射器(Value Mapper) | |
655 | ||
656 | 值映射器允许使用一个自定义函数自动展开值的具体内容,例如:运行时获取环境变量: | |
657 | ||
658 | ```go | |
659 | type Env struct { | |
660 | Foo string `ini:"foo"` | |
661 | } | |
662 | ||
663 | func main() { | |
664 | cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") | |
665 | cfg.ValueMapper = os.ExpandEnv | |
666 | // ... | |
667 | env := &Env{} | |
668 | err = cfg.Section("env").MapTo(env) | |
669 | } | |
670 | ``` | |
671 | ||
672 | 本例中,`env.Foo` 将会是运行时所获取到环境变量 `MY_VAR` 的值。 | |
673 | ||
674 | #### 映射/反射的其它说明 | |
675 | ||
676 | 任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联: | |
677 | ||
678 | ```go | |
679 | type Child struct { | |
680 | Age string | |
681 | } | |
682 | ||
683 | type Parent struct { | |
684 | Name string | |
685 | Child | |
686 | } | |
687 | ||
688 | type Config struct { | |
689 | City string | |
690 | Parent | |
691 | } | |
692 | ``` | |
693 | ||
694 | 示例配置文件: | |
695 | ||
696 | ```ini | |
697 | City = Boston | |
698 | ||
699 | [Parent] | |
700 | Name = Unknwon | |
701 | ||
702 | [Child] | |
703 | Age = 21 | |
704 | ``` | |
705 | ||
706 | 很好,但是,我就是要嵌入结构也在同一个分区。好吧,你爹是李刚! | |
707 | ||
708 | ```go | |
709 | type Child struct { | |
710 | Age string | |
711 | } | |
712 | ||
713 | type Parent struct { | |
714 | Name string | |
715 | Child `ini:"Parent"` | |
716 | } | |
717 | ||
718 | type Config struct { | |
719 | City string | |
720 | Parent | |
721 | } | |
722 | ``` | |
723 | ||
724 | 示例配置文件: | |
725 | ||
726 | ```ini | |
727 | City = Boston | |
728 | ||
729 | [Parent] | |
730 | Name = Unknwon | |
731 | Age = 21 | |
732 | ``` | |
733 | ||
734 | ## 获取帮助 | |
735 | ||
736 | - [API 文档](https://gowalker.org/gopkg.in/ini.v1) | |
737 | - [创建工单](https://github.com/go-ini/ini/issues/new) | |
738 | ||
739 | ## 常见问题 | |
740 | ||
741 | ### 字段 `BlockMode` 是什么? | |
742 | ||
743 | 默认情况下,本库会在您进行读写操作时采用锁机制来确保数据时间。但在某些情况下,您非常确定只进行读操作。此时,您可以通过设置 `cfg.BlockMode = false` 来将读操作提升大约 **50-70%** 的性能。 | |
744 | ||
745 | ### 为什么要写另一个 INI 解析库? | |
746 | ||
747 | 许多人都在使用我的 [goconfig](https://github.com/Unknwon/goconfig) 来完成对 INI 文件的操作,但我希望使用更加 Go 风格的代码。并且当您设置 `cfg.BlockMode = false` 时,会有大约 **10-30%** 的性能提升。 | |
748 | ||
749 | 为了做出这些改变,我必须对 API 进行破坏,所以新开一个仓库是最安全的做法。除此之外,本库直接使用 `gopkg.in` 来进行版本化发布。(其实真相是导入路径更短了) |
20 | 20 | ) |
21 | 21 | |
22 | 22 | func newTestFile(block bool) *ini.File { |
23 | c, _ := ini.Load([]byte(_CONF_DATA)) | |
23 | c, _ := ini.Load([]byte(confData)) | |
24 | 24 | c.BlockMode = block |
25 | 25 | return c |
26 | 26 | } |
0 | coverage: | |
1 | range: "60...95" | |
2 | status: | |
3 | project: | |
4 | default: | |
5 | threshold: 1% | |
6 | ||
7 | comment: | |
8 | layout: 'diff, files' |
0 | // Copyright 2019 Unknwon | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"): you may | |
3 | // not use this file except in compliance with the License. You may obtain | |
4 | // a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
10 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
11 | // License for the specific language governing permissions and limitations | |
12 | // under the License. | |
13 | ||
14 | package ini | |
15 | ||
16 | import ( | |
17 | "bytes" | |
18 | "fmt" | |
19 | "io" | |
20 | "io/ioutil" | |
21 | "os" | |
22 | ) | |
23 | ||
24 | var ( | |
25 | _ dataSource = (*sourceFile)(nil) | |
26 | _ dataSource = (*sourceData)(nil) | |
27 | _ dataSource = (*sourceReadCloser)(nil) | |
28 | ) | |
29 | ||
30 | // dataSource is an interface that returns object which can be read and closed. | |
31 | type dataSource interface { | |
32 | ReadCloser() (io.ReadCloser, error) | |
33 | } | |
34 | ||
35 | // sourceFile represents an object that contains content on the local file system. | |
36 | type sourceFile struct { | |
37 | name string | |
38 | } | |
39 | ||
40 | func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { | |
41 | return os.Open(s.name) | |
42 | } | |
43 | ||
44 | // sourceData represents an object that contains content in memory. | |
45 | type sourceData struct { | |
46 | data []byte | |
47 | } | |
48 | ||
49 | func (s *sourceData) ReadCloser() (io.ReadCloser, error) { | |
50 | return ioutil.NopCloser(bytes.NewReader(s.data)), nil | |
51 | } | |
52 | ||
53 | // sourceReadCloser represents an input stream with Close method. | |
54 | type sourceReadCloser struct { | |
55 | reader io.ReadCloser | |
56 | } | |
57 | ||
58 | func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) { | |
59 | return s.reader, nil | |
60 | } | |
61 | ||
62 | func parseDataSource(source interface{}) (dataSource, error) { | |
63 | switch s := source.(type) { | |
64 | case string: | |
65 | return sourceFile{s}, nil | |
66 | case []byte: | |
67 | return &sourceData{s}, nil | |
68 | case io.ReadCloser: | |
69 | return &sourceReadCloser{s}, nil | |
70 | case io.Reader: | |
71 | return &sourceReadCloser{ioutil.NopCloser(s)}, nil | |
72 | default: | |
73 | return nil, fmt.Errorf("error parsing data source: unknown type %q", s) | |
74 | } | |
75 | } |
0 | golang-github-go-ini-ini (1.62.0-1) UNRELEASED; urgency=low | |
1 | ||
2 | * New upstream release. | |
3 | ||
4 | -- Debian Janitor <janitor@jelmer.uk> Mon, 07 Jun 2021 12:02:28 -0000 | |
5 | ||
0 | 6 | golang-github-go-ini-ini (1.32.0-2) unstable; urgency=medium |
1 | 7 | |
2 | 8 | * Fix URI of Vcs-Browser and Vcs-Git. |
0 | // Copyright 2019 Unknwon | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"): you may | |
3 | // not use this file except in compliance with the License. You may obtain | |
4 | // a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
10 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
11 | // License for the specific language governing permissions and limitations | |
12 | // under the License. | |
13 | ||
14 | package ini | |
15 | ||
16 | const ( | |
17 | // Deprecated: Use "DefaultSection" instead. | |
18 | DEFAULT_SECTION = DefaultSection | |
19 | ) | |
20 | ||
21 | var ( | |
22 | // Deprecated: AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE. | |
23 | AllCapsUnderscore = SnackCase | |
24 | ) |
17 | 17 | "fmt" |
18 | 18 | ) |
19 | 19 | |
20 | // ErrDelimiterNotFound indicates the error type of no delimiter is found which there should be one. | |
20 | 21 | type ErrDelimiterNotFound struct { |
21 | 22 | Line string |
22 | 23 | } |
23 | 24 | |
25 | // IsErrDelimiterNotFound returns true if the given error is an instance of ErrDelimiterNotFound. | |
24 | 26 | func IsErrDelimiterNotFound(err error) bool { |
25 | 27 | _, ok := err.(ErrDelimiterNotFound) |
26 | 28 | return ok |
24 | 24 | "sync" |
25 | 25 | ) |
26 | 26 | |
27 | // File represents a combination of a or more INI file(s) in memory. | |
27 | // File represents a combination of one or more INI files in memory. | |
28 | 28 | type File struct { |
29 | 29 | options LoadOptions |
30 | 30 | dataSources []dataSource |
35 | 35 | |
36 | 36 | // To keep data in order. |
37 | 37 | sectionList []string |
38 | // To keep track of the index of a section with same name. | |
39 | // This meta list is only used with non-unique section names are allowed. | |
40 | sectionIndexes []int | |
41 | ||
38 | 42 | // Actual data is stored here. |
39 | sections map[string]*Section | |
43 | sections map[string][]*Section | |
40 | 44 | |
41 | 45 | NameMapper |
42 | 46 | ValueMapper |
44 | 48 | |
45 | 49 | // newFile initializes File object with given data sources. |
46 | 50 | func newFile(dataSources []dataSource, opts LoadOptions) *File { |
51 | if len(opts.KeyValueDelimiters) == 0 { | |
52 | opts.KeyValueDelimiters = "=:" | |
53 | } | |
54 | if len(opts.KeyValueDelimiterOnWrite) == 0 { | |
55 | opts.KeyValueDelimiterOnWrite = "=" | |
56 | } | |
57 | if len(opts.ChildSectionDelimiter) == 0 { | |
58 | opts.ChildSectionDelimiter = "." | |
59 | } | |
60 | ||
47 | 61 | return &File{ |
48 | 62 | BlockMode: true, |
49 | 63 | dataSources: dataSources, |
50 | sections: make(map[string]*Section), | |
51 | sectionList: make([]string, 0, 10), | |
64 | sections: make(map[string][]*Section), | |
52 | 65 | options: opts, |
53 | 66 | } |
54 | 67 | } |
55 | 68 | |
56 | 69 | // Empty returns an empty file object. |
57 | func Empty() *File { | |
58 | // Ignore error here, we sure our data is good. | |
59 | f, _ := Load([]byte("")) | |
70 | func Empty(opts ...LoadOptions) *File { | |
71 | var opt LoadOptions | |
72 | if len(opts) > 0 { | |
73 | opt = opts[0] | |
74 | } | |
75 | ||
76 | // Ignore error here, we are sure our data is good. | |
77 | f, _ := LoadSources(opt, []byte("")) | |
60 | 78 | return f |
61 | 79 | } |
62 | 80 | |
63 | 81 | // NewSection creates a new section. |
64 | 82 | func (f *File) NewSection(name string) (*Section, error) { |
65 | 83 | if len(name) == 0 { |
66 | return nil, errors.New("error creating new section: empty section name") | |
67 | } else if f.options.Insensitive && name != DEFAULT_SECTION { | |
84 | return nil, errors.New("empty section name") | |
85 | } | |
86 | ||
87 | if (f.options.Insensitive || f.options.InsensitiveSections) && name != DefaultSection { | |
68 | 88 | name = strings.ToLower(name) |
69 | 89 | } |
70 | 90 | |
73 | 93 | defer f.lock.Unlock() |
74 | 94 | } |
75 | 95 | |
76 | if inSlice(name, f.sectionList) { | |
77 | return f.sections[name], nil | |
96 | if !f.options.AllowNonUniqueSections && inSlice(name, f.sectionList) { | |
97 | return f.sections[name][0], nil | |
78 | 98 | } |
79 | 99 | |
80 | 100 | f.sectionList = append(f.sectionList, name) |
81 | f.sections[name] = newSection(f, name) | |
82 | return f.sections[name], nil | |
101 | ||
102 | // NOTE: Append to indexes must happen before appending to sections, | |
103 | // otherwise index will have off-by-one problem. | |
104 | f.sectionIndexes = append(f.sectionIndexes, len(f.sections[name])) | |
105 | ||
106 | sec := newSection(f, name) | |
107 | f.sections[name] = append(f.sections[name], sec) | |
108 | ||
109 | return sec, nil | |
83 | 110 | } |
84 | 111 | |
85 | 112 | // NewRawSection creates a new section with an unparseable body. |
106 | 133 | |
107 | 134 | // GetSection returns section by given name. |
108 | 135 | func (f *File) GetSection(name string) (*Section, error) { |
136 | secs, err := f.SectionsByName(name) | |
137 | if err != nil { | |
138 | return nil, err | |
139 | } | |
140 | ||
141 | return secs[0], err | |
142 | } | |
143 | ||
144 | // SectionsByName returns all sections with given name. | |
145 | func (f *File) SectionsByName(name string) ([]*Section, error) { | |
109 | 146 | if len(name) == 0 { |
110 | name = DEFAULT_SECTION | |
111 | } | |
112 | if f.options.Insensitive { | |
147 | name = DefaultSection | |
148 | } | |
149 | if f.options.Insensitive || f.options.InsensitiveSections { | |
113 | 150 | name = strings.ToLower(name) |
114 | 151 | } |
115 | 152 | |
118 | 155 | defer f.lock.RUnlock() |
119 | 156 | } |
120 | 157 | |
121 | sec := f.sections[name] | |
122 | if sec == nil { | |
123 | return nil, fmt.Errorf("section '%s' does not exist", name) | |
124 | } | |
125 | return sec, nil | |
158 | secs := f.sections[name] | |
159 | if len(secs) == 0 { | |
160 | return nil, fmt.Errorf("section %q does not exist", name) | |
161 | } | |
162 | ||
163 | return secs, nil | |
126 | 164 | } |
127 | 165 | |
128 | 166 | // Section assumes named section exists and returns a zero-value when not. |
137 | 175 | return sec |
138 | 176 | } |
139 | 177 | |
140 | // Section returns list of Section. | |
178 | // SectionWithIndex assumes named section exists and returns a new section when not. | |
179 | func (f *File) SectionWithIndex(name string, index int) *Section { | |
180 | secs, err := f.SectionsByName(name) | |
181 | if err != nil || len(secs) <= index { | |
182 | // NOTE: It's OK here because the only possible error is empty section name, | |
183 | // but if it's empty, this piece of code won't be executed. | |
184 | newSec, _ := f.NewSection(name) | |
185 | return newSec | |
186 | } | |
187 | ||
188 | return secs[index] | |
189 | } | |
190 | ||
191 | // Sections returns a list of Section stored in the current instance. | |
141 | 192 | func (f *File) Sections() []*Section { |
193 | if f.BlockMode { | |
194 | f.lock.RLock() | |
195 | defer f.lock.RUnlock() | |
196 | } | |
197 | ||
142 | 198 | sections := make([]*Section, len(f.sectionList)) |
143 | for i := range f.sectionList { | |
144 | sections[i] = f.Section(f.sectionList[i]) | |
199 | for i, name := range f.sectionList { | |
200 | sections[i] = f.sections[name][f.sectionIndexes[i]] | |
145 | 201 | } |
146 | 202 | return sections |
147 | 203 | } |
158 | 214 | return list |
159 | 215 | } |
160 | 216 | |
161 | // DeleteSection deletes a section. | |
217 | // DeleteSection deletes a section or all sections with given name. | |
162 | 218 | func (f *File) DeleteSection(name string) { |
219 | secs, err := f.SectionsByName(name) | |
220 | if err != nil { | |
221 | return | |
222 | } | |
223 | ||
224 | for i := 0; i < len(secs); i++ { | |
225 | // For non-unique sections, it is always needed to remove the first one so | |
226 | // in the next iteration, the subsequent section continue having index 0. | |
227 | // Ignoring the error as index 0 never returns an error. | |
228 | _ = f.DeleteSectionWithIndex(name, 0) | |
229 | } | |
230 | } | |
231 | ||
232 | // DeleteSectionWithIndex deletes a section with given name and index. | |
233 | func (f *File) DeleteSectionWithIndex(name string, index int) error { | |
234 | if !f.options.AllowNonUniqueSections && index != 0 { | |
235 | return fmt.Errorf("delete section with non-zero index is only allowed when non-unique sections is enabled") | |
236 | } | |
237 | ||
238 | if len(name) == 0 { | |
239 | name = DefaultSection | |
240 | } | |
241 | if f.options.Insensitive || f.options.InsensitiveSections { | |
242 | name = strings.ToLower(name) | |
243 | } | |
244 | ||
163 | 245 | if f.BlockMode { |
164 | 246 | f.lock.Lock() |
165 | 247 | defer f.lock.Unlock() |
166 | 248 | } |
167 | 249 | |
168 | if len(name) == 0 { | |
169 | name = DEFAULT_SECTION | |
170 | } | |
171 | ||
172 | for i, s := range f.sectionList { | |
173 | if s == name { | |
250 | // Count occurrences of the sections | |
251 | occurrences := 0 | |
252 | ||
253 | sectionListCopy := make([]string, len(f.sectionList)) | |
254 | copy(sectionListCopy, f.sectionList) | |
255 | ||
256 | for i, s := range sectionListCopy { | |
257 | if s != name { | |
258 | continue | |
259 | } | |
260 | ||
261 | if occurrences == index { | |
262 | if len(f.sections[name]) <= 1 { | |
263 | delete(f.sections, name) // The last one in the map | |
264 | } else { | |
265 | f.sections[name] = append(f.sections[name][:index], f.sections[name][index+1:]...) | |
266 | } | |
267 | ||
268 | // Fix section lists | |
174 | 269 | f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...) |
175 | delete(f.sections, name) | |
176 | return | |
177 | } | |
178 | } | |
270 | f.sectionIndexes = append(f.sectionIndexes[:i], f.sectionIndexes[i+1:]...) | |
271 | ||
272 | } else if occurrences > index { | |
273 | // Fix the indices of all following sections with this name. | |
274 | f.sectionIndexes[i-1]-- | |
275 | } | |
276 | ||
277 | occurrences++ | |
278 | } | |
279 | ||
280 | return nil | |
179 | 281 | } |
180 | 282 | |
181 | 283 | func (f *File) reload(s dataSource) error { |
194 | 296 | if err = f.reload(s); err != nil { |
195 | 297 | // In loose mode, we create an empty default section for nonexistent files. |
196 | 298 | if os.IsNotExist(err) && f.options.Loose { |
197 | f.parse(bytes.NewBuffer(nil)) | |
299 | _ = f.parse(bytes.NewBuffer(nil)) | |
198 | 300 | continue |
199 | 301 | } |
200 | 302 | return err |
303 | } | |
304 | if f.options.ShortCircuit { | |
305 | return nil | |
201 | 306 | } |
202 | 307 | } |
203 | 308 | return nil |
221 | 326 | } |
222 | 327 | |
223 | 328 | func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) { |
224 | equalSign := "=" | |
225 | if PrettyFormat { | |
226 | equalSign = " = " | |
329 | equalSign := DefaultFormatLeft + f.options.KeyValueDelimiterOnWrite + DefaultFormatRight | |
330 | ||
331 | if PrettyFormat || PrettyEqual { | |
332 | equalSign = fmt.Sprintf(" %s ", f.options.KeyValueDelimiterOnWrite) | |
227 | 333 | } |
228 | 334 | |
229 | 335 | // Use buffer to make sure target is safe until finish encoding. |
230 | 336 | buf := bytes.NewBuffer(nil) |
231 | 337 | for i, sname := range f.sectionList { |
232 | sec := f.Section(sname) | |
338 | sec := f.SectionWithIndex(sname, f.sectionIndexes[i]) | |
233 | 339 | if len(sec.Comment) > 0 { |
234 | if sec.Comment[0] != '#' && sec.Comment[0] != ';' { | |
235 | sec.Comment = "; " + sec.Comment | |
236 | } else { | |
237 | sec.Comment = sec.Comment[:1] + " " + strings.TrimSpace(sec.Comment[1:]) | |
238 | } | |
239 | if _, err := buf.WriteString(sec.Comment + LineBreak); err != nil { | |
240 | return nil, err | |
241 | } | |
242 | } | |
243 | ||
244 | if i > 0 || DefaultHeader { | |
340 | // Support multiline comments | |
341 | lines := strings.Split(sec.Comment, LineBreak) | |
342 | for i := range lines { | |
343 | if lines[i][0] != '#' && lines[i][0] != ';' { | |
344 | lines[i] = "; " + lines[i] | |
345 | } else { | |
346 | lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:]) | |
347 | } | |
348 | ||
349 | if _, err := buf.WriteString(lines[i] + LineBreak); err != nil { | |
350 | return nil, err | |
351 | } | |
352 | } | |
353 | } | |
354 | ||
355 | if i > 0 || DefaultHeader || (i == 0 && strings.ToUpper(sec.name) != DefaultSection) { | |
245 | 356 | if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil { |
246 | 357 | return nil, err |
247 | 358 | } |
267 | 378 | } |
268 | 379 | |
269 | 380 | // Count and generate alignment length and buffer spaces using the |
270 | // longest key. Keys may be modifed if they contain certain characters so | |
381 | // longest key. Keys may be modified if they contain certain characters so | |
271 | 382 | // we need to take that into account in our calculation. |
272 | 383 | alignLength := 0 |
273 | 384 | if PrettyFormat { |
274 | 385 | for _, kname := range sec.keyList { |
275 | 386 | keyLength := len(kname) |
276 | 387 | // First case will surround key by ` and second by """ |
277 | if strings.ContainsAny(kname, "\"=:") { | |
388 | if strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters) { | |
278 | 389 | keyLength += 2 |
279 | 390 | } else if strings.Contains(kname, "`") { |
280 | 391 | keyLength += 6 |
287 | 398 | } |
288 | 399 | alignSpaces := bytes.Repeat([]byte(" "), alignLength) |
289 | 400 | |
290 | KEY_LIST: | |
401 | KeyList: | |
291 | 402 | for _, kname := range sec.keyList { |
292 | 403 | key := sec.Key(kname) |
293 | 404 | if len(key.Comment) > 0 { |
294 | if len(indent) > 0 && sname != DEFAULT_SECTION { | |
405 | if len(indent) > 0 && sname != DefaultSection { | |
295 | 406 | buf.WriteString(indent) |
296 | 407 | } |
297 | if key.Comment[0] != '#' && key.Comment[0] != ';' { | |
298 | key.Comment = "; " + key.Comment | |
299 | } else { | |
300 | key.Comment = key.Comment[:1] + " " + strings.TrimSpace(key.Comment[1:]) | |
301 | } | |
302 | if _, err := buf.WriteString(key.Comment + LineBreak); err != nil { | |
303 | return nil, err | |
304 | } | |
305 | } | |
306 | ||
307 | if len(indent) > 0 && sname != DEFAULT_SECTION { | |
408 | ||
409 | // Support multiline comments | |
410 | lines := strings.Split(key.Comment, LineBreak) | |
411 | for i := range lines { | |
412 | if lines[i][0] != '#' && lines[i][0] != ';' { | |
413 | lines[i] = "; " + strings.TrimSpace(lines[i]) | |
414 | } else { | |
415 | lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:]) | |
416 | } | |
417 | ||
418 | if _, err := buf.WriteString(lines[i] + LineBreak); err != nil { | |
419 | return nil, err | |
420 | } | |
421 | } | |
422 | } | |
423 | ||
424 | if len(indent) > 0 && sname != DefaultSection { | |
308 | 425 | buf.WriteString(indent) |
309 | 426 | } |
310 | 427 | |
311 | 428 | switch { |
312 | 429 | case key.isAutoIncrement: |
313 | 430 | kname = "-" |
314 | case strings.ContainsAny(kname, "\"=:"): | |
431 | case strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters): | |
315 | 432 | kname = "`" + kname + "`" |
316 | 433 | case strings.Contains(kname, "`"): |
317 | 434 | kname = `"""` + kname + `"""` |
326 | 443 | if kname != sec.keyList[len(sec.keyList)-1] { |
327 | 444 | buf.WriteString(LineBreak) |
328 | 445 | } |
329 | continue KEY_LIST | |
446 | continue KeyList | |
330 | 447 | } |
331 | 448 | |
332 | 449 | // Write out alignment spaces before "=" sign |
339 | 456 | val = `"""` + val + `"""` |
340 | 457 | } else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") { |
341 | 458 | val = "`" + val + "`" |
459 | } else if len(strings.TrimSpace(val)) != len(val) { | |
460 | val = `"` + val + `"` | |
342 | 461 | } |
343 | 462 | if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil { |
344 | 463 | return nil, err |
382 | 501 | // SaveToIndent writes content to file system with given value indention. |
383 | 502 | func (f *File) SaveToIndent(filename, indent string) error { |
384 | 503 | // Note: Because we are truncating with os.Create, |
385 | // so it's safer to save to a temporary file location and rename afte done. | |
504 | // so it's safer to save to a temporary file location and rename after done. | |
386 | 505 | buf, err := f.writeToBuffer(indent) |
387 | 506 | if err != nil { |
388 | 507 | return err |
15 | 15 | |
16 | 16 | import ( |
17 | 17 | "bytes" |
18 | "io/ioutil" | |
19 | "runtime" | |
18 | 20 | "testing" |
19 | 21 | |
20 | 22 | . "github.com/smartystreets/goconvey/convey" |
44 | 46 | So(sec, ShouldNotBeNil) |
45 | 47 | So(sec.Name(), ShouldEqual, "author") |
46 | 48 | |
47 | So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "author"}) | |
49 | So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "author"}) | |
48 | 50 | |
49 | 51 | Convey("With duplicated name", func() { |
50 | 52 | sec, err := f.NewSection("author") |
52 | 54 | So(sec, ShouldNotBeNil) |
53 | 55 | |
54 | 56 | // Does nothing if section already exists |
55 | So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "author"}) | |
57 | So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "author"}) | |
56 | 58 | }) |
57 | 59 | |
58 | 60 | Convey("With empty string", func() { |
62 | 64 | }) |
63 | 65 | } |
64 | 66 | |
67 | func TestFile_NonUniqueSection(t *testing.T) { | |
68 | Convey("Read and write non-unique sections", t, func() { | |
69 | f, err := ini.LoadSources(ini.LoadOptions{ | |
70 | AllowNonUniqueSections: true, | |
71 | }, []byte(`[Interface] | |
72 | Address = 192.168.2.1 | |
73 | PrivateKey = <server's privatekey> | |
74 | ListenPort = 51820 | |
75 | ||
76 | [Peer] | |
77 | PublicKey = <client's publickey> | |
78 | AllowedIPs = 192.168.2.2/32 | |
79 | ||
80 | [Peer] | |
81 | PublicKey = <client2's publickey> | |
82 | AllowedIPs = 192.168.2.3/32`)) | |
83 | So(err, ShouldBeNil) | |
84 | So(f, ShouldNotBeNil) | |
85 | ||
86 | sec, err := f.NewSection("Peer") | |
87 | So(err, ShouldBeNil) | |
88 | So(f, ShouldNotBeNil) | |
89 | ||
90 | _, _ = sec.NewKey("PublicKey", "<client3's publickey>") | |
91 | _, _ = sec.NewKey("AllowedIPs", "192.168.2.4/32") | |
92 | ||
93 | var buf bytes.Buffer | |
94 | _, err = f.WriteTo(&buf) | |
95 | So(err, ShouldBeNil) | |
96 | str := buf.String() | |
97 | So(str, ShouldEqual, `[Interface] | |
98 | Address = 192.168.2.1 | |
99 | PrivateKey = <server's privatekey> | |
100 | ListenPort = 51820 | |
101 | ||
102 | [Peer] | |
103 | PublicKey = <client's publickey> | |
104 | AllowedIPs = 192.168.2.2/32 | |
105 | ||
106 | [Peer] | |
107 | PublicKey = <client2's publickey> | |
108 | AllowedIPs = 192.168.2.3/32 | |
109 | ||
110 | [Peer] | |
111 | PublicKey = <client3's publickey> | |
112 | AllowedIPs = 192.168.2.4/32 | |
113 | ||
114 | `) | |
115 | }) | |
116 | ||
117 | Convey("Delete non-unique section", t, func() { | |
118 | f, err := ini.LoadSources(ini.LoadOptions{ | |
119 | AllowNonUniqueSections: true, | |
120 | }, []byte(`[Interface] | |
121 | Address = 192.168.2.1 | |
122 | PrivateKey = <server's privatekey> | |
123 | ListenPort = 51820 | |
124 | ||
125 | [Peer] | |
126 | PublicKey = <client's publickey> | |
127 | AllowedIPs = 192.168.2.2/32 | |
128 | ||
129 | [Peer] | |
130 | PublicKey = <client2's publickey> | |
131 | AllowedIPs = 192.168.2.3/32 | |
132 | ||
133 | [Peer] | |
134 | PublicKey = <client3's publickey> | |
135 | AllowedIPs = 192.168.2.4/32 | |
136 | ||
137 | `)) | |
138 | So(err, ShouldBeNil) | |
139 | So(f, ShouldNotBeNil) | |
140 | ||
141 | err = f.DeleteSectionWithIndex("Peer", 1) | |
142 | So(err, ShouldBeNil) | |
143 | ||
144 | var buf bytes.Buffer | |
145 | _, err = f.WriteTo(&buf) | |
146 | So(err, ShouldBeNil) | |
147 | str := buf.String() | |
148 | So(str, ShouldEqual, `[Interface] | |
149 | Address = 192.168.2.1 | |
150 | PrivateKey = <server's privatekey> | |
151 | ListenPort = 51820 | |
152 | ||
153 | [Peer] | |
154 | PublicKey = <client's publickey> | |
155 | AllowedIPs = 192.168.2.2/32 | |
156 | ||
157 | [Peer] | |
158 | PublicKey = <client3's publickey> | |
159 | AllowedIPs = 192.168.2.4/32 | |
160 | ||
161 | `) | |
162 | }) | |
163 | ||
164 | Convey("Delete all sections", t, func() { | |
165 | f := ini.Empty(ini.LoadOptions{ | |
166 | AllowNonUniqueSections: true, | |
167 | }) | |
168 | So(f, ShouldNotBeNil) | |
169 | ||
170 | _ = f.NewSections("Interface", "Peer", "Peer") | |
171 | So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "Interface", "Peer", "Peer"}) | |
172 | f.DeleteSection("Peer") | |
173 | So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "Interface"}) | |
174 | }) | |
175 | } | |
176 | ||
65 | 177 | func TestFile_NewRawSection(t *testing.T) { |
66 | 178 | Convey("Create a new raw section", t, func() { |
67 | 179 | f := ini.Empty() |
73 | 185 | So(sec, ShouldNotBeNil) |
74 | 186 | So(sec.Name(), ShouldEqual, "comments") |
75 | 187 | |
76 | So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "comments"}) | |
188 | So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "comments"}) | |
77 | 189 | So(f.Section("comments").Body(), ShouldEqual, `1111111111111111111000000000000000001110000 |
78 | 190 | 111111111111111111100000000000111000000000`) |
79 | 191 | |
81 | 193 | sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000`) |
82 | 194 | So(err, ShouldBeNil) |
83 | 195 | So(sec, ShouldNotBeNil) |
84 | So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "comments"}) | |
196 | So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "comments"}) | |
85 | 197 | |
86 | 198 | // Overwrite previous existed section |
87 | 199 | So(f.Section("comments").Body(), ShouldEqual, `1111111111111111111000000000000000001110000`) |
100 | 212 | So(f, ShouldNotBeNil) |
101 | 213 | |
102 | 214 | So(f.NewSections("package", "author"), ShouldBeNil) |
103 | So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "package", "author"}) | |
215 | So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "package", "author"}) | |
104 | 216 | |
105 | 217 | Convey("With duplicated name", func() { |
106 | 218 | So(f.NewSections("author", "features"), ShouldBeNil) |
107 | 219 | |
108 | 220 | // Ignore section already exists |
109 | So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "package", "author", "features"}) | |
221 | So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "package", "author", "features"}) | |
110 | 222 | }) |
111 | 223 | |
112 | 224 | Convey("With empty string", func() { |
117 | 229 | |
118 | 230 | func TestFile_GetSection(t *testing.T) { |
119 | 231 | Convey("Get a section", t, func() { |
120 | f, err := ini.Load(_FULL_CONF) | |
232 | f, err := ini.Load(fullConf) | |
121 | 233 | So(err, ShouldBeNil) |
122 | 234 | So(f, ShouldNotBeNil) |
123 | 235 | |
135 | 247 | |
136 | 248 | func TestFile_Section(t *testing.T) { |
137 | 249 | Convey("Get a section", t, func() { |
138 | f, err := ini.Load(_FULL_CONF) | |
250 | f, err := ini.Load(fullConf) | |
139 | 251 | So(err, ShouldBeNil) |
140 | 252 | So(f, ShouldNotBeNil) |
141 | 253 | |
165 | 277 | |
166 | 278 | func TestFile_Sections(t *testing.T) { |
167 | 279 | Convey("Get all sections", t, func() { |
168 | f, err := ini.Load(_FULL_CONF) | |
280 | f, err := ini.Load(fullConf) | |
169 | 281 | So(err, ShouldBeNil) |
170 | 282 | So(f, ShouldNotBeNil) |
171 | 283 | |
172 | 284 | secs := f.Sections() |
173 | names := []string{ini.DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} | |
285 | names := []string{ini.DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} | |
174 | 286 | So(len(secs), ShouldEqual, len(names)) |
175 | 287 | for i, name := range names { |
176 | 288 | So(secs[i].Name(), ShouldEqual, name) |
201 | 313 | |
202 | 314 | func TestFile_SectionStrings(t *testing.T) { |
203 | 315 | Convey("Get all section names", t, func() { |
204 | f, err := ini.Load(_FULL_CONF) | |
205 | So(err, ShouldBeNil) | |
206 | So(f, ShouldNotBeNil) | |
207 | ||
208 | So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"}) | |
316 | f, err := ini.Load(fullConf) | |
317 | So(err, ShouldBeNil) | |
318 | So(f, ShouldNotBeNil) | |
319 | ||
320 | So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"}) | |
209 | 321 | }) |
210 | 322 | } |
211 | 323 | |
214 | 326 | f := ini.Empty() |
215 | 327 | So(f, ShouldNotBeNil) |
216 | 328 | |
217 | f.NewSections("author", "package", "features") | |
329 | _ = f.NewSections("author", "package", "features") | |
218 | 330 | f.DeleteSection("features") |
219 | 331 | f.DeleteSection("") |
220 | 332 | So(f.SectionStrings(), ShouldResemble, []string{"author", "package"}) |
221 | 333 | }) |
334 | ||
335 | Convey("Delete default section", t, func() { | |
336 | f := ini.Empty() | |
337 | So(f, ShouldNotBeNil) | |
338 | ||
339 | f.Section("").Key("foo").SetValue("bar") | |
340 | f.Section("section1").Key("key1").SetValue("value1") | |
341 | f.DeleteSection("") | |
342 | So(f.SectionStrings(), ShouldResemble, []string{"section1"}) | |
343 | ||
344 | var buf bytes.Buffer | |
345 | _, err := f.WriteTo(&buf) | |
346 | So(err, ShouldBeNil) | |
347 | ||
348 | So(buf.String(), ShouldEqual, `[section1] | |
349 | key1 = value1 | |
350 | ||
351 | `) | |
352 | }) | |
353 | ||
354 | Convey("Delete a section with InsensitiveSections", t, func() { | |
355 | f := ini.Empty(ini.LoadOptions{InsensitiveSections: true}) | |
356 | So(f, ShouldNotBeNil) | |
357 | ||
358 | _ = f.NewSections("author", "package", "features") | |
359 | f.DeleteSection("FEATURES") | |
360 | f.DeleteSection("") | |
361 | So(f.SectionStrings(), ShouldResemble, []string{"author", "package"}) | |
362 | }) | |
222 | 363 | } |
223 | 364 | |
224 | 365 | func TestFile_Append(t *testing.T) { |
226 | 367 | f := ini.Empty() |
227 | 368 | So(f, ShouldNotBeNil) |
228 | 369 | |
229 | So(f.Append(_MINIMAL_CONF, []byte(` | |
370 | So(f.Append(minimalConf, []byte(` | |
230 | 371 | [author] |
231 | 372 | NAME = Unknwon`)), ShouldBeNil) |
232 | 373 | |
233 | 374 | Convey("With bad input", func() { |
234 | 375 | So(f.Append(123), ShouldNotBeNil) |
235 | So(f.Append(_MINIMAL_CONF, 123), ShouldNotBeNil) | |
376 | So(f.Append(minimalConf, 123), ShouldNotBeNil) | |
236 | 377 | }) |
237 | 378 | }) |
238 | 379 | } |
239 | 380 | |
240 | 381 | func TestFile_WriteTo(t *testing.T) { |
382 | if runtime.GOOS == "windows" { | |
383 | t.Skip("Skipping testing on Windows") | |
384 | } | |
385 | ||
241 | 386 | Convey("Write content to somewhere", t, func() { |
242 | f, err := ini.Load(_FULL_CONF) | |
387 | f, err := ini.Load(fullConf) | |
243 | 388 | So(err, ShouldBeNil) |
244 | 389 | So(f, ShouldNotBeNil) |
245 | 390 | |
246 | 391 | f.Section("author").Comment = `Information about package author |
247 | 392 | # Bio can be written in multiple lines.` |
248 | 393 | f.Section("author").Key("NAME").Comment = "This is author name" |
249 | f.Section("note").NewBooleanKey("boolean_key") | |
250 | f.Section("note").NewKey("more", "notes") | |
394 | _, _ = f.Section("note").NewBooleanKey("boolean_key") | |
395 | _, _ = f.Section("note").NewKey("more", "notes") | |
251 | 396 | |
252 | 397 | var buf bytes.Buffer |
253 | 398 | _, err = f.WriteTo(&buf) |
254 | 399 | So(err, ShouldBeNil) |
255 | So(buf.String(), ShouldEqual, `; Package name | |
256 | NAME = ini | |
257 | ; Package version | |
258 | VERSION = v1 | |
259 | ; Package import path | |
260 | IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s | |
261 | ||
262 | ; Information about package author | |
263 | # Bio can be written in multiple lines. | |
264 | [author] | |
265 | ; This is author name | |
266 | NAME = Unknwon | |
267 | E-MAIL = u@gogs.io | |
268 | GITHUB = https://github.com/%(NAME)s | |
269 | # Succeeding comment | |
270 | BIO = """Gopher. | |
271 | Coding addict. | |
272 | Good man. | |
273 | """ | |
274 | ||
275 | [package] | |
276 | CLONE_URL = https://%(IMPORT_PATH)s | |
277 | ||
278 | [package.sub] | |
279 | UNUSED_KEY = should be deleted | |
280 | ||
281 | [features] | |
282 | - = Support read/write comments of keys and sections | |
283 | - = Support auto-increment of key names | |
284 | - = Support load multiple files to overwrite key values | |
285 | ||
286 | [types] | |
287 | STRING = str | |
288 | BOOL = true | |
289 | BOOL_FALSE = false | |
290 | FLOAT64 = 1.25 | |
291 | INT = 10 | |
292 | TIME = 2015-01-01T20:17:05Z | |
293 | DURATION = 2h45m | |
294 | UINT = 3 | |
295 | ||
296 | [array] | |
297 | STRINGS = en, zh, de | |
298 | FLOAT64S = 1.1, 2.2, 3.3 | |
299 | INTS = 1, 2, 3 | |
300 | UINTS = 1, 2, 3 | |
301 | TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z | |
302 | ||
303 | [note] | |
304 | empty_lines = next line is empty | |
305 | boolean_key | |
306 | more = notes | |
307 | ||
308 | ; Comment before the section | |
309 | ; This is a comment for the section too | |
310 | [comments] | |
311 | ; Comment before key | |
312 | key = value | |
313 | ; This is a comment for key2 | |
314 | key2 = value2 | |
315 | key3 = "one", "two", "three" | |
316 | ||
317 | [string escapes] | |
318 | key1 = value1, value2, value3 | |
319 | key2 = value1\, value2 | |
320 | key3 = val\ue1, value2 | |
321 | key4 = value1\\, value\\\\2 | |
322 | key5 = value1\,, value2 | |
323 | key6 = aaa bbb\ and\ space ccc | |
324 | ||
325 | [advance] | |
326 | value with quotes = some value | |
327 | value quote2 again = some value | |
328 | includes comment sign = `+"`"+"my#password"+"`"+` | |
329 | includes comment sign2 = `+"`"+"my;password"+"`"+` | |
330 | true = 2+3=5 | |
331 | `+"`"+`1+1=2`+"`"+` = true | |
332 | `+"`"+`6+1=7`+"`"+` = true | |
333 | """`+"`"+`5+5`+"`"+`""" = 10 | |
334 | `+"`"+`"6+6"`+"`"+` = 12 | |
335 | `+"`"+`7-2=4`+"`"+` = false | |
336 | ADDRESS = """404 road, | |
337 | NotFound, State, 50000""" | |
338 | two_lines = how about continuation lines? | |
339 | lots_of_lines = 1 2 3 4 | |
400 | ||
401 | golden := "testdata/TestFile_WriteTo.golden" | |
402 | if *update { | |
403 | So(ioutil.WriteFile(golden, buf.Bytes(), 0644), ShouldBeNil) | |
404 | } | |
405 | ||
406 | expected, err := ioutil.ReadFile(golden) | |
407 | So(err, ShouldBeNil) | |
408 | So(buf.String(), ShouldEqual, string(expected)) | |
409 | }) | |
410 | ||
411 | Convey("Support multiline comments", t, func() { | |
412 | f, err := ini.Load([]byte(` | |
413 | # | |
414 | # general.domain | |
415 | # | |
416 | # Domain name of XX system. | |
417 | domain = mydomain.com | |
418 | `)) | |
419 | So(err, ShouldBeNil) | |
420 | ||
421 | f.Section("").Key("test").Comment = "Multiline\nComment" | |
422 | ||
423 | var buf bytes.Buffer | |
424 | _, err = f.WriteTo(&buf) | |
425 | So(err, ShouldBeNil) | |
426 | ||
427 | So(buf.String(), ShouldEqual, `# | |
428 | # general.domain | |
429 | # | |
430 | # Domain name of XX system. | |
431 | domain = mydomain.com | |
432 | ; Multiline | |
433 | ; Comment | |
434 | test = | |
435 | ||
436 | `) | |
437 | ||
438 | }) | |
439 | ||
440 | Convey("Keep leading and trailing spaces in value", t, func() { | |
441 | f, _ := ini.Load([]byte(`[foo] | |
442 | bar1 = ' val ue1 ' | |
443 | bar2 = """ val ue2 """ | |
444 | bar3 = " val ue3 " | |
445 | `)) | |
446 | So(f, ShouldNotBeNil) | |
447 | ||
448 | var buf bytes.Buffer | |
449 | _, err := f.WriteTo(&buf) | |
450 | So(err, ShouldBeNil) | |
451 | So(buf.String(), ShouldEqual, `[foo] | |
452 | bar1 = " val ue1 " | |
453 | bar2 = " val ue2 " | |
454 | bar3 = " val ue3 " | |
340 | 455 | |
341 | 456 | `) |
342 | 457 | }) |
344 | 459 | |
345 | 460 | func TestFile_SaveTo(t *testing.T) { |
346 | 461 | Convey("Write content to somewhere", t, func() { |
347 | f, err := ini.Load(_FULL_CONF) | |
462 | f, err := ini.Load(fullConf) | |
348 | 463 | So(err, ShouldBeNil) |
349 | 464 | So(f, ShouldNotBeNil) |
350 | 465 | |
352 | 467 | So(f.SaveToIndent("testdata/conf_out.ini", "\t"), ShouldBeNil) |
353 | 468 | }) |
354 | 469 | } |
470 | ||
471 | func TestFile_WriteToWithOutputDelimiter(t *testing.T) { | |
472 | Convey("Write content to somewhere using a custom output delimiter", t, func() { | |
473 | f, err := ini.LoadSources(ini.LoadOptions{ | |
474 | KeyValueDelimiterOnWrite: "->", | |
475 | }, []byte(`[Others] | |
476 | Cities = HangZhou|Boston | |
477 | Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z | |
478 | Years = 1993,1994 | |
479 | Numbers = 10010,10086 | |
480 | Ages = 18,19 | |
481 | Populations = 12345678,98765432 | |
482 | Coordinates = 192.168,10.11 | |
483 | Flags = true,false | |
484 | Note = Hello world!`)) | |
485 | So(err, ShouldBeNil) | |
486 | So(f, ShouldNotBeNil) | |
487 | ||
488 | var actual bytes.Buffer | |
489 | var expected = []byte(`[Others] | |
490 | Cities -> HangZhou|Boston | |
491 | Visits -> 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z | |
492 | Years -> 1993,1994 | |
493 | Numbers -> 10010,10086 | |
494 | Ages -> 18,19 | |
495 | Populations -> 12345678,98765432 | |
496 | Coordinates -> 192.168,10.11 | |
497 | Flags -> true,false | |
498 | Note -> Hello world! | |
499 | ||
500 | `) | |
501 | _, err = f.WriteTo(&actual) | |
502 | So(err, ShouldBeNil) | |
503 | ||
504 | So(bytes.Equal(expected, actual.Bytes()), ShouldBeTrue) | |
505 | }) | |
506 | } | |
507 | ||
508 | // Inspired by https://github.com/go-ini/ini/issues/207 | |
509 | func TestReloadAfterShadowLoad(t *testing.T) { | |
510 | Convey("Reload file after ShadowLoad", t, func() { | |
511 | f, err := ini.ShadowLoad([]byte(` | |
512 | [slice] | |
513 | v = 1 | |
514 | v = 2 | |
515 | v = 3 | |
516 | `)) | |
517 | So(err, ShouldBeNil) | |
518 | So(f, ShouldNotBeNil) | |
519 | ||
520 | So(f.Section("slice").Key("v").ValueWithShadows(), ShouldResemble, []string{"1", "2", "3"}) | |
521 | ||
522 | So(f.Reload(), ShouldBeNil) | |
523 | So(f.Section("slice").Key("v").ValueWithShadows(), ShouldResemble, []string{"1", "2", "3"}) | |
524 | }) | |
525 | } |
0 | // Copyright 2019 Unknwon | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"): you may | |
3 | // not use this file except in compliance with the License. You may obtain | |
4 | // a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
10 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
11 | // License for the specific language governing permissions and limitations | |
12 | // under the License. | |
13 | ||
14 | package ini | |
15 | ||
16 | func inSlice(str string, s []string) bool { | |
17 | for _, v := range s { | |
18 | if str == v { | |
19 | return true | |
20 | } | |
21 | } | |
22 | return false | |
23 | } |
0 | // Copyright 2019 Unknwon | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"): you may | |
3 | // not use this file except in compliance with the License. You may obtain | |
4 | // a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
10 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
11 | // License for the specific language governing permissions and limitations | |
12 | // under the License. | |
13 | ||
14 | package ini | |
15 | ||
16 | import ( | |
17 | "testing" | |
18 | ||
19 | . "github.com/smartystreets/goconvey/convey" | |
20 | ) | |
21 | ||
22 | func Test_isSlice(t *testing.T) { | |
23 | Convey("Check if a string is in the slice", t, func() { | |
24 | ss := []string{"a", "b", "c"} | |
25 | So(inSlice("a", ss), ShouldBeTrue) | |
26 | So(inSlice("d", ss), ShouldBeFalse) | |
27 | }) | |
28 | } |
0 | // +build go1.6 | |
1 | ||
0 | 2 | // Copyright 2014 Unknwon |
1 | 3 | // |
2 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may |
15 | 17 | package ini |
16 | 18 | |
17 | 19 | import ( |
18 | "bytes" | |
19 | "fmt" | |
20 | "io" | |
21 | "io/ioutil" | |
22 | 20 | "os" |
23 | 21 | "regexp" |
24 | 22 | "runtime" |
23 | "strings" | |
25 | 24 | ) |
26 | 25 | |
27 | 26 | const ( |
28 | // Name for default section. You can use this constant or the string literal. | |
27 | // DefaultSection is the name of default section. You can use this constant or the string literal. | |
29 | 28 | // In most of cases, an empty string is all you need to access the section. |
30 | DEFAULT_SECTION = "DEFAULT" | |
29 | DefaultSection = "DEFAULT" | |
31 | 30 | |
32 | 31 | // Maximum allowed depth when recursively substituing variable names. |
33 | _DEPTH_VALUES = 99 | |
34 | _VERSION = "1.32.0" | |
32 | depthValues = 99 | |
35 | 33 | ) |
36 | 34 | |
37 | // Version returns current package version literal. | |
38 | func Version() string { | |
39 | return _VERSION | |
40 | } | |
41 | ||
42 | 35 | var ( |
43 | // Delimiter to determine or compose a new line. | |
44 | // This variable will be changed to "\r\n" automatically on Windows | |
45 | // at package init time. | |
36 | // LineBreak is the delimiter to determine or compose a new line. | |
37 | // This variable will be changed to "\r\n" automatically on Windows at package init time. | |
46 | 38 | LineBreak = "\n" |
47 | 39 | |
48 | 40 | // Variable regexp pattern: %(variable)s |
49 | varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`) | |
41 | varPattern = regexp.MustCompile(`%\(([^)]+)\)s`) | |
50 | 42 | |
51 | // Indicate whether to align "=" sign with spaces to produce pretty output | |
43 | // DefaultHeader explicitly writes default section header. | |
44 | DefaultHeader = false | |
45 | ||
46 | // PrettySection indicates whether to put a line between sections. | |
47 | PrettySection = true | |
48 | // PrettyFormat indicates whether to align "=" sign with spaces to produce pretty output | |
52 | 49 | // or reduce all possible spaces for compact format. |
53 | 50 | PrettyFormat = true |
54 | ||
55 | // Explicitly write DEFAULT section header | |
56 | DefaultHeader = false | |
57 | ||
58 | // Indicate whether to put a line between sections | |
59 | PrettySection = true | |
51 | // PrettyEqual places spaces around "=" sign even when PrettyFormat is false. | |
52 | PrettyEqual = false | |
53 | // DefaultFormatLeft places custom spaces on the left when PrettyFormat and PrettyEqual are both disabled. | |
54 | DefaultFormatLeft = "" | |
55 | // DefaultFormatRight places custom spaces on the right when PrettyFormat and PrettyEqual are both disabled. | |
56 | DefaultFormatRight = "" | |
60 | 57 | ) |
61 | 58 | |
59 | var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test") | |
60 | ||
62 | 61 | func init() { |
63 | if runtime.GOOS == "windows" { | |
62 | if runtime.GOOS == "windows" && !inTest { | |
64 | 63 | LineBreak = "\r\n" |
65 | 64 | } |
66 | 65 | } |
67 | 66 | |
68 | func inSlice(str string, s []string) bool { | |
69 | for _, v := range s { | |
70 | if str == v { | |
71 | return true | |
72 | } | |
73 | } | |
74 | return false | |
75 | } | |
76 | ||
77 | // dataSource is an interface that returns object which can be read and closed. | |
78 | type dataSource interface { | |
79 | ReadCloser() (io.ReadCloser, error) | |
80 | } | |
81 | ||
82 | // sourceFile represents an object that contains content on the local file system. | |
83 | type sourceFile struct { | |
84 | name string | |
85 | } | |
86 | ||
87 | func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { | |
88 | return os.Open(s.name) | |
89 | } | |
90 | ||
91 | // sourceData represents an object that contains content in memory. | |
92 | type sourceData struct { | |
93 | data []byte | |
94 | } | |
95 | ||
96 | func (s *sourceData) ReadCloser() (io.ReadCloser, error) { | |
97 | return ioutil.NopCloser(bytes.NewReader(s.data)), nil | |
98 | } | |
99 | ||
100 | // sourceReadCloser represents an input stream with Close method. | |
101 | type sourceReadCloser struct { | |
102 | reader io.ReadCloser | |
103 | } | |
104 | ||
105 | func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) { | |
106 | return s.reader, nil | |
107 | } | |
108 | ||
109 | func parseDataSource(source interface{}) (dataSource, error) { | |
110 | switch s := source.(type) { | |
111 | case string: | |
112 | return sourceFile{s}, nil | |
113 | case []byte: | |
114 | return &sourceData{s}, nil | |
115 | case io.ReadCloser: | |
116 | return &sourceReadCloser{s}, nil | |
117 | default: | |
118 | return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s) | |
119 | } | |
120 | } | |
121 | ||
67 | // LoadOptions contains all customized options used for load data source(s). | |
122 | 68 | type LoadOptions struct { |
123 | 69 | // Loose indicates whether the parser should ignore nonexistent files or return error. |
124 | 70 | Loose bool |
125 | 71 | // Insensitive indicates whether the parser forces all section and key names to lowercase. |
126 | 72 | Insensitive bool |
73 | // InsensitiveSections indicates whether the parser forces all section to lowercase. | |
74 | InsensitiveSections bool | |
75 | // InsensitiveKeys indicates whether the parser forces all key names to lowercase. | |
76 | InsensitiveKeys bool | |
127 | 77 | // IgnoreContinuation indicates whether to ignore continuation lines while parsing. |
128 | 78 | IgnoreContinuation bool |
129 | 79 | // IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value. |
130 | 80 | IgnoreInlineComment bool |
81 | // SkipUnrecognizableLines indicates whether to skip unrecognizable lines that do not conform to key/value pairs. | |
82 | SkipUnrecognizableLines bool | |
83 | // ShortCircuit indicates whether to ignore other configuration sources after loaded the first available configuration source. | |
84 | ShortCircuit bool | |
131 | 85 | // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing. |
132 | 86 | // This type of keys are mostly used in my.cnf. |
133 | 87 | AllowBooleanKeys bool |
136 | 90 | // AllowNestedValues indicates whether to allow AWS-like nested values. |
137 | 91 | // Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values |
138 | 92 | AllowNestedValues bool |
93 | // AllowPythonMultilineValues indicates whether to allow Python-like multi-line values. | |
94 | // Docs: https://docs.python.org/3/library/configparser.html#supported-ini-file-structure | |
95 | // Relevant quote: Values can also span multiple lines, as long as they are indented deeper | |
96 | // than the first line of the value. | |
97 | AllowPythonMultilineValues bool | |
98 | // SpaceBeforeInlineComment indicates whether to allow comment symbols (\# and \;) inside value. | |
99 | // Docs: https://docs.python.org/2/library/configparser.html | |
100 | // Quote: Comments may appear on their own in an otherwise empty line, or may be entered in lines holding values or section names. | |
101 | // In the latter case, they need to be preceded by a whitespace character to be recognized as a comment. | |
102 | SpaceBeforeInlineComment bool | |
139 | 103 | // UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format |
140 | 104 | // when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value" |
141 | 105 | UnescapeValueDoubleQuotes bool |
143 | 107 | // when value is NOT surrounded by any quotes. |
144 | 108 | // Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all. |
145 | 109 | UnescapeValueCommentSymbols bool |
146 | // Some INI formats allow group blocks that store a block of raw content that doesn't otherwise | |
110 | // UnparseableSections stores a list of blocks that are allowed with raw content which do not otherwise | |
147 | 111 | // conform to key/value pairs. Specify the names of those blocks here. |
148 | 112 | UnparseableSections []string |
113 | // KeyValueDelimiters is the sequence of delimiters that are used to separate key and value. By default, it is "=:". | |
114 | KeyValueDelimiters string | |
115 | // KeyValueDelimiterOnWrite is the delimiter that are used to separate key and value output. By default, it is "=". | |
116 | KeyValueDelimiterOnWrite string | |
117 | // ChildSectionDelimiter is the delimiter that is used to separate child sections. By default, it is ".". | |
118 | ChildSectionDelimiter string | |
119 | // PreserveSurroundedQuote indicates whether to preserve surrounded quote (single and double quotes). | |
120 | PreserveSurroundedQuote bool | |
121 | // DebugFunc is called to collect debug information (currently only useful to debug parsing Python-style multiline values). | |
122 | DebugFunc DebugFunc | |
123 | // ReaderBufferSize is the buffer size of the reader in bytes. | |
124 | ReaderBufferSize int | |
125 | // AllowNonUniqueSections indicates whether to allow sections with the same name multiple times. | |
126 | AllowNonUniqueSections bool | |
149 | 127 | } |
150 | 128 | |
129 | // DebugFunc is the type of function called to log parse events. | |
130 | type DebugFunc func(message string) | |
131 | ||
132 | // LoadSources allows caller to apply customized options for loading from data source(s). | |
151 | 133 | func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) { |
152 | 134 | sources := make([]dataSource, len(others)+1) |
153 | 135 | sources[0], err = parseDataSource(source) |
186 | 168 | return LoadSources(LoadOptions{Insensitive: true}, source, others...) |
187 | 169 | } |
188 | 170 | |
189 | // InsensitiveLoad has exactly same functionality as Load function | |
171 | // ShadowLoad has exactly same functionality as Load function | |
190 | 172 | // except it allows have shadow keys. |
191 | 173 | func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { |
192 | 174 | return LoadSources(LoadOptions{AllowShadows: true}, source, others...) |
0 | // Copyright 2017 Unknwon | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"): you may | |
3 | // not use this file except in compliance with the License. You may obtain | |
4 | // a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
10 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
11 | // License for the specific language governing permissions and limitations | |
12 | // under the License. | |
13 | ||
14 | package ini | |
15 | ||
16 | import ( | |
17 | "testing" | |
18 | ||
19 | . "github.com/smartystreets/goconvey/convey" | |
20 | ) | |
21 | ||
22 | func Test_Version(t *testing.T) { | |
23 | Convey("Get version", t, func() { | |
24 | So(Version(), ShouldEqual, _VERSION) | |
25 | }) | |
26 | } | |
27 | ||
28 | func Test_isSlice(t *testing.T) { | |
29 | Convey("Check if a string is in the slice", t, func() { | |
30 | ss := []string{"a", "b", "c"} | |
31 | So(inSlice("a", ss), ShouldBeTrue) | |
32 | So(inSlice("d", ss), ShouldBeFalse) | |
33 | }) | |
34 | } |
0 | package ini_test | |
1 | ||
2 | import ( | |
3 | "path/filepath" | |
4 | "runtime" | |
5 | "testing" | |
6 | ||
7 | . "github.com/smartystreets/goconvey/convey" | |
8 | "gopkg.in/ini.v1" | |
9 | ) | |
10 | ||
11 | type testData struct { | |
12 | Value1 string `ini:"value1"` | |
13 | Value2 string `ini:"value2"` | |
14 | Value3 string `ini:"value3"` | |
15 | } | |
16 | ||
17 | func TestMultiline(t *testing.T) { | |
18 | if runtime.GOOS == "windows" { | |
19 | t.Skip("Skipping testing on Windows") | |
20 | } | |
21 | ||
22 | Convey("Parse Python-style multiline values", t, func() { | |
23 | path := filepath.Join("testdata", "multiline.ini") | |
24 | f, err := ini.LoadSources(ini.LoadOptions{ | |
25 | AllowPythonMultilineValues: true, | |
26 | ReaderBufferSize: 64 * 1024, | |
27 | }, path) | |
28 | So(err, ShouldBeNil) | |
29 | So(f, ShouldNotBeNil) | |
30 | So(len(f.Sections()), ShouldEqual, 1) | |
31 | ||
32 | defaultSection := f.Section("") | |
33 | So(f.Section(""), ShouldNotBeNil) | |
34 | ||
35 | var testData testData | |
36 | err = defaultSection.MapTo(&testData) | |
37 | So(err, ShouldBeNil) | |
38 | So(testData.Value1, ShouldEqual, "some text here\nsome more text here\n\nthere is an empty line above and below\n") | |
39 | So(testData.Value2, ShouldEqual, "there is an empty line above\nthat is not indented so it should not be part\nof the value") | |
40 | So(testData.Value3, ShouldEqual, `. | |
41 | ||
42 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eu consequat ac felis donec et odio pellentesque diam volutpat. Mauris commodo quis imperdiet massa tincidunt nunc. Interdum velit euismod in pellentesque. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Nascetur ridiculus mus mauris vitae. Posuere urna nec tincidunt praesent semper feugiat. Lorem donec massa sapien faucibus et molestie ac feugiat sed. Ipsum dolor sit amet consectetur adipiscing elit. Enim sed faucibus turpis in eu mi. A diam sollicitudin tempor id. Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit. | |
43 | ||
44 | Lectus sit amet est placerat in egestas. At risus viverra adipiscing at in tellus integer. Tristique senectus et netus et malesuada fames ac. In hac habitasse platea dictumst. Purus in mollis nunc sed. Pellentesque sit amet porttitor eget dolor morbi. Elit at imperdiet dui accumsan sit amet nulla. Cursus in hac habitasse platea dictumst. Bibendum arcu vitae elementum curabitur. Faucibus ornare suspendisse sed nisi lacus. In vitae turpis massa sed. Libero nunc consequat interdum varius sit amet. Molestie a iaculis at erat pellentesque. | |
45 | ||
46 | Dui faucibus in ornare quam viverra orci sagittis eu. Purus in mollis nunc sed id semper. Sed arcu non odio euismod lacinia at. Quis commodo odio aenean sed adipiscing diam donec. Quisque id diam vel quam elementum pulvinar. Lorem ipsum dolor sit amet. Purus ut faucibus pulvinar elementum integer enim neque volutpat ac. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh sed. Gravida rutrum quisque non tellus orci. Ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant. Et sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Eget gravida cum sociis natoque penatibus et magnis. Elementum eu facilisis sed odio morbi quis commodo. Mollis nunc sed id semper risus in hendrerit gravida rutrum. Lorem dolor sed viverra ipsum. | |
47 | ||
48 | Pellentesque adipiscing commodo elit at imperdiet dui accumsan sit amet. Justo eget magna fermentum iaculis eu non diam. Condimentum mattis pellentesque id nibh tortor id aliquet lectus. Tellus molestie nunc non blandit massa enim. Mauris ultrices eros in cursus turpis. Purus viverra accumsan in nisl nisi scelerisque. Quis lectus nulla at volutpat. Purus ut faucibus pulvinar elementum integer enim. In pellentesque massa placerat duis ultricies lacus sed turpis. Elit sed vulputate mi sit amet mauris commodo. Tellus elementum sagittis vitae et. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi nullam. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Libero id faucibus nisl tincidunt eget nullam. Mattis aliquam faucibus purus in massa tempor. Fames ac turpis egestas sed tempus urna. Gravida in fermentum et sollicitudin ac orci phasellus egestas. | |
49 | ||
50 | Blandit turpis cursus in hac habitasse. Sed id semper risus in. Amet porttitor eget dolor morbi non arcu. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Ut morbi tincidunt augue interdum velit. Lorem mollis aliquam ut porttitor leo a. Nunc eget lorem dolor sed viverra. Scelerisque mauris pellentesque pulvinar pellentesque. Elit at imperdiet dui accumsan sit amet. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Laoreet non curabitur gravida arcu ac tortor dignissim. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus. Lacus sed viverra tellus in hac habitasse platea dictumst vestibulum. Viverra adipiscing at in tellus. Duis at tellus at urna condimentum. Eget gravida cum sociis natoque penatibus et magnis dis parturient. Pharetra massa massa ultricies mi quis hendrerit. | |
51 | ||
52 | Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Maecenas volutpat blandit aliquam etiam. Sed turpis tincidunt id aliquet. Eget duis at tellus at urna condimentum. Pellentesque habitant morbi tristique senectus et. Amet aliquam id diam maecenas. Volutpat est velit egestas dui id. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel orci. Massa sed elementum tempus egestas sed sed risus pretium. Quam quisque id diam vel quam elementum pulvinar etiam non. Sapien faucibus et molestie ac. Ipsum dolor sit amet consectetur adipiscing. Viverra orci sagittis eu volutpat. Leo urna molestie at elementum. Commodo viverra maecenas accumsan lacus. Non sodales neque sodales ut etiam sit amet. Habitant morbi tristique senectus et netus et malesuada fames. Habitant morbi tristique senectus et netus et malesuada. Blandit aliquam etiam erat velit scelerisque in. Varius duis at consectetur lorem donec massa sapien faucibus et. | |
53 | ||
54 | Augue mauris augue neque gravida in. Odio ut sem nulla pharetra diam sit amet nisl suscipit. Nulla aliquet enim tortor at auctor urna nunc id. Morbi tristique senectus et netus et malesuada fames ac. Quam id leo in vitae turpis massa sed elementum tempus. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus nullam. Maecenas volutpat blandit aliquam etiam erat velit scelerisque in. Sagittis nisl rhoncus mattis rhoncus urna neque viverra justo. Massa tempor nec feugiat nisl pretium. Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum. Enim lobortis scelerisque fermentum dui faucibus in ornare. Faucibus ornare suspendisse sed nisi lacus. Morbi tristique senectus et netus et malesuada fames. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Dictum non consectetur a erat nam at. Leo urna molestie at elementum eu facilisis sed odio morbi. Quam id leo in vitae turpis massa. Neque egestas congue quisque egestas diam in arcu. Varius morbi enim nunc faucibus a pellentesque sit. Aliquet enim tortor at auctor urna. | |
55 | ||
56 | Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Luctus accumsan tortor posuere ac. Eu ultrices vitae auctor eu augue ut lectus arcu bibendum. Pretium nibh ipsum consequat nisl vel pretium lectus. Aliquam etiam erat velit scelerisque in dictum. Sem et tortor consequat id porta nibh venenatis cras sed. A scelerisque purus semper eget duis at tellus at urna. At auctor urna nunc id. Ornare quam viverra orci sagittis eu volutpat odio. Nisl purus in mollis nunc sed id semper. Ornare suspendisse sed nisi lacus sed. Consectetur lorem donec massa sapien faucibus et. Ipsum dolor sit amet consectetur adipiscing elit ut. Porta nibh venenatis cras sed. Dignissim diam quis enim lobortis scelerisque. Quam nulla porttitor massa id. Tellus molestie nunc non blandit massa. | |
57 | ||
58 | Malesuada fames ac turpis egestas. Suscipit tellus mauris a diam maecenas. Turpis in eu mi bibendum neque egestas. Venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Quis imperdiet massa tincidunt nunc pulvinar sapien et. Urna duis convallis convallis tellus id. Velit egestas dui id ornare arcu odio. Consectetur purus ut faucibus pulvinar elementum integer enim neque. Aenean sed adipiscing diam donec adipiscing tristique. Tortor aliquam nulla facilisi cras fermentum odio eu. Diam in arcu cursus euismod quis viverra nibh cras. | |
59 | ||
60 | Id ornare arcu odio ut sem. Arcu dictum varius duis at consectetur lorem donec massa sapien. Proin libero nunc consequat interdum varius sit. Ut eu sem integer vitae justo. Vitae elementum curabitur vitae nunc. Diam quam nulla porttitor massa. Lectus mauris ultrices eros in cursus turpis massa tincidunt dui. Natoque penatibus et magnis dis parturient montes. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Libero nunc consequat interdum varius sit. Rhoncus dolor purus non enim praesent. Pellentesque sit amet porttitor eget. Nibh tortor id aliquet lectus proin nibh. Fermentum iaculis eu non diam phasellus vestibulum lorem sed. | |
61 | ||
62 | Eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus. Habitant morbi tristique senectus et netus et malesuada fames ac. Urna condimentum mattis pellentesque id. Lorem sed risus ultricies tristique nulla aliquet enim tortor at. Ipsum dolor sit amet consectetur adipiscing elit. Convallis a cras semper auctor neque vitae tempus quam. A diam sollicitudin tempor id eu nisl nunc mi ipsum. Maecenas sed enim ut sem viverra aliquet eget. Massa enim nec dui nunc mattis enim. Nam aliquam sem et tortor consequat. Adipiscing commodo elit at imperdiet dui accumsan sit amet nulla. Nullam eget felis eget nunc lobortis. Mauris a diam maecenas sed enim ut sem viverra. Ornare massa eget egestas purus. In hac habitasse platea dictumst. Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Nisl nunc mi ipsum faucibus. At varius vel pharetra vel. Mauris ultrices eros in cursus turpis massa tincidunt.`) | |
63 | }) | |
64 | } |
15 | 15 | |
16 | 16 | import ( |
17 | 17 | "bytes" |
18 | "flag" | |
18 | 19 | "io/ioutil" |
19 | 20 | "testing" |
20 | 21 | |
23 | 24 | ) |
24 | 25 | |
25 | 26 | const ( |
26 | _CONF_DATA = ` | |
27 | confData = ` | |
27 | 28 | ; Package name |
28 | 29 | NAME = ini |
29 | 30 | ; Package version |
41 | 42 | Coding addict. |
42 | 43 | Good man. |
43 | 44 | """ # Succeeding comment` |
44 | _MINIMAL_CONF = "testdata/minimal.ini" | |
45 | _FULL_CONF = "testdata/full.ini" | |
46 | _NOT_FOUND_CONF = "testdata/404.ini" | |
45 | minimalConf = "testdata/minimal.ini" | |
46 | fullConf = "testdata/full.ini" | |
47 | notFoundConf = "testdata/404.ini" | |
47 | 48 | ) |
49 | ||
50 | var update = flag.Bool("update", false, "Update .golden files") | |
48 | 51 | |
49 | 52 | func TestLoad(t *testing.T) { |
50 | 53 | Convey("Load from good data sources", t, func() { |
51 | f, err := ini.Load([]byte(` | |
52 | NAME = ini | |
53 | VERSION = v1 | |
54 | IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s`), | |
54 | f, err := ini.Load( | |
55 | 55 | "testdata/minimal.ini", |
56 | ioutil.NopCloser(bytes.NewReader([]byte(` | |
57 | [author] | |
58 | NAME = Unknwon | |
59 | `))), | |
56 | []byte("NAME = ini\nIMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s"), | |
57 | bytes.NewReader([]byte(`VERSION = v1`)), | |
58 | ioutil.NopCloser(bytes.NewReader([]byte("[author]\nNAME = Unknwon"))), | |
60 | 59 | ) |
61 | 60 | So(err, ShouldBeNil) |
62 | 61 | So(f, ShouldNotBeNil) |
63 | 62 | |
64 | // Vaildate values make sure all sources are loaded correctly | |
63 | // Validate values make sure all sources are loaded correctly | |
65 | 64 | sec := f.Section("") |
66 | 65 | So(sec.Key("NAME").String(), ShouldEqual, "ini") |
67 | 66 | So(sec.Key("VERSION").String(), ShouldEqual, "v1") |
74 | 73 | |
75 | 74 | Convey("Load from bad data sources", t, func() { |
76 | 75 | Convey("Invalid input", func() { |
77 | _, err := ini.Load(_NOT_FOUND_CONF) | |
76 | _, err := ini.Load(notFoundConf) | |
78 | 77 | So(err, ShouldNotBeNil) |
79 | 78 | }) |
80 | 79 | |
83 | 82 | So(err, ShouldNotBeNil) |
84 | 83 | }) |
85 | 84 | }) |
85 | ||
86 | Convey("Can't properly parse INI files containing `#` or `;` in value", t, func() { | |
87 | f, err := ini.Load([]byte(` | |
88 | [author] | |
89 | NAME = U#n#k#n#w#o#n | |
90 | GITHUB = U;n;k;n;w;o;n | |
91 | `)) | |
92 | So(err, ShouldBeNil) | |
93 | So(f, ShouldNotBeNil) | |
94 | ||
95 | sec := f.Section("author") | |
96 | nameValue := sec.Key("NAME").String() | |
97 | githubValue := sec.Key("GITHUB").String() | |
98 | So(nameValue, ShouldEqual, "U") | |
99 | So(githubValue, ShouldEqual, "U") | |
100 | }) | |
101 | ||
102 | Convey("Can't parse small python-compatible INI files", t, func() { | |
103 | f, err := ini.Load([]byte(` | |
104 | [long] | |
105 | long_rsa_private_key = -----BEGIN RSA PRIVATE KEY----- | |
106 | foo | |
107 | bar | |
108 | foobar | |
109 | barfoo | |
110 | -----END RSA PRIVATE KEY----- | |
111 | `)) | |
112 | So(err, ShouldNotBeNil) | |
113 | So(f, ShouldBeNil) | |
114 | So(err.Error(), ShouldEqual, "key-value delimiter not found: foo\n") | |
115 | }) | |
116 | ||
117 | Convey("Can't parse big python-compatible INI files", t, func() { | |
118 | f, err := ini.Load([]byte(` | |
119 | [long] | |
120 | long_rsa_private_key = -----BEGIN RSA PRIVATE KEY----- | |
121 | 1foo | |
122 | 2bar | |
123 | 3foobar | |
124 | 4barfoo | |
125 | 5foo | |
126 | 6bar | |
127 | 7foobar | |
128 | 8barfoo | |
129 | 9foo | |
130 | 10bar | |
131 | 11foobar | |
132 | 12barfoo | |
133 | 13foo | |
134 | 14bar | |
135 | 15foobar | |
136 | 16barfoo | |
137 | 17foo | |
138 | 18bar | |
139 | 19foobar | |
140 | 20barfoo | |
141 | 21foo | |
142 | 22bar | |
143 | 23foobar | |
144 | 24barfoo | |
145 | 25foo | |
146 | 26bar | |
147 | 27foobar | |
148 | 28barfoo | |
149 | 29foo | |
150 | 30bar | |
151 | 31foobar | |
152 | 32barfoo | |
153 | 33foo | |
154 | 34bar | |
155 | 35foobar | |
156 | 36barfoo | |
157 | 37foo | |
158 | 38bar | |
159 | 39foobar | |
160 | 40barfoo | |
161 | 41foo | |
162 | 42bar | |
163 | 43foobar | |
164 | 44barfoo | |
165 | 45foo | |
166 | 46bar | |
167 | 47foobar | |
168 | 48barfoo | |
169 | 49foo | |
170 | 50bar | |
171 | 51foobar | |
172 | 52barfoo | |
173 | 53foo | |
174 | 54bar | |
175 | 55foobar | |
176 | 56barfoo | |
177 | 57foo | |
178 | 58bar | |
179 | 59foobar | |
180 | 60barfoo | |
181 | 61foo | |
182 | 62bar | |
183 | 63foobar | |
184 | 64barfoo | |
185 | 65foo | |
186 | 66bar | |
187 | 67foobar | |
188 | 68barfoo | |
189 | 69foo | |
190 | 70bar | |
191 | 71foobar | |
192 | 72barfoo | |
193 | 73foo | |
194 | 74bar | |
195 | 75foobar | |
196 | 76barfoo | |
197 | 77foo | |
198 | 78bar | |
199 | 79foobar | |
200 | 80barfoo | |
201 | 81foo | |
202 | 82bar | |
203 | 83foobar | |
204 | 84barfoo | |
205 | 85foo | |
206 | 86bar | |
207 | 87foobar | |
208 | 88barfoo | |
209 | 89foo | |
210 | 90bar | |
211 | 91foobar | |
212 | 92barfoo | |
213 | 93foo | |
214 | 94bar | |
215 | 95foobar | |
216 | 96barfoo | |
217 | -----END RSA PRIVATE KEY----- | |
218 | `)) | |
219 | So(err, ShouldNotBeNil) | |
220 | So(f, ShouldBeNil) | |
221 | So(err.Error(), ShouldEqual, "key-value delimiter not found: 1foo\n") | |
222 | }) | |
223 | } | |
224 | ||
225 | func TestLooseLoad(t *testing.T) { | |
226 | Convey("Load from data sources with option `Loose` true", t, func() { | |
227 | f, err := ini.LoadSources(ini.LoadOptions{Loose: true}, notFoundConf, minimalConf) | |
228 | So(err, ShouldBeNil) | |
229 | So(f, ShouldNotBeNil) | |
230 | ||
231 | Convey("Inverse case", func() { | |
232 | _, err = ini.Load(notFoundConf) | |
233 | So(err, ShouldNotBeNil) | |
234 | }) | |
235 | }) | |
236 | } | |
237 | ||
238 | func TestInsensitiveLoad(t *testing.T) { | |
239 | Convey("Insensitive to section and key names", t, func() { | |
240 | f, err := ini.InsensitiveLoad(minimalConf) | |
241 | So(err, ShouldBeNil) | |
242 | So(f, ShouldNotBeNil) | |
243 | ||
244 | So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io") | |
245 | ||
246 | Convey("Write out", func() { | |
247 | var buf bytes.Buffer | |
248 | _, err := f.WriteTo(&buf) | |
249 | So(err, ShouldBeNil) | |
250 | So(buf.String(), ShouldEqual, `[author] | |
251 | e-mail = u@gogs.io | |
252 | ||
253 | `) | |
254 | }) | |
255 | ||
256 | Convey("Inverse case", func() { | |
257 | f, err := ini.Load(minimalConf) | |
258 | So(err, ShouldBeNil) | |
259 | So(f, ShouldNotBeNil) | |
260 | ||
261 | So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty) | |
262 | }) | |
263 | }) | |
264 | ||
265 | // Ref: https://github.com/go-ini/ini/issues/198 | |
266 | Convey("Insensitive load with default section", t, func() { | |
267 | f, err := ini.InsensitiveLoad([]byte(` | |
268 | user = unknwon | |
269 | [profile] | |
270 | email = unknwon@local | |
271 | `)) | |
272 | So(err, ShouldBeNil) | |
273 | So(f, ShouldNotBeNil) | |
274 | ||
275 | So(f.Section(ini.DefaultSection).Key("user").String(), ShouldEqual, "unknwon") | |
276 | }) | |
86 | 277 | } |
87 | 278 | |
88 | 279 | func TestLoadSources(t *testing.T) { |
89 | 280 | Convey("Load from data sources with options", t, func() { |
90 | Convey("Ignore nonexistent files", func() { | |
91 | f, err := ini.LooseLoad(_NOT_FOUND_CONF, _MINIMAL_CONF) | |
92 | So(err, ShouldBeNil) | |
93 | So(f, ShouldNotBeNil) | |
94 | ||
95 | Convey("Inverse case", func() { | |
96 | _, err = ini.Load(_NOT_FOUND_CONF) | |
97 | So(err, ShouldNotBeNil) | |
98 | }) | |
99 | }) | |
100 | ||
101 | Convey("Insensitive to section and key names", func() { | |
102 | f, err := ini.InsensitiveLoad(_MINIMAL_CONF) | |
103 | So(err, ShouldBeNil) | |
104 | So(f, ShouldNotBeNil) | |
105 | ||
106 | So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io") | |
107 | ||
108 | Convey("Write out", func() { | |
109 | var buf bytes.Buffer | |
110 | _, err := f.WriteTo(&buf) | |
111 | So(err, ShouldBeNil) | |
112 | So(buf.String(), ShouldEqual, `[author] | |
281 | Convey("with true `AllowPythonMultilineValues`", func() { | |
282 | Convey("Ignore nonexistent files", func() { | |
283 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true, Loose: true}, notFoundConf, minimalConf) | |
284 | So(err, ShouldBeNil) | |
285 | So(f, ShouldNotBeNil) | |
286 | ||
287 | Convey("Inverse case", func() { | |
288 | _, err = ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, notFoundConf) | |
289 | So(err, ShouldNotBeNil) | |
290 | }) | |
291 | }) | |
292 | ||
293 | Convey("Insensitive to section and key names", func() { | |
294 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true, Insensitive: true}, minimalConf) | |
295 | So(err, ShouldBeNil) | |
296 | So(f, ShouldNotBeNil) | |
297 | ||
298 | So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io") | |
299 | ||
300 | Convey("Write out", func() { | |
301 | var buf bytes.Buffer | |
302 | _, err := f.WriteTo(&buf) | |
303 | So(err, ShouldBeNil) | |
304 | So(buf.String(), ShouldEqual, `[author] | |
113 | 305 | e-mail = u@gogs.io |
114 | 306 | |
115 | 307 | `) |
116 | }) | |
117 | ||
118 | Convey("Inverse case", func() { | |
119 | f, err := ini.Load(_MINIMAL_CONF) | |
120 | So(err, ShouldBeNil) | |
121 | So(f, ShouldNotBeNil) | |
122 | ||
123 | So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty) | |
124 | }) | |
125 | }) | |
126 | ||
127 | Convey("Ignore continuation lines", func() { | |
128 | f, err := ini.LoadSources(ini.LoadOptions{ | |
129 | IgnoreContinuation: true, | |
130 | }, []byte(` | |
308 | }) | |
309 | ||
310 | Convey("Inverse case", func() { | |
311 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, minimalConf) | |
312 | So(err, ShouldBeNil) | |
313 | So(f, ShouldNotBeNil) | |
314 | ||
315 | So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty) | |
316 | }) | |
317 | }) | |
318 | ||
319 | Convey("Insensitive to sections and sensitive to key names", func() { | |
320 | f, err := ini.LoadSources(ini.LoadOptions{InsensitiveSections: true}, minimalConf) | |
321 | So(err, ShouldBeNil) | |
322 | So(f, ShouldNotBeNil) | |
323 | ||
324 | So(f.Section("Author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io") | |
325 | ||
326 | Convey("Write out", func() { | |
327 | var buf bytes.Buffer | |
328 | _, err := f.WriteTo(&buf) | |
329 | So(err, ShouldBeNil) | |
330 | So(buf.String(), ShouldEqual, `[author] | |
331 | E-MAIL = u@gogs.io | |
332 | ||
333 | `) | |
334 | }) | |
335 | ||
336 | Convey("Inverse case", func() { | |
337 | f, err := ini.LoadSources(ini.LoadOptions{}, minimalConf) | |
338 | So(err, ShouldBeNil) | |
339 | So(f, ShouldNotBeNil) | |
340 | ||
341 | So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty) | |
342 | }) | |
343 | }) | |
344 | ||
345 | Convey("Sensitive to sections and insensitive to key names", func() { | |
346 | f, err := ini.LoadSources(ini.LoadOptions{InsensitiveKeys: true}, minimalConf) | |
347 | So(err, ShouldBeNil) | |
348 | So(f, ShouldNotBeNil) | |
349 | ||
350 | So(f.Section("author").Key("e-mail").String(), ShouldEqual, "u@gogs.io") | |
351 | ||
352 | Convey("Write out", func() { | |
353 | var buf bytes.Buffer | |
354 | _, err := f.WriteTo(&buf) | |
355 | So(err, ShouldBeNil) | |
356 | So(buf.String(), ShouldEqual, `[author] | |
357 | e-mail = u@gogs.io | |
358 | ||
359 | `) | |
360 | }) | |
361 | ||
362 | Convey("Inverse case", func() { | |
363 | f, err := ini.LoadSources(ini.LoadOptions{}, minimalConf) | |
364 | So(err, ShouldBeNil) | |
365 | So(f, ShouldNotBeNil) | |
366 | ||
367 | So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty) | |
368 | }) | |
369 | }) | |
370 | ||
371 | Convey("Ignore continuation lines", func() { | |
372 | f, err := ini.LoadSources(ini.LoadOptions{ | |
373 | AllowPythonMultilineValues: true, | |
374 | IgnoreContinuation: true, | |
375 | }, []byte(` | |
131 | 376 | key1=a\b\ |
132 | 377 | key2=c\d\ |
133 | 378 | key3=value`)) |
134 | So(err, ShouldBeNil) | |
135 | So(f, ShouldNotBeNil) | |
136 | ||
137 | So(f.Section("").Key("key1").String(), ShouldEqual, `a\b\`) | |
138 | So(f.Section("").Key("key2").String(), ShouldEqual, `c\d\`) | |
139 | So(f.Section("").Key("key3").String(), ShouldEqual, "value") | |
140 | ||
141 | Convey("Inverse case", func() { | |
142 | f, err := ini.Load([]byte(` | |
379 | So(err, ShouldBeNil) | |
380 | So(f, ShouldNotBeNil) | |
381 | ||
382 | So(f.Section("").Key("key1").String(), ShouldEqual, `a\b\`) | |
383 | So(f.Section("").Key("key2").String(), ShouldEqual, `c\d\`) | |
384 | So(f.Section("").Key("key3").String(), ShouldEqual, "value") | |
385 | ||
386 | Convey("Inverse case", func() { | |
387 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(` | |
143 | 388 | key1=a\b\ |
144 | 389 | key2=c\d\`)) |
145 | So(err, ShouldBeNil) | |
146 | So(f, ShouldNotBeNil) | |
147 | ||
148 | So(f.Section("").Key("key1").String(), ShouldEqual, `a\bkey2=c\d`) | |
149 | }) | |
150 | }) | |
151 | ||
152 | Convey("Ignore inline comments", func() { | |
153 | f, err := ini.LoadSources(ini.LoadOptions{ | |
154 | IgnoreInlineComment: true, | |
155 | }, []byte(` | |
390 | So(err, ShouldBeNil) | |
391 | So(f, ShouldNotBeNil) | |
392 | ||
393 | So(f.Section("").Key("key1").String(), ShouldEqual, `a\bkey2=c\d`) | |
394 | }) | |
395 | }) | |
396 | ||
397 | Convey("Ignore inline comments", func() { | |
398 | f, err := ini.LoadSources(ini.LoadOptions{ | |
399 | AllowPythonMultilineValues: true, | |
400 | IgnoreInlineComment: true, | |
401 | }, []byte(` | |
402 | key1=value ;comment | |
403 | key2=value2 #comment2 | |
404 | key3=val#ue #comment3`)) | |
405 | So(err, ShouldBeNil) | |
406 | So(f, ShouldNotBeNil) | |
407 | ||
408 | So(f.Section("").Key("key1").String(), ShouldEqual, `value ;comment`) | |
409 | So(f.Section("").Key("key2").String(), ShouldEqual, `value2 #comment2`) | |
410 | So(f.Section("").Key("key3").String(), ShouldEqual, `val#ue #comment3`) | |
411 | ||
412 | Convey("Inverse case", func() { | |
413 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(` | |
156 | 414 | key1=value ;comment |
157 | 415 | key2=value2 #comment2`)) |
158 | So(err, ShouldBeNil) | |
159 | So(f, ShouldNotBeNil) | |
160 | ||
161 | So(f.Section("").Key("key1").String(), ShouldEqual, `value ;comment`) | |
162 | So(f.Section("").Key("key2").String(), ShouldEqual, `value2 #comment2`) | |
163 | ||
164 | Convey("Inverse case", func() { | |
165 | f, err := ini.Load([]byte(` | |
166 | key1=value ;comment | |
167 | key2=value2 #comment2`)) | |
168 | So(err, ShouldBeNil) | |
169 | So(f, ShouldNotBeNil) | |
170 | ||
171 | So(f.Section("").Key("key1").String(), ShouldEqual, `value`) | |
172 | So(f.Section("").Key("key1").Comment, ShouldEqual, `;comment`) | |
173 | So(f.Section("").Key("key2").String(), ShouldEqual, `value2`) | |
174 | So(f.Section("").Key("key2").Comment, ShouldEqual, `#comment2`) | |
175 | }) | |
176 | }) | |
177 | ||
178 | Convey("Allow boolean type keys", func() { | |
179 | f, err := ini.LoadSources(ini.LoadOptions{ | |
180 | AllowBooleanKeys: true, | |
181 | }, []byte(` | |
416 | So(err, ShouldBeNil) | |
417 | So(f, ShouldNotBeNil) | |
418 | ||
419 | So(f.Section("").Key("key1").String(), ShouldEqual, `value`) | |
420 | So(f.Section("").Key("key1").Comment, ShouldEqual, `;comment`) | |
421 | So(f.Section("").Key("key2").String(), ShouldEqual, `value2`) | |
422 | So(f.Section("").Key("key2").Comment, ShouldEqual, `#comment2`) | |
423 | }) | |
424 | }) | |
425 | ||
426 | Convey("Skip unrecognizable lines", func() { | |
427 | f, err := ini.LoadSources(ini.LoadOptions{ | |
428 | SkipUnrecognizableLines: true, | |
429 | }, []byte(` | |
430 | GenerationDepth: 13 | |
431 | ||
432 | BiomeRarityScale: 100 | |
433 | ||
434 | ################ | |
435 | # Biome Groups # | |
436 | ################ | |
437 | ||
438 | BiomeGroup(NormalBiomes, 3, 99, RoofedForestEnchanted, ForestSakura, FloatingJungle | |
439 | BiomeGroup(IceBiomes, 4, 85, Ice Plains) | |
440 | `)) | |
441 | So(err, ShouldBeNil) | |
442 | So(f, ShouldNotBeNil) | |
443 | ||
444 | So(f.Section("").Key("GenerationDepth").String(), ShouldEqual, "13") | |
445 | So(f.Section("").Key("BiomeRarityScale").String(), ShouldEqual, "100") | |
446 | So(f.Section("").HasKey("BiomeGroup"), ShouldBeFalse) | |
447 | }) | |
448 | ||
449 | Convey("Allow boolean type keys", func() { | |
450 | f, err := ini.LoadSources(ini.LoadOptions{ | |
451 | AllowPythonMultilineValues: true, | |
452 | AllowBooleanKeys: true, | |
453 | }, []byte(` | |
182 | 454 | key1=hello |
183 | 455 | #key2 |
184 | 456 | key3`)) |
185 | So(err, ShouldBeNil) | |
186 | So(f, ShouldNotBeNil) | |
187 | ||
188 | So(f.Section("").KeyStrings(), ShouldResemble, []string{"key1", "key3"}) | |
189 | So(f.Section("").Key("key3").MustBool(false), ShouldBeTrue) | |
190 | ||
191 | Convey("Write out", func() { | |
192 | var buf bytes.Buffer | |
193 | _, err := f.WriteTo(&buf) | |
194 | So(err, ShouldBeNil) | |
195 | So(buf.String(), ShouldEqual, `key1 = hello | |
457 | So(err, ShouldBeNil) | |
458 | So(f, ShouldNotBeNil) | |
459 | ||
460 | So(f.Section("").KeyStrings(), ShouldResemble, []string{"key1", "key3"}) | |
461 | So(f.Section("").Key("key3").MustBool(false), ShouldBeTrue) | |
462 | ||
463 | Convey("Write out", func() { | |
464 | var buf bytes.Buffer | |
465 | _, err := f.WriteTo(&buf) | |
466 | So(err, ShouldBeNil) | |
467 | So(buf.String(), ShouldEqual, `key1 = hello | |
196 | 468 | # key2 |
197 | 469 | key3 |
198 | 470 | `) |
199 | }) | |
200 | ||
201 | Convey("Inverse case", func() { | |
202 | _, err := ini.Load([]byte(` | |
471 | }) | |
472 | ||
473 | Convey("Inverse case", func() { | |
474 | _, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(` | |
203 | 475 | key1=hello |
204 | 476 | #key2 |
205 | 477 | key3`)) |
206 | So(err, ShouldNotBeNil) | |
207 | }) | |
208 | }) | |
209 | ||
210 | Convey("Allow shadow keys", func() { | |
211 | f, err := ini.ShadowLoad([]byte(` | |
478 | So(err, ShouldNotBeNil) | |
479 | }) | |
480 | }) | |
481 | ||
482 | Convey("Allow shadow keys", func() { | |
483 | f, err := ini.LoadSources(ini.LoadOptions{AllowShadows: true, AllowPythonMultilineValues: true}, []byte(` | |
212 | 484 | [remote "origin"] |
213 | 485 | url = https://github.com/Antergone/test1.git |
214 | 486 | url = https://github.com/Antergone/test2.git |
215 | 487 | fetch = +refs/heads/*:refs/remotes/origin/*`)) |
216 | So(err, ShouldBeNil) | |
217 | So(f, ShouldNotBeNil) | |
218 | ||
219 | So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test1.git") | |
220 | So(f.Section(`remote "origin"`).Key("url").ValueWithShadows(), ShouldResemble, []string{ | |
221 | "https://github.com/Antergone/test1.git", | |
222 | "https://github.com/Antergone/test2.git", | |
223 | }) | |
224 | So(f.Section(`remote "origin"`).Key("fetch").String(), ShouldEqual, "+refs/heads/*:refs/remotes/origin/*") | |
225 | ||
226 | Convey("Write out", func() { | |
227 | var buf bytes.Buffer | |
228 | _, err := f.WriteTo(&buf) | |
229 | So(err, ShouldBeNil) | |
230 | So(buf.String(), ShouldEqual, `[remote "origin"] | |
488 | So(err, ShouldBeNil) | |
489 | So(f, ShouldNotBeNil) | |
490 | ||
491 | So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test1.git") | |
492 | So(f.Section(`remote "origin"`).Key("url").ValueWithShadows(), ShouldResemble, []string{ | |
493 | "https://github.com/Antergone/test1.git", | |
494 | "https://github.com/Antergone/test2.git", | |
495 | }) | |
496 | So(f.Section(`remote "origin"`).Key("fetch").String(), ShouldEqual, "+refs/heads/*:refs/remotes/origin/*") | |
497 | ||
498 | Convey("Write out", func() { | |
499 | var buf bytes.Buffer | |
500 | _, err := f.WriteTo(&buf) | |
501 | So(err, ShouldBeNil) | |
502 | So(buf.String(), ShouldEqual, `[remote "origin"] | |
231 | 503 | url = https://github.com/Antergone/test1.git |
232 | 504 | url = https://github.com/Antergone/test2.git |
233 | 505 | fetch = +refs/heads/*:refs/remotes/origin/* |
234 | 506 | |
235 | 507 | `) |
236 | }) | |
237 | ||
238 | Convey("Inverse case", func() { | |
239 | f, err := ini.Load([]byte(` | |
508 | }) | |
509 | ||
510 | Convey("Inverse case", func() { | |
511 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(` | |
240 | 512 | [remote "origin"] |
241 | 513 | url = https://github.com/Antergone/test1.git |
242 | 514 | url = https://github.com/Antergone/test2.git`)) |
243 | So(err, ShouldBeNil) | |
244 | So(f, ShouldNotBeNil) | |
245 | ||
246 | So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test2.git") | |
247 | }) | |
248 | }) | |
249 | ||
250 | Convey("Unescape double quotes inside value", func() { | |
251 | f, err := ini.LoadSources(ini.LoadOptions{ | |
252 | UnescapeValueDoubleQuotes: true, | |
253 | }, []byte(` | |
515 | So(err, ShouldBeNil) | |
516 | So(f, ShouldNotBeNil) | |
517 | ||
518 | So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test2.git") | |
519 | }) | |
520 | }) | |
521 | ||
522 | Convey("Unescape double quotes inside value", func() { | |
523 | f, err := ini.LoadSources(ini.LoadOptions{ | |
524 | AllowPythonMultilineValues: true, | |
525 | UnescapeValueDoubleQuotes: true, | |
526 | }, []byte(` | |
254 | 527 | create_repo="创建了仓库 <a href=\"%s\">%s</a>"`)) |
255 | So(err, ShouldBeNil) | |
256 | So(f, ShouldNotBeNil) | |
257 | ||
258 | So(f.Section("").Key("create_repo").String(), ShouldEqual, `创建了仓库 <a href="%s">%s</a>`) | |
259 | ||
260 | Convey("Inverse case", func() { | |
261 | f, err := ini.Load([]byte(` | |
528 | So(err, ShouldBeNil) | |
529 | So(f, ShouldNotBeNil) | |
530 | ||
531 | So(f.Section("").Key("create_repo").String(), ShouldEqual, `创建了仓库 <a href="%s">%s</a>`) | |
532 | ||
533 | Convey("Inverse case", func() { | |
534 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(` | |
262 | 535 | create_repo="创建了仓库 <a href=\"%s\">%s</a>"`)) |
263 | So(err, ShouldBeNil) | |
264 | So(f, ShouldNotBeNil) | |
265 | ||
266 | So(f.Section("").Key("create_repo").String(), ShouldEqual, `"创建了仓库 <a href=\"%s\">%s</a>"`) | |
267 | }) | |
268 | }) | |
269 | ||
270 | Convey("Unescape comment symbols inside value", func() { | |
271 | f, err := ini.LoadSources(ini.LoadOptions{ | |
272 | IgnoreInlineComment: true, | |
273 | UnescapeValueCommentSymbols: true, | |
274 | }, []byte(` | |
536 | So(err, ShouldBeNil) | |
537 | So(f, ShouldNotBeNil) | |
538 | ||
539 | So(f.Section("").Key("create_repo").String(), ShouldEqual, `"创建了仓库 <a href=\"%s\">%s</a>"`) | |
540 | }) | |
541 | }) | |
542 | ||
543 | Convey("Unescape comment symbols inside value", func() { | |
544 | f, err := ini.LoadSources(ini.LoadOptions{ | |
545 | AllowPythonMultilineValues: true, | |
546 | IgnoreInlineComment: true, | |
547 | UnescapeValueCommentSymbols: true, | |
548 | }, []byte(` | |
275 | 549 | key = test value <span style="color: %s\; background: %s">more text</span> |
276 | 550 | `)) |
277 | So(err, ShouldBeNil) | |
278 | So(f, ShouldNotBeNil) | |
279 | ||
280 | So(f.Section("").Key("key").String(), ShouldEqual, `test value <span style="color: %s; background: %s">more text</span>`) | |
281 | }) | |
282 | ||
283 | Convey("Allow unparseable sections", func() { | |
284 | f, err := ini.LoadSources(ini.LoadOptions{ | |
285 | Insensitive: true, | |
286 | UnparseableSections: []string{"core_lesson", "comments"}, | |
287 | }, []byte(` | |
551 | So(err, ShouldBeNil) | |
552 | So(f, ShouldNotBeNil) | |
553 | ||
554 | So(f.Section("").Key("key").String(), ShouldEqual, `test value <span style="color: %s; background: %s">more text</span>`) | |
555 | }) | |
556 | ||
557 | Convey("Can parse small python-compatible INI files", func() { | |
558 | f, err := ini.LoadSources(ini.LoadOptions{ | |
559 | AllowPythonMultilineValues: true, | |
560 | Insensitive: true, | |
561 | UnparseableSections: []string{"core_lesson", "comments"}, | |
562 | }, []byte(` | |
563 | [long] | |
564 | long_rsa_private_key = -----BEGIN RSA PRIVATE KEY----- | |
565 | foo | |
566 | bar | |
567 | foobar | |
568 | barfoo | |
569 | -----END RSA PRIVATE KEY----- | |
570 | multiline_list = | |
571 | first | |
572 | second | |
573 | third | |
574 | `)) | |
575 | So(err, ShouldBeNil) | |
576 | So(f, ShouldNotBeNil) | |
577 | ||
578 | So(f.Section("long").Key("long_rsa_private_key").String(), ShouldEqual, "-----BEGIN RSA PRIVATE KEY-----\nfoo\nbar\nfoobar\nbarfoo\n-----END RSA PRIVATE KEY-----") | |
579 | So(f.Section("long").Key("multiline_list").String(), ShouldEqual, "\nfirst\nsecond\nthird") | |
580 | }) | |
581 | ||
582 | Convey("Can parse big python-compatible INI files", func() { | |
583 | f, err := ini.LoadSources(ini.LoadOptions{ | |
584 | AllowPythonMultilineValues: true, | |
585 | Insensitive: true, | |
586 | UnparseableSections: []string{"core_lesson", "comments"}, | |
587 | }, []byte(` | |
588 | [long] | |
589 | long_rsa_private_key = -----BEGIN RSA PRIVATE KEY----- | |
590 | 1foo | |
591 | 2bar | |
592 | 3foobar | |
593 | 4barfoo | |
594 | 5foo | |
595 | 6bar | |
596 | 7foobar | |
597 | 8barfoo | |
598 | 9foo | |
599 | 10bar | |
600 | 11foobar | |
601 | 12barfoo | |
602 | 13foo | |
603 | 14bar | |
604 | 15foobar | |
605 | 16barfoo | |
606 | 17foo | |
607 | 18bar | |
608 | 19foobar | |
609 | 20barfoo | |
610 | 21foo | |
611 | 22bar | |
612 | 23foobar | |
613 | 24barfoo | |
614 | 25foo | |
615 | 26bar | |
616 | 27foobar | |
617 | 28barfoo | |
618 | 29foo | |
619 | 30bar | |
620 | 31foobar | |
621 | 32barfoo | |
622 | 33foo | |
623 | 34bar | |
624 | 35foobar | |
625 | 36barfoo | |
626 | 37foo | |
627 | 38bar | |
628 | 39foobar | |
629 | 40barfoo | |
630 | 41foo | |
631 | 42bar | |
632 | 43foobar | |
633 | 44barfoo | |
634 | 45foo | |
635 | 46bar | |
636 | 47foobar | |
637 | 48barfoo | |
638 | 49foo | |
639 | 50bar | |
640 | 51foobar | |
641 | 52barfoo | |
642 | 53foo | |
643 | 54bar | |
644 | 55foobar | |
645 | 56barfoo | |
646 | 57foo | |
647 | 58bar | |
648 | 59foobar | |
649 | 60barfoo | |
650 | 61foo | |
651 | 62bar | |
652 | 63foobar | |
653 | 64barfoo | |
654 | 65foo | |
655 | 66bar | |
656 | 67foobar | |
657 | 68barfoo | |
658 | 69foo | |
659 | 70bar | |
660 | 71foobar | |
661 | 72barfoo | |
662 | 73foo | |
663 | 74bar | |
664 | 75foobar | |
665 | 76barfoo | |
666 | 77foo | |
667 | 78bar | |
668 | 79foobar | |
669 | 80barfoo | |
670 | 81foo | |
671 | 82bar | |
672 | 83foobar | |
673 | 84barfoo | |
674 | 85foo | |
675 | 86bar | |
676 | 87foobar | |
677 | 88barfoo | |
678 | 89foo | |
679 | 90bar | |
680 | 91foobar | |
681 | 92barfoo | |
682 | 93foo | |
683 | 94bar | |
684 | 95foobar | |
685 | 96barfoo | |
686 | -----END RSA PRIVATE KEY----- | |
687 | `)) | |
688 | So(err, ShouldBeNil) | |
689 | So(f, ShouldNotBeNil) | |
690 | ||
691 | So(f.Section("long").Key("long_rsa_private_key").String(), ShouldEqual, `-----BEGIN RSA PRIVATE KEY----- | |
692 | 1foo | |
693 | 2bar | |
694 | 3foobar | |
695 | 4barfoo | |
696 | 5foo | |
697 | 6bar | |
698 | 7foobar | |
699 | 8barfoo | |
700 | 9foo | |
701 | 10bar | |
702 | 11foobar | |
703 | 12barfoo | |
704 | 13foo | |
705 | 14bar | |
706 | 15foobar | |
707 | 16barfoo | |
708 | 17foo | |
709 | 18bar | |
710 | 19foobar | |
711 | 20barfoo | |
712 | 21foo | |
713 | 22bar | |
714 | 23foobar | |
715 | 24barfoo | |
716 | 25foo | |
717 | 26bar | |
718 | 27foobar | |
719 | 28barfoo | |
720 | 29foo | |
721 | 30bar | |
722 | 31foobar | |
723 | 32barfoo | |
724 | 33foo | |
725 | 34bar | |
726 | 35foobar | |
727 | 36barfoo | |
728 | 37foo | |
729 | 38bar | |
730 | 39foobar | |
731 | 40barfoo | |
732 | 41foo | |
733 | 42bar | |
734 | 43foobar | |
735 | 44barfoo | |
736 | 45foo | |
737 | 46bar | |
738 | 47foobar | |
739 | 48barfoo | |
740 | 49foo | |
741 | 50bar | |
742 | 51foobar | |
743 | 52barfoo | |
744 | 53foo | |
745 | 54bar | |
746 | 55foobar | |
747 | 56barfoo | |
748 | 57foo | |
749 | 58bar | |
750 | 59foobar | |
751 | 60barfoo | |
752 | 61foo | |
753 | 62bar | |
754 | 63foobar | |
755 | 64barfoo | |
756 | 65foo | |
757 | 66bar | |
758 | 67foobar | |
759 | 68barfoo | |
760 | 69foo | |
761 | 70bar | |
762 | 71foobar | |
763 | 72barfoo | |
764 | 73foo | |
765 | 74bar | |
766 | 75foobar | |
767 | 76barfoo | |
768 | 77foo | |
769 | 78bar | |
770 | 79foobar | |
771 | 80barfoo | |
772 | 81foo | |
773 | 82bar | |
774 | 83foobar | |
775 | 84barfoo | |
776 | 85foo | |
777 | 86bar | |
778 | 87foobar | |
779 | 88barfoo | |
780 | 89foo | |
781 | 90bar | |
782 | 91foobar | |
783 | 92barfoo | |
784 | 93foo | |
785 | 94bar | |
786 | 95foobar | |
787 | 96barfoo | |
788 | -----END RSA PRIVATE KEY-----`) | |
789 | }) | |
790 | ||
791 | Convey("Allow unparsable sections", func() { | |
792 | f, err := ini.LoadSources(ini.LoadOptions{ | |
793 | AllowPythonMultilineValues: true, | |
794 | Insensitive: true, | |
795 | UnparseableSections: []string{"core_lesson", "comments"}, | |
796 | }, []byte(` | |
288 | 797 | Lesson_Location = 87 |
289 | 798 | Lesson_Status = C |
290 | 799 | Score = 3 |
296 | 805 | |
297 | 806 | [COMMENTS] |
298 | 807 | <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)) |
299 | So(err, ShouldBeNil) | |
300 | So(f, ShouldNotBeNil) | |
301 | ||
302 | So(f.Section("").Key("score").String(), ShouldEqual, "3") | |
303 | So(f.Section("").Body(), ShouldBeEmpty) | |
304 | So(f.Section("core_lesson").Body(), ShouldEqual, `my lesson state data – 1111111111111111111000000000000000001110000 | |
808 | So(err, ShouldBeNil) | |
809 | So(f, ShouldNotBeNil) | |
810 | ||
811 | So(f.Section("").Key("score").String(), ShouldEqual, "3") | |
812 | So(f.Section("").Body(), ShouldBeEmpty) | |
813 | So(f.Section("core_lesson").Body(), ShouldEqual, `my lesson state data – 1111111111111111111000000000000000001110000 | |
305 | 814 | 111111111111111111100000000000111000000000 – end my lesson state data`) |
306 | So(f.Section("comments").Body(), ShouldEqual, `<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`) | |
307 | ||
308 | Convey("Write out", func() { | |
309 | var buf bytes.Buffer | |
310 | _, err := f.WriteTo(&buf) | |
311 | So(err, ShouldBeNil) | |
312 | So(buf.String(), ShouldEqual, `lesson_location = 87 | |
815 | So(f.Section("comments").Body(), ShouldEqual, `<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`) | |
816 | ||
817 | Convey("Write out", func() { | |
818 | var buf bytes.Buffer | |
819 | _, err := f.WriteTo(&buf) | |
820 | So(err, ShouldBeNil) | |
821 | So(buf.String(), ShouldEqual, `lesson_location = 87 | |
313 | 822 | lesson_status = C |
314 | 823 | score = 3 |
315 | 824 | time = 00:02:30 |
321 | 830 | [comments] |
322 | 831 | <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1> |
323 | 832 | `) |
324 | }) | |
325 | ||
326 | Convey("Inverse case", func() { | |
327 | _, err := ini.Load([]byte(` | |
833 | }) | |
834 | ||
835 | Convey("Inverse case", func() { | |
836 | _, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(` | |
328 | 837 | [CORE_LESSON] |
329 | 838 | my lesson state data – 1111111111111111111000000000000000001110000 |
330 | 839 | 111111111111111111100000000000111000000000 – end my lesson state data`)) |
840 | So(err, ShouldNotBeNil) | |
841 | }) | |
842 | }) | |
843 | ||
844 | Convey("And false `SpaceBeforeInlineComment`", func() { | |
845 | Convey("Can't parse INI files containing `#` or `;` in value", func() { | |
846 | f, err := ini.LoadSources( | |
847 | ini.LoadOptions{AllowPythonMultilineValues: false, SpaceBeforeInlineComment: false}, | |
848 | []byte(` | |
849 | [author] | |
850 | NAME = U#n#k#n#w#o#n | |
851 | GITHUB = U;n;k;n;w;o;n | |
852 | `)) | |
853 | So(err, ShouldBeNil) | |
854 | So(f, ShouldNotBeNil) | |
855 | sec := f.Section("author") | |
856 | nameValue := sec.Key("NAME").String() | |
857 | githubValue := sec.Key("GITHUB").String() | |
858 | So(nameValue, ShouldEqual, "U") | |
859 | So(githubValue, ShouldEqual, "U") | |
860 | }) | |
861 | }) | |
862 | ||
863 | Convey("And true `SpaceBeforeInlineComment`", func() { | |
864 | Convey("Can parse INI files containing `#` or `;` in value", func() { | |
865 | f, err := ini.LoadSources( | |
866 | ini.LoadOptions{AllowPythonMultilineValues: false, SpaceBeforeInlineComment: true}, | |
867 | []byte(` | |
868 | [author] | |
869 | NAME = U#n#k#n#w#o#n | |
870 | GITHUB = U;n;k;n;w;o;n | |
871 | `)) | |
872 | So(err, ShouldBeNil) | |
873 | So(f, ShouldNotBeNil) | |
874 | sec := f.Section("author") | |
875 | nameValue := sec.Key("NAME").String() | |
876 | githubValue := sec.Key("GITHUB").String() | |
877 | So(nameValue, ShouldEqual, "U#n#k#n#w#o#n") | |
878 | So(githubValue, ShouldEqual, "U;n;k;n;w;o;n") | |
879 | }) | |
880 | }) | |
881 | }) | |
882 | ||
883 | Convey("with false `AllowPythonMultilineValues`", func() { | |
884 | Convey("Ignore nonexistent files", func() { | |
885 | f, err := ini.LoadSources(ini.LoadOptions{ | |
886 | AllowPythonMultilineValues: false, | |
887 | Loose: true, | |
888 | }, notFoundConf, minimalConf) | |
889 | So(err, ShouldBeNil) | |
890 | So(f, ShouldNotBeNil) | |
891 | ||
892 | Convey("Inverse case", func() { | |
893 | _, err = ini.LoadSources(ini.LoadOptions{ | |
894 | AllowPythonMultilineValues: false, | |
895 | }, notFoundConf) | |
896 | So(err, ShouldNotBeNil) | |
897 | }) | |
898 | }) | |
899 | ||
900 | Convey("Insensitive to section and key names", func() { | |
901 | f, err := ini.LoadSources(ini.LoadOptions{ | |
902 | AllowPythonMultilineValues: false, | |
903 | Insensitive: true, | |
904 | }, minimalConf) | |
905 | So(err, ShouldBeNil) | |
906 | So(f, ShouldNotBeNil) | |
907 | ||
908 | So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io") | |
909 | ||
910 | Convey("Write out", func() { | |
911 | var buf bytes.Buffer | |
912 | _, err := f.WriteTo(&buf) | |
913 | So(err, ShouldBeNil) | |
914 | So(buf.String(), ShouldEqual, `[author] | |
915 | e-mail = u@gogs.io | |
916 | ||
917 | `) | |
918 | }) | |
919 | ||
920 | Convey("Inverse case", func() { | |
921 | f, err := ini.LoadSources(ini.LoadOptions{ | |
922 | AllowPythonMultilineValues: false, | |
923 | }, minimalConf) | |
924 | So(err, ShouldBeNil) | |
925 | So(f, ShouldNotBeNil) | |
926 | ||
927 | So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty) | |
928 | }) | |
929 | }) | |
930 | ||
931 | Convey("Ignore continuation lines", func() { | |
932 | f, err := ini.LoadSources(ini.LoadOptions{ | |
933 | AllowPythonMultilineValues: false, | |
934 | IgnoreContinuation: true, | |
935 | }, []byte(` | |
936 | key1=a\b\ | |
937 | key2=c\d\ | |
938 | key3=value`)) | |
939 | So(err, ShouldBeNil) | |
940 | So(f, ShouldNotBeNil) | |
941 | ||
942 | So(f.Section("").Key("key1").String(), ShouldEqual, `a\b\`) | |
943 | So(f.Section("").Key("key2").String(), ShouldEqual, `c\d\`) | |
944 | So(f.Section("").Key("key3").String(), ShouldEqual, "value") | |
945 | ||
946 | Convey("Inverse case", func() { | |
947 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(` | |
948 | key1=a\b\ | |
949 | key2=c\d\`)) | |
950 | So(err, ShouldBeNil) | |
951 | So(f, ShouldNotBeNil) | |
952 | ||
953 | So(f.Section("").Key("key1").String(), ShouldEqual, `a\bkey2=c\d`) | |
954 | }) | |
955 | }) | |
956 | ||
957 | Convey("Ignore inline comments", func() { | |
958 | f, err := ini.LoadSources(ini.LoadOptions{ | |
959 | AllowPythonMultilineValues: false, | |
960 | IgnoreInlineComment: true, | |
961 | }, []byte(` | |
962 | key1=value ;comment | |
963 | key2=value2 #comment2 | |
964 | key3=val#ue #comment3`)) | |
965 | So(err, ShouldBeNil) | |
966 | So(f, ShouldNotBeNil) | |
967 | ||
968 | So(f.Section("").Key("key1").String(), ShouldEqual, `value ;comment`) | |
969 | So(f.Section("").Key("key2").String(), ShouldEqual, `value2 #comment2`) | |
970 | So(f.Section("").Key("key3").String(), ShouldEqual, `val#ue #comment3`) | |
971 | ||
972 | Convey("Inverse case", func() { | |
973 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(` | |
974 | key1=value ;comment | |
975 | key2=value2 #comment2`)) | |
976 | So(err, ShouldBeNil) | |
977 | So(f, ShouldNotBeNil) | |
978 | ||
979 | So(f.Section("").Key("key1").String(), ShouldEqual, `value`) | |
980 | So(f.Section("").Key("key1").Comment, ShouldEqual, `;comment`) | |
981 | So(f.Section("").Key("key2").String(), ShouldEqual, `value2`) | |
982 | So(f.Section("").Key("key2").Comment, ShouldEqual, `#comment2`) | |
983 | }) | |
984 | }) | |
985 | ||
986 | Convey("Allow boolean type keys", func() { | |
987 | f, err := ini.LoadSources(ini.LoadOptions{ | |
988 | AllowPythonMultilineValues: false, | |
989 | AllowBooleanKeys: true, | |
990 | }, []byte(` | |
991 | key1=hello | |
992 | #key2 | |
993 | key3`)) | |
994 | So(err, ShouldBeNil) | |
995 | So(f, ShouldNotBeNil) | |
996 | ||
997 | So(f.Section("").KeyStrings(), ShouldResemble, []string{"key1", "key3"}) | |
998 | So(f.Section("").Key("key3").MustBool(false), ShouldBeTrue) | |
999 | ||
1000 | Convey("Write out", func() { | |
1001 | var buf bytes.Buffer | |
1002 | _, err := f.WriteTo(&buf) | |
1003 | So(err, ShouldBeNil) | |
1004 | So(buf.String(), ShouldEqual, `key1 = hello | |
1005 | # key2 | |
1006 | key3 | |
1007 | `) | |
1008 | }) | |
1009 | ||
1010 | Convey("Inverse case", func() { | |
1011 | _, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(` | |
1012 | key1=hello | |
1013 | #key2 | |
1014 | key3`)) | |
1015 | So(err, ShouldNotBeNil) | |
1016 | }) | |
1017 | }) | |
1018 | ||
1019 | Convey("Allow shadow keys", func() { | |
1020 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false, AllowShadows: true}, []byte(` | |
1021 | [remote "origin"] | |
1022 | url = https://github.com/Antergone/test1.git | |
1023 | url = https://github.com/Antergone/test2.git | |
1024 | fetch = +refs/heads/*:refs/remotes/origin/*`)) | |
1025 | So(err, ShouldBeNil) | |
1026 | So(f, ShouldNotBeNil) | |
1027 | ||
1028 | So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test1.git") | |
1029 | So(f.Section(`remote "origin"`).Key("url").ValueWithShadows(), ShouldResemble, []string{ | |
1030 | "https://github.com/Antergone/test1.git", | |
1031 | "https://github.com/Antergone/test2.git", | |
1032 | }) | |
1033 | So(f.Section(`remote "origin"`).Key("fetch").String(), ShouldEqual, "+refs/heads/*:refs/remotes/origin/*") | |
1034 | ||
1035 | Convey("Write out", func() { | |
1036 | var buf bytes.Buffer | |
1037 | _, err := f.WriteTo(&buf) | |
1038 | So(err, ShouldBeNil) | |
1039 | So(buf.String(), ShouldEqual, `[remote "origin"] | |
1040 | url = https://github.com/Antergone/test1.git | |
1041 | url = https://github.com/Antergone/test2.git | |
1042 | fetch = +refs/heads/*:refs/remotes/origin/* | |
1043 | ||
1044 | `) | |
1045 | }) | |
1046 | ||
1047 | Convey("Inverse case", func() { | |
1048 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(` | |
1049 | [remote "origin"] | |
1050 | url = https://github.com/Antergone/test1.git | |
1051 | url = https://github.com/Antergone/test2.git`)) | |
1052 | So(err, ShouldBeNil) | |
1053 | So(f, ShouldNotBeNil) | |
1054 | ||
1055 | So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test2.git") | |
1056 | }) | |
1057 | }) | |
1058 | ||
1059 | Convey("Unescape double quotes inside value", func() { | |
1060 | f, err := ini.LoadSources(ini.LoadOptions{ | |
1061 | AllowPythonMultilineValues: false, | |
1062 | UnescapeValueDoubleQuotes: true, | |
1063 | }, []byte(` | |
1064 | create_repo="创建了仓库 <a href=\"%s\">%s</a>"`)) | |
1065 | So(err, ShouldBeNil) | |
1066 | So(f, ShouldNotBeNil) | |
1067 | ||
1068 | So(f.Section("").Key("create_repo").String(), ShouldEqual, `创建了仓库 <a href="%s">%s</a>`) | |
1069 | ||
1070 | Convey("Inverse case", func() { | |
1071 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(` | |
1072 | create_repo="创建了仓库 <a href=\"%s\">%s</a>"`)) | |
1073 | So(err, ShouldBeNil) | |
1074 | So(f, ShouldNotBeNil) | |
1075 | ||
1076 | So(f.Section("").Key("create_repo").String(), ShouldEqual, `"创建了仓库 <a href=\"%s\">%s</a>"`) | |
1077 | }) | |
1078 | }) | |
1079 | ||
1080 | Convey("Unescape comment symbols inside value", func() { | |
1081 | f, err := ini.LoadSources(ini.LoadOptions{ | |
1082 | AllowPythonMultilineValues: false, | |
1083 | IgnoreInlineComment: true, | |
1084 | UnescapeValueCommentSymbols: true, | |
1085 | }, []byte(` | |
1086 | key = test value <span style="color: %s\; background: %s">more text</span> | |
1087 | `)) | |
1088 | So(err, ShouldBeNil) | |
1089 | So(f, ShouldNotBeNil) | |
1090 | ||
1091 | So(f.Section("").Key("key").String(), ShouldEqual, `test value <span style="color: %s; background: %s">more text</span>`) | |
1092 | }) | |
1093 | ||
1094 | Convey("Can't parse small python-compatible INI files", func() { | |
1095 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(` | |
1096 | [long] | |
1097 | long_rsa_private_key = -----BEGIN RSA PRIVATE KEY----- | |
1098 | foo | |
1099 | bar | |
1100 | foobar | |
1101 | barfoo | |
1102 | -----END RSA PRIVATE KEY----- | |
1103 | `)) | |
331 | 1104 | So(err, ShouldNotBeNil) |
1105 | So(f, ShouldBeNil) | |
1106 | So(err.Error(), ShouldEqual, "key-value delimiter not found: foo\n") | |
1107 | }) | |
1108 | ||
1109 | Convey("Can't parse big python-compatible INI files", func() { | |
1110 | f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(` | |
1111 | [long] | |
1112 | long_rsa_private_key = -----BEGIN RSA PRIVATE KEY----- | |
1113 | 1foo | |
1114 | 2bar | |
1115 | 3foobar | |
1116 | 4barfoo | |
1117 | 5foo | |
1118 | 6bar | |
1119 | 7foobar | |
1120 | 8barfoo | |
1121 | 9foo | |
1122 | 10bar | |
1123 | 11foobar | |
1124 | 12barfoo | |
1125 | 13foo | |
1126 | 14bar | |
1127 | 15foobar | |
1128 | 16barfoo | |
1129 | 17foo | |
1130 | 18bar | |
1131 | 19foobar | |
1132 | 20barfoo | |
1133 | 21foo | |
1134 | 22bar | |
1135 | 23foobar | |
1136 | 24barfoo | |
1137 | 25foo | |
1138 | 26bar | |
1139 | 27foobar | |
1140 | 28barfoo | |
1141 | 29foo | |
1142 | 30bar | |
1143 | 31foobar | |
1144 | 32barfoo | |
1145 | 33foo | |
1146 | 34bar | |
1147 | 35foobar | |
1148 | 36barfoo | |
1149 | 37foo | |
1150 | 38bar | |
1151 | 39foobar | |
1152 | 40barfoo | |
1153 | 41foo | |
1154 | 42bar | |
1155 | 43foobar | |
1156 | 44barfoo | |
1157 | 45foo | |
1158 | 46bar | |
1159 | 47foobar | |
1160 | 48barfoo | |
1161 | 49foo | |
1162 | 50bar | |
1163 | 51foobar | |
1164 | 52barfoo | |
1165 | 53foo | |
1166 | 54bar | |
1167 | 55foobar | |
1168 | 56barfoo | |
1169 | 57foo | |
1170 | 58bar | |
1171 | 59foobar | |
1172 | 60barfoo | |
1173 | 61foo | |
1174 | 62bar | |
1175 | 63foobar | |
1176 | 64barfoo | |
1177 | 65foo | |
1178 | 66bar | |
1179 | 67foobar | |
1180 | 68barfoo | |
1181 | 69foo | |
1182 | 70bar | |
1183 | 71foobar | |
1184 | 72barfoo | |
1185 | 73foo | |
1186 | 74bar | |
1187 | 75foobar | |
1188 | 76barfoo | |
1189 | 77foo | |
1190 | 78bar | |
1191 | 79foobar | |
1192 | 80barfoo | |
1193 | 81foo | |
1194 | 82bar | |
1195 | 83foobar | |
1196 | 84barfoo | |
1197 | 85foo | |
1198 | 86bar | |
1199 | 87foobar | |
1200 | 88barfoo | |
1201 | 89foo | |
1202 | 90bar | |
1203 | 91foobar | |
1204 | 92barfoo | |
1205 | 93foo | |
1206 | 94bar | |
1207 | 95foobar | |
1208 | 96barfoo | |
1209 | -----END RSA PRIVATE KEY----- | |
1210 | `)) | |
1211 | So(err, ShouldNotBeNil) | |
1212 | So(f, ShouldBeNil) | |
1213 | So(err.Error(), ShouldEqual, "key-value delimiter not found: 1foo\n") | |
1214 | }) | |
1215 | ||
1216 | Convey("Allow unparsable sections", func() { | |
1217 | f, err := ini.LoadSources(ini.LoadOptions{ | |
1218 | AllowPythonMultilineValues: false, | |
1219 | Insensitive: true, | |
1220 | UnparseableSections: []string{"core_lesson", "comments"}, | |
1221 | }, []byte(` | |
1222 | Lesson_Location = 87 | |
1223 | Lesson_Status = C | |
1224 | Score = 3 | |
1225 | Time = 00:02:30 | |
1226 | ||
1227 | [CORE_LESSON] | |
1228 | my lesson state data – 1111111111111111111000000000000000001110000 | |
1229 | 111111111111111111100000000000111000000000 – end my lesson state data | |
1230 | ||
1231 | [COMMENTS] | |
1232 | <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)) | |
1233 | So(err, ShouldBeNil) | |
1234 | So(f, ShouldNotBeNil) | |
1235 | ||
1236 | So(f.Section("").Key("score").String(), ShouldEqual, "3") | |
1237 | So(f.Section("").Body(), ShouldBeEmpty) | |
1238 | So(f.Section("core_lesson").Body(), ShouldEqual, `my lesson state data – 1111111111111111111000000000000000001110000 | |
1239 | 111111111111111111100000000000111000000000 – end my lesson state data`) | |
1240 | So(f.Section("comments").Body(), ShouldEqual, `<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`) | |
1241 | ||
1242 | Convey("Write out", func() { | |
1243 | var buf bytes.Buffer | |
1244 | _, err := f.WriteTo(&buf) | |
1245 | So(err, ShouldBeNil) | |
1246 | So(buf.String(), ShouldEqual, `lesson_location = 87 | |
1247 | lesson_status = C | |
1248 | score = 3 | |
1249 | time = 00:02:30 | |
1250 | ||
1251 | [core_lesson] | |
1252 | my lesson state data – 1111111111111111111000000000000000001110000 | |
1253 | 111111111111111111100000000000111000000000 – end my lesson state data | |
1254 | ||
1255 | [comments] | |
1256 | <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1> | |
1257 | `) | |
1258 | }) | |
1259 | ||
1260 | Convey("Inverse case", func() { | |
1261 | _, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(` | |
1262 | [CORE_LESSON] | |
1263 | my lesson state data – 1111111111111111111000000000000000001110000 | |
1264 | 111111111111111111100000000000111000000000 – end my lesson state data`)) | |
1265 | So(err, ShouldNotBeNil) | |
1266 | }) | |
1267 | }) | |
1268 | ||
1269 | Convey("And false `SpaceBeforeInlineComment`", func() { | |
1270 | Convey("Can't parse INI files containing `#` or `;` in value", func() { | |
1271 | f, err := ini.LoadSources( | |
1272 | ini.LoadOptions{AllowPythonMultilineValues: true, SpaceBeforeInlineComment: false}, | |
1273 | []byte(` | |
1274 | [author] | |
1275 | NAME = U#n#k#n#w#o#n | |
1276 | GITHUB = U;n;k;n;w;o;n | |
1277 | `)) | |
1278 | So(err, ShouldBeNil) | |
1279 | So(f, ShouldNotBeNil) | |
1280 | sec := f.Section("author") | |
1281 | nameValue := sec.Key("NAME").String() | |
1282 | githubValue := sec.Key("GITHUB").String() | |
1283 | So(nameValue, ShouldEqual, "U") | |
1284 | So(githubValue, ShouldEqual, "U") | |
1285 | }) | |
1286 | }) | |
1287 | ||
1288 | Convey("And true `SpaceBeforeInlineComment`", func() { | |
1289 | Convey("Can parse INI files containing `#` or `;` in value", func() { | |
1290 | f, err := ini.LoadSources( | |
1291 | ini.LoadOptions{AllowPythonMultilineValues: true, SpaceBeforeInlineComment: true}, | |
1292 | []byte(` | |
1293 | [author] | |
1294 | NAME = U#n#k#n#w#o#n | |
1295 | GITHUB = U;n;k;n;w;o;n | |
1296 | `)) | |
1297 | So(err, ShouldBeNil) | |
1298 | So(f, ShouldNotBeNil) | |
1299 | sec := f.Section("author") | |
1300 | nameValue := sec.Key("NAME").String() | |
1301 | githubValue := sec.Key("GITHUB").String() | |
1302 | So(nameValue, ShouldEqual, "U#n#k#n#w#o#n") | |
1303 | So(githubValue, ShouldEqual, "U;n;k;n;w;o;n") | |
1304 | }) | |
1305 | }) | |
1306 | }) | |
1307 | ||
1308 | Convey("with `ChildSectionDelimiter` ':'", func() { | |
1309 | Convey("Get all keys of parent sections", func() { | |
1310 | f := ini.Empty(ini.LoadOptions{ChildSectionDelimiter: ":"}) | |
1311 | So(f, ShouldNotBeNil) | |
1312 | ||
1313 | k, err := f.Section("package").NewKey("NAME", "ini") | |
1314 | So(err, ShouldBeNil) | |
1315 | So(k, ShouldNotBeNil) | |
1316 | k, err = f.Section("package").NewKey("VERSION", "v1") | |
1317 | So(err, ShouldBeNil) | |
1318 | So(k, ShouldNotBeNil) | |
1319 | k, err = f.Section("package").NewKey("IMPORT_PATH", "gopkg.in/ini.v1") | |
1320 | So(err, ShouldBeNil) | |
1321 | So(k, ShouldNotBeNil) | |
1322 | ||
1323 | keys := f.Section("package:sub:sub2").ParentKeys() | |
1324 | names := []string{"NAME", "VERSION", "IMPORT_PATH"} | |
1325 | So(len(keys), ShouldEqual, len(names)) | |
1326 | for i, name := range names { | |
1327 | So(keys[i].Name(), ShouldEqual, name) | |
1328 | } | |
1329 | }) | |
1330 | ||
1331 | Convey("Getting and setting values", func() { | |
1332 | f, err := ini.LoadSources(ini.LoadOptions{ChildSectionDelimiter: ":"}, fullConf) | |
1333 | So(err, ShouldBeNil) | |
1334 | So(f, ShouldNotBeNil) | |
1335 | ||
1336 | Convey("Get parent-keys that are available to the child section", func() { | |
1337 | parentKeys := f.Section("package:sub").ParentKeys() | |
1338 | So(parentKeys, ShouldNotBeNil) | |
1339 | for _, k := range parentKeys { | |
1340 | So(k.Name(), ShouldEqual, "CLONE_URL") | |
1341 | } | |
1342 | }) | |
1343 | ||
1344 | Convey("Get parent section value", func() { | |
1345 | So(f.Section("package:sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") | |
1346 | So(f.Section("package:fake:sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") | |
1347 | }) | |
1348 | }) | |
1349 | ||
1350 | Convey("Get child sections by parent name", func() { | |
1351 | f, err := ini.LoadSources(ini.LoadOptions{ChildSectionDelimiter: ":"}, []byte(` | |
1352 | [node] | |
1353 | [node:biz1] | |
1354 | [node:biz2] | |
1355 | [node.biz3] | |
1356 | [node.bizN] | |
1357 | `)) | |
1358 | So(err, ShouldBeNil) | |
1359 | So(f, ShouldNotBeNil) | |
1360 | ||
1361 | children := f.ChildSections("node") | |
1362 | names := []string{"node:biz1", "node:biz2"} | |
1363 | So(len(children), ShouldEqual, len(names)) | |
1364 | for i, name := range names { | |
1365 | So(children[i].Name(), ShouldEqual, name) | |
1366 | } | |
1367 | }) | |
1368 | }) | |
1369 | ||
1370 | Convey("ShortCircuit", func() { | |
1371 | Convey("Load the first available configuration, ignore other configuration", func() { | |
1372 | f, err := ini.LoadSources(ini.LoadOptions{ShortCircuit: true}, minimalConf, []byte(`key1 = value1`)) | |
1373 | So(f, ShouldNotBeNil) | |
1374 | So(err, ShouldBeNil) | |
1375 | var buf bytes.Buffer | |
1376 | _, err = f.WriteTo(&buf) | |
1377 | So(err, ShouldBeNil) | |
1378 | So(buf.String(), ShouldEqual, `[author] | |
1379 | E-MAIL = u@gogs.io | |
1380 | ||
1381 | `) | |
1382 | }) | |
1383 | ||
1384 | Convey("Return an error when fail to load", func() { | |
1385 | f, err := ini.LoadSources(ini.LoadOptions{ShortCircuit: true}, notFoundConf, minimalConf) | |
1386 | So(f, ShouldBeNil) | |
1387 | So(err, ShouldNotBeNil) | |
1388 | }) | |
1389 | ||
1390 | Convey("Used with Loose to ignore errors that the file does not exist", func() { | |
1391 | f, err := ini.LoadSources(ini.LoadOptions{ShortCircuit: true, Loose: true}, notFoundConf, minimalConf) | |
1392 | So(f, ShouldNotBeNil) | |
1393 | So(err, ShouldBeNil) | |
1394 | var buf bytes.Buffer | |
1395 | _, err = f.WriteTo(&buf) | |
1396 | So(err, ShouldBeNil) | |
1397 | So(buf.String(), ShouldEqual, `[author] | |
1398 | E-MAIL = u@gogs.io | |
1399 | ||
1400 | `) | |
1401 | }) | |
1402 | ||
1403 | Convey("Ensure all sources are loaded without ShortCircuit", func() { | |
1404 | f, err := ini.LoadSources(ini.LoadOptions{ShortCircuit: false}, minimalConf, []byte(`key1 = value1`)) | |
1405 | So(f, ShouldNotBeNil) | |
1406 | So(err, ShouldBeNil) | |
1407 | var buf bytes.Buffer | |
1408 | _, err = f.WriteTo(&buf) | |
1409 | So(err, ShouldBeNil) | |
1410 | So(buf.String(), ShouldEqual, `key1 = value1 | |
1411 | ||
1412 | [author] | |
1413 | E-MAIL = u@gogs.io | |
1414 | ||
1415 | `) | |
332 | 1416 | }) |
333 | 1417 | }) |
334 | 1418 | }) |
335 | 1419 | } |
1420 | ||
1421 | func Test_KeyValueDelimiters(t *testing.T) { | |
1422 | Convey("Custom key-value delimiters", t, func() { | |
1423 | f, err := ini.LoadSources(ini.LoadOptions{ | |
1424 | KeyValueDelimiters: "?!", | |
1425 | }, []byte(` | |
1426 | [section] | |
1427 | key1?value1 | |
1428 | key2!value2 | |
1429 | `)) | |
1430 | So(err, ShouldBeNil) | |
1431 | So(f, ShouldNotBeNil) | |
1432 | ||
1433 | So(f.Section("section").Key("key1").String(), ShouldEqual, "value1") | |
1434 | So(f.Section("section").Key("key2").String(), ShouldEqual, "value2") | |
1435 | }) | |
1436 | } | |
1437 | ||
1438 | func Test_PreserveSurroundedQuote(t *testing.T) { | |
1439 | Convey("Preserve surrounded quote test", t, func() { | |
1440 | f, err := ini.LoadSources(ini.LoadOptions{ | |
1441 | PreserveSurroundedQuote: true, | |
1442 | }, []byte(` | |
1443 | [section] | |
1444 | key1 = "value1" | |
1445 | key2 = value2 | |
1446 | `)) | |
1447 | So(err, ShouldBeNil) | |
1448 | So(f, ShouldNotBeNil) | |
1449 | ||
1450 | So(f.Section("section").Key("key1").String(), ShouldEqual, "\"value1\"") | |
1451 | So(f.Section("section").Key("key2").String(), ShouldEqual, "value2") | |
1452 | }) | |
1453 | ||
1454 | Convey("Preserve surrounded quote test inverse test", t, func() { | |
1455 | f, err := ini.LoadSources(ini.LoadOptions{ | |
1456 | PreserveSurroundedQuote: false, | |
1457 | }, []byte(` | |
1458 | [section] | |
1459 | key1 = "value1" | |
1460 | key2 = value2 | |
1461 | `)) | |
1462 | So(err, ShouldBeNil) | |
1463 | So(f, ShouldNotBeNil) | |
1464 | ||
1465 | So(f.Section("section").Key("key1").String(), ShouldEqual, "value1") | |
1466 | So(f.Section("section").Key("key2").String(), ShouldEqual, "value2") | |
1467 | }) | |
1468 | } |
53 | 53 | return errors.New("cannot add shadow to auto-increment or boolean key") |
54 | 54 | } |
55 | 55 | |
56 | // Deduplicate shadows based on their values. | |
57 | if k.value == val { | |
58 | return nil | |
59 | } | |
60 | for i := range k.shadows { | |
61 | if k.shadows[i].value == val { | |
62 | return nil | |
63 | } | |
64 | } | |
65 | ||
56 | 66 | shadow := newKey(k.s, k.name, val) |
57 | 67 | shadow.isShadow = true |
58 | 68 | k.shadows = append(k.shadows, shadow) |
76 | 86 | return nil |
77 | 87 | } |
78 | 88 | |
89 | // AddNestedValue adds a nested value to the key. | |
79 | 90 | func (k *Key) AddNestedValue(val string) error { |
80 | 91 | if !k.s.f.options.AllowNestedValues { |
81 | 92 | return errors.New("nested value is not allowed") |
125 | 136 | if !strings.Contains(val, "%") { |
126 | 137 | return val |
127 | 138 | } |
128 | for i := 0; i < _DEPTH_VALUES; i++ { | |
139 | for i := 0; i < depthValues; i++ { | |
129 | 140 | vr := varPattern.FindString(val) |
130 | 141 | if len(vr) == 0 { |
131 | 142 | break |
132 | 143 | } |
133 | 144 | |
134 | 145 | // Take off leading '%(' and trailing ')s'. |
135 | noption := strings.TrimLeft(vr, "%(") | |
136 | noption = strings.TrimRight(noption, ")s") | |
146 | noption := vr[2 : len(vr)-2] | |
137 | 147 | |
138 | 148 | // Search in the same section. |
149 | // If not found or found the key itself, then search again in default section. | |
139 | 150 | nk, err := k.s.GetKey(noption) |
140 | 151 | if err != nil || k == nk { |
141 | // Search again in default section. | |
142 | 152 | nk, _ = k.s.f.Section("").GetKey(noption) |
153 | if nk == nil { | |
154 | // Stop when no results found in the default section, | |
155 | // and returns the value as-is. | |
156 | break | |
157 | } | |
143 | 158 | } |
144 | 159 | |
145 | 160 | // Substitute by new value and take off leading '%(' and trailing ')s'. |
186 | 201 | |
187 | 202 | // Int returns int type value. |
188 | 203 | func (k *Key) Int() (int, error) { |
189 | return strconv.Atoi(k.String()) | |
204 | v, err := strconv.ParseInt(k.String(), 0, 64) | |
205 | return int(v), err | |
190 | 206 | } |
191 | 207 | |
192 | 208 | // Int64 returns int64 type value. |
193 | 209 | func (k *Key) Int64() (int64, error) { |
194 | return strconv.ParseInt(k.String(), 10, 64) | |
210 | return strconv.ParseInt(k.String(), 0, 64) | |
195 | 211 | } |
196 | 212 | |
197 | 213 | // Uint returns uint type valued. |
198 | 214 | func (k *Key) Uint() (uint, error) { |
199 | u, e := strconv.ParseUint(k.String(), 10, 64) | |
215 | u, e := strconv.ParseUint(k.String(), 0, 64) | |
200 | 216 | return uint(u), e |
201 | 217 | } |
202 | 218 | |
203 | 219 | // Uint64 returns uint64 type value. |
204 | 220 | func (k *Key) Uint64() (uint64, error) { |
205 | return strconv.ParseUint(k.String(), 10, 64) | |
221 | return strconv.ParseUint(k.String(), 0, 64) | |
206 | 222 | } |
207 | 223 | |
208 | 224 | // Duration returns time.Duration type value. |
490 | 506 | buf.WriteRune(runes[idx]) |
491 | 507 | } |
492 | 508 | } |
493 | idx += 1 | |
509 | idx++ | |
494 | 510 | if idx == len(runes) { |
495 | 511 | break |
496 | 512 | } |
552 | 568 | return vals |
553 | 569 | } |
554 | 570 | |
571 | // Bools returns list of bool divided by given delimiter. Any invalid input will be treated as zero value. | |
572 | func (k *Key) Bools(delim string) []bool { | |
573 | vals, _ := k.parseBools(k.Strings(delim), true, false) | |
574 | return vals | |
575 | } | |
576 | ||
555 | 577 | // TimesFormat parses with given format and returns list of time.Time divided by given delimiter. |
556 | 578 | // Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). |
557 | 579 | func (k *Key) TimesFormat(format, delim string) []time.Time { |
600 | 622 | return vals |
601 | 623 | } |
602 | 624 | |
625 | // ValidBools returns list of bool divided by given delimiter. If some value is not 64-bit unsigned | |
626 | // integer, then it will not be included to result list. | |
627 | func (k *Key) ValidBools(delim string) []bool { | |
628 | vals, _ := k.parseBools(k.Strings(delim), false, false) | |
629 | return vals | |
630 | } | |
631 | ||
603 | 632 | // ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter. |
604 | 633 | func (k *Key) ValidTimesFormat(format, delim string) []time.Time { |
605 | 634 | vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false) |
634 | 663 | // StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input. |
635 | 664 | func (k *Key) StrictUint64s(delim string) ([]uint64, error) { |
636 | 665 | return k.parseUint64s(k.Strings(delim), false, true) |
666 | } | |
667 | ||
668 | // StrictBools returns list of bool divided by given delimiter or error on first invalid input. | |
669 | func (k *Key) StrictBools(delim string) ([]bool, error) { | |
670 | return k.parseBools(k.Strings(delim), false, true) | |
637 | 671 | } |
638 | 672 | |
639 | 673 | // StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter |
648 | 682 | return k.StrictTimesFormat(time.RFC3339, delim) |
649 | 683 | } |
650 | 684 | |
685 | // parseBools transforms strings to bools. | |
686 | func (k *Key) parseBools(strs []string, addInvalid, returnOnInvalid bool) ([]bool, error) { | |
687 | vals := make([]bool, 0, len(strs)) | |
688 | parser := func(str string) (interface{}, error) { | |
689 | val, err := parseBool(str) | |
690 | return val, err | |
691 | } | |
692 | rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser) | |
693 | if err == nil { | |
694 | for _, val := range rawVals { | |
695 | vals = append(vals, val.(bool)) | |
696 | } | |
697 | } | |
698 | return vals, err | |
699 | } | |
700 | ||
651 | 701 | // parseFloat64s transforms strings to float64s. |
652 | 702 | func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) { |
653 | 703 | vals := make([]float64, 0, len(strs)) |
654 | for _, str := range strs { | |
704 | parser := func(str string) (interface{}, error) { | |
655 | 705 | val, err := strconv.ParseFloat(str, 64) |
656 | if err != nil && returnOnInvalid { | |
657 | return nil, err | |
658 | } | |
659 | if err == nil || addInvalid { | |
660 | vals = append(vals, val) | |
661 | } | |
662 | } | |
663 | return vals, nil | |
706 | return val, err | |
707 | } | |
708 | rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser) | |
709 | if err == nil { | |
710 | for _, val := range rawVals { | |
711 | vals = append(vals, val.(float64)) | |
712 | } | |
713 | } | |
714 | return vals, err | |
664 | 715 | } |
665 | 716 | |
666 | 717 | // parseInts transforms strings to ints. |
667 | 718 | func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) { |
668 | 719 | vals := make([]int, 0, len(strs)) |
669 | for _, str := range strs { | |
670 | val, err := strconv.Atoi(str) | |
671 | if err != nil && returnOnInvalid { | |
672 | return nil, err | |
673 | } | |
674 | if err == nil || addInvalid { | |
675 | vals = append(vals, val) | |
676 | } | |
677 | } | |
678 | return vals, nil | |
720 | parser := func(str string) (interface{}, error) { | |
721 | val, err := strconv.ParseInt(str, 0, 64) | |
722 | return val, err | |
723 | } | |
724 | rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser) | |
725 | if err == nil { | |
726 | for _, val := range rawVals { | |
727 | vals = append(vals, int(val.(int64))) | |
728 | } | |
729 | } | |
730 | return vals, err | |
679 | 731 | } |
680 | 732 | |
681 | 733 | // parseInt64s transforms strings to int64s. |
682 | 734 | func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) { |
683 | 735 | vals := make([]int64, 0, len(strs)) |
684 | for _, str := range strs { | |
685 | val, err := strconv.ParseInt(str, 10, 64) | |
686 | if err != nil && returnOnInvalid { | |
687 | return nil, err | |
688 | } | |
689 | if err == nil || addInvalid { | |
690 | vals = append(vals, val) | |
691 | } | |
692 | } | |
693 | return vals, nil | |
736 | parser := func(str string) (interface{}, error) { | |
737 | val, err := strconv.ParseInt(str, 0, 64) | |
738 | return val, err | |
739 | } | |
740 | ||
741 | rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser) | |
742 | if err == nil { | |
743 | for _, val := range rawVals { | |
744 | vals = append(vals, val.(int64)) | |
745 | } | |
746 | } | |
747 | return vals, err | |
694 | 748 | } |
695 | 749 | |
696 | 750 | // parseUints transforms strings to uints. |
697 | 751 | func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) { |
698 | 752 | vals := make([]uint, 0, len(strs)) |
699 | for _, str := range strs { | |
700 | val, err := strconv.ParseUint(str, 10, 0) | |
701 | if err != nil && returnOnInvalid { | |
702 | return nil, err | |
703 | } | |
704 | if err == nil || addInvalid { | |
705 | vals = append(vals, uint(val)) | |
706 | } | |
707 | } | |
708 | return vals, nil | |
753 | parser := func(str string) (interface{}, error) { | |
754 | val, err := strconv.ParseUint(str, 0, 64) | |
755 | return val, err | |
756 | } | |
757 | ||
758 | rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser) | |
759 | if err == nil { | |
760 | for _, val := range rawVals { | |
761 | vals = append(vals, uint(val.(uint64))) | |
762 | } | |
763 | } | |
764 | return vals, err | |
709 | 765 | } |
710 | 766 | |
711 | 767 | // parseUint64s transforms strings to uint64s. |
712 | 768 | func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) { |
713 | 769 | vals := make([]uint64, 0, len(strs)) |
714 | for _, str := range strs { | |
715 | val, err := strconv.ParseUint(str, 10, 64) | |
716 | if err != nil && returnOnInvalid { | |
717 | return nil, err | |
718 | } | |
719 | if err == nil || addInvalid { | |
720 | vals = append(vals, val) | |
721 | } | |
722 | } | |
723 | return vals, nil | |
724 | } | |
770 | parser := func(str string) (interface{}, error) { | |
771 | val, err := strconv.ParseUint(str, 0, 64) | |
772 | return val, err | |
773 | } | |
774 | rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser) | |
775 | if err == nil { | |
776 | for _, val := range rawVals { | |
777 | vals = append(vals, val.(uint64)) | |
778 | } | |
779 | } | |
780 | return vals, err | |
781 | } | |
782 | ||
783 | ||
784 | type Parser func(str string) (interface{}, error) | |
785 | ||
725 | 786 | |
726 | 787 | // parseTimesFormat transforms strings to times in given format. |
727 | 788 | func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) { |
728 | 789 | vals := make([]time.Time, 0, len(strs)) |
790 | parser := func(str string) (interface{}, error) { | |
791 | val, err := time.Parse(format, str) | |
792 | return val, err | |
793 | } | |
794 | rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser) | |
795 | if err == nil { | |
796 | for _, val := range rawVals { | |
797 | vals = append(vals, val.(time.Time)) | |
798 | } | |
799 | } | |
800 | return vals, err | |
801 | } | |
802 | ||
803 | ||
804 | // doParse transforms strings to different types | |
805 | func (k *Key) doParse(strs []string, addInvalid, returnOnInvalid bool, parser Parser) ([]interface{}, error) { | |
806 | vals := make([]interface{}, 0, len(strs)) | |
729 | 807 | for _, str := range strs { |
730 | val, err := time.Parse(format, str) | |
808 | val, err := parser(str) | |
731 | 809 | if err != nil && returnOnInvalid { |
732 | 810 | return nil, err |
733 | 811 | } |
16 | 16 | import ( |
17 | 17 | "bytes" |
18 | 18 | "fmt" |
19 | "runtime" | |
19 | 20 | "strings" |
20 | 21 | "testing" |
21 | 22 | "time" |
99 | 100 | } |
100 | 101 | } |
101 | 102 | |
103 | func boolsEqual(values []bool, expected ...bool) { | |
104 | So(values, ShouldHaveLength, len(expected)) | |
105 | for i, v := range expected { | |
106 | So(values[i], ShouldEqual, v) | |
107 | } | |
108 | } | |
109 | ||
102 | 110 | func timesEqual(values []time.Time, expected ...time.Time) { |
103 | 111 | So(values, ShouldHaveLength, len(expected)) |
104 | 112 | for i, v := range expected { |
108 | 116 | |
109 | 117 | func TestKey_Helpers(t *testing.T) { |
110 | 118 | Convey("Getting and setting values", t, func() { |
111 | f, err := ini.Load(_FULL_CONF) | |
119 | f, err := ini.Load(fullConf) | |
112 | 120 | So(err, ShouldBeNil) |
113 | 121 | So(f, ShouldNotBeNil) |
114 | 122 | |
165 | 173 | |
166 | 174 | Convey("Get sections", func() { |
167 | 175 | sections := f.Sections() |
168 | for i, name := range []string{ini.DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} { | |
176 | for i, name := range []string{ini.DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} { | |
169 | 177 | So(sections[i].Name(), ShouldEqual, name) |
170 | 178 | } |
171 | 179 | }) |
176 | 184 | }) |
177 | 185 | |
178 | 186 | Convey("Get multiple line value", func() { |
187 | if runtime.GOOS == "windows" { | |
188 | t.Skip("Skipping testing on Windows") | |
189 | } | |
190 | ||
179 | 191 | So(f.Section("author").Key("BIO").String(), ShouldEqual, "Gopher.\nCoding addict.\nGood man.\n") |
180 | 192 | }) |
181 | 193 | |
214 | 226 | v7, err := sec.Key("TIME").Time() |
215 | 227 | So(err, ShouldBeNil) |
216 | 228 | So(v7.String(), ShouldEqual, t.String()) |
229 | ||
230 | v8, err := sec.Key("HEX_NUMBER").Int() | |
231 | So(err, ShouldBeNil) | |
232 | So(v8, ShouldEqual, 0x3000) | |
217 | 233 | |
218 | 234 | Convey("Must get values with type", func() { |
219 | 235 | So(sec.Key("STRING").MustString("404"), ShouldEqual, "str") |
224 | 240 | So(sec.Key("UINT").MustUint(), ShouldEqual, 3) |
225 | 241 | So(sec.Key("UINT").MustUint64(), ShouldEqual, 3) |
226 | 242 | So(sec.Key("TIME").MustTime().String(), ShouldEqual, t.String()) |
243 | So(sec.Key("HEX_NUMBER").MustInt(), ShouldEqual, 0x3000) | |
227 | 244 | |
228 | 245 | dur, err := time.ParseDuration("2h45m") |
229 | 246 | So(err, ShouldBeNil) |
237 | 254 | So(sec.Key("INT64_404").MustInt64(15), ShouldEqual, 15) |
238 | 255 | So(sec.Key("UINT_404").MustUint(6), ShouldEqual, 6) |
239 | 256 | So(sec.Key("UINT64_404").MustUint64(6), ShouldEqual, 6) |
257 | So(sec.Key("HEX_NUMBER_404").MustInt(0x3001), ShouldEqual, 0x3001) | |
240 | 258 | |
241 | 259 | t, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z") |
242 | 260 | So(err, ShouldBeNil) |
254 | 272 | So(sec.Key("UINT64_404").String(), ShouldEqual, "6") |
255 | 273 | So(sec.Key("TIME_404").String(), ShouldEqual, "2014-01-01T20:17:05Z") |
256 | 274 | So(sec.Key("DURATION_404").String(), ShouldEqual, "2h45m0s") |
275 | So(sec.Key("HEX_NUMBER_404").String(), ShouldEqual, "12289") | |
257 | 276 | }) |
258 | 277 | }) |
259 | 278 | }) |
329 | 348 | vals5 := sec.Key("UINTS").Uint64s(",") |
330 | 349 | uint64sEqual(vals5, 1, 2, 3) |
331 | 350 | |
351 | vals6 := sec.Key("BOOLS").Bools(",") | |
352 | boolsEqual(vals6, true, false, false) | |
353 | ||
332 | 354 | t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") |
333 | 355 | So(err, ShouldBeNil) |
334 | vals6 := sec.Key("TIMES").Times(",") | |
335 | timesEqual(vals6, t, t, t) | |
356 | vals7 := sec.Key("TIMES").Times(",") | |
357 | timesEqual(vals7, t, t, t) | |
336 | 358 | }) |
337 | 359 | |
338 | 360 | Convey("Test string slice escapes", func() { |
362 | 384 | vals5 := sec.Key("UINTS").ValidUint64s(",") |
363 | 385 | uint64sEqual(vals5, 1, 2, 3) |
364 | 386 | |
387 | vals6 := sec.Key("BOOLS").ValidBools(",") | |
388 | boolsEqual(vals6, true, false, false) | |
389 | ||
365 | 390 | t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") |
366 | 391 | So(err, ShouldBeNil) |
367 | vals6 := sec.Key("TIMES").ValidTimes(",") | |
368 | timesEqual(vals6, t, t, t) | |
392 | vals7 := sec.Key("TIMES").ValidTimes(",") | |
393 | timesEqual(vals7, t, t, t) | |
369 | 394 | }) |
370 | 395 | |
371 | 396 | Convey("Get values one type into slice of another type", func() { |
385 | 410 | vals5 := sec.Key("STRINGS").ValidUint64s(",") |
386 | 411 | So(vals5, ShouldBeEmpty) |
387 | 412 | |
388 | vals6 := sec.Key("STRINGS").ValidTimes(",") | |
413 | vals6 := sec.Key("STRINGS").ValidBools(",") | |
389 | 414 | So(vals6, ShouldBeEmpty) |
415 | ||
416 | vals7 := sec.Key("STRINGS").ValidTimes(",") | |
417 | So(vals7, ShouldBeEmpty) | |
390 | 418 | }) |
391 | 419 | |
392 | 420 | Convey("Get valid values into slice without errors", func() { |
411 | 439 | So(err, ShouldBeNil) |
412 | 440 | uint64sEqual(vals5, 1, 2, 3) |
413 | 441 | |
442 | vals6, err := sec.Key("BOOLS").StrictBools(",") | |
443 | So(err, ShouldBeNil) | |
444 | boolsEqual(vals6, true, false, false) | |
445 | ||
414 | 446 | t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") |
415 | 447 | So(err, ShouldBeNil) |
416 | vals6, err := sec.Key("TIMES").StrictTimes(",") | |
417 | So(err, ShouldBeNil) | |
418 | timesEqual(vals6, t, t, t) | |
448 | vals7, err := sec.Key("TIMES").StrictTimes(",") | |
449 | So(err, ShouldBeNil) | |
450 | timesEqual(vals7, t, t, t) | |
419 | 451 | }) |
420 | 452 | |
421 | 453 | Convey("Get invalid values into slice", func() { |
440 | 472 | So(vals5, ShouldBeEmpty) |
441 | 473 | So(err, ShouldNotBeNil) |
442 | 474 | |
443 | vals6, err := sec.Key("STRINGS").StrictTimes(",") | |
475 | vals6, err := sec.Key("STRINGS").StrictBools(",") | |
444 | 476 | So(vals6, ShouldBeEmpty) |
477 | So(err, ShouldNotBeNil) | |
478 | ||
479 | vals7, err := sec.Key("STRINGS").StrictTimes(",") | |
480 | So(vals7, ShouldBeEmpty) | |
445 | 481 | So(err, ShouldNotBeNil) |
446 | 482 | }) |
447 | 483 | }) |
513 | 549 | Convey("Recursive values should not reflect on same key", t, func() { |
514 | 550 | f, err := ini.Load([]byte(` |
515 | 551 | NAME = ini |
552 | expires = yes | |
516 | 553 | [package] |
517 | NAME = %(NAME)s`)) | |
518 | So(err, ShouldBeNil) | |
519 | So(f, ShouldNotBeNil) | |
554 | NAME = %(NAME)s | |
555 | expires = %(expires)s`)) | |
556 | So(err, ShouldBeNil) | |
557 | So(f, ShouldNotBeNil) | |
558 | ||
520 | 559 | So(f.Section("package").Key("NAME").String(), ShouldEqual, "ini") |
521 | }) | |
522 | } | |
560 | So(f.Section("package").Key("expires").String(), ShouldEqual, "yes") | |
561 | }) | |
562 | ||
563 | Convey("Recursive value with no target found", t, func() { | |
564 | f, err := ini.Load([]byte(` | |
565 | [foo] | |
566 | bar = %(missing)s | |
567 | `)) | |
568 | So(err, ShouldBeNil) | |
569 | So(f, ShouldNotBeNil) | |
570 | ||
571 | So(f.Section("foo").Key("bar").String(), ShouldEqual, "%(missing)s") | |
572 | }) | |
573 | } |
18 | 18 | "bytes" |
19 | 19 | "fmt" |
20 | 20 | "io" |
21 | "regexp" | |
21 | 22 | "strconv" |
22 | 23 | "strings" |
23 | 24 | "unicode" |
24 | 25 | ) |
25 | 26 | |
26 | type tokenType int | |
27 | ||
28 | const ( | |
29 | _TOKEN_INVALID tokenType = iota | |
30 | _TOKEN_COMMENT | |
31 | _TOKEN_SECTION | |
32 | _TOKEN_KEY | |
33 | ) | |
27 | const minReaderBufferSize = 4096 | |
28 | ||
29 | var pythonMultiline = regexp.MustCompile(`^([\t\f ]+)(.*)`) | |
30 | ||
31 | type parserOptions struct { | |
32 | IgnoreContinuation bool | |
33 | IgnoreInlineComment bool | |
34 | AllowPythonMultilineValues bool | |
35 | SpaceBeforeInlineComment bool | |
36 | UnescapeValueDoubleQuotes bool | |
37 | UnescapeValueCommentSymbols bool | |
38 | PreserveSurroundedQuote bool | |
39 | DebugFunc DebugFunc | |
40 | ReaderBufferSize int | |
41 | } | |
34 | 42 | |
35 | 43 | type parser struct { |
36 | 44 | buf *bufio.Reader |
45 | options parserOptions | |
46 | ||
37 | 47 | isEOF bool |
38 | 48 | count int |
39 | 49 | comment *bytes.Buffer |
40 | 50 | } |
41 | 51 | |
42 | func newParser(r io.Reader) *parser { | |
52 | func (p *parser) debug(format string, args ...interface{}) { | |
53 | if p.options.DebugFunc != nil { | |
54 | p.options.DebugFunc(fmt.Sprintf(format, args...)) | |
55 | } | |
56 | } | |
57 | ||
58 | func newParser(r io.Reader, opts parserOptions) *parser { | |
59 | size := opts.ReaderBufferSize | |
60 | if size < minReaderBufferSize { | |
61 | size = minReaderBufferSize | |
62 | } | |
63 | ||
43 | 64 | return &parser{ |
44 | buf: bufio.NewReader(r), | |
65 | buf: bufio.NewReaderSize(r, size), | |
66 | options: opts, | |
45 | 67 | count: 1, |
46 | 68 | comment: &bytes.Buffer{}, |
47 | 69 | } |
61 | 83 | case mask[0] == 254 && mask[1] == 255: |
62 | 84 | fallthrough |
63 | 85 | case mask[0] == 255 && mask[1] == 254: |
64 | p.buf.Read(mask) | |
86 | _, err = p.buf.Read(mask) | |
87 | if err != nil { | |
88 | return err | |
89 | } | |
65 | 90 | case mask[0] == 239 && mask[1] == 187: |
66 | 91 | mask, err := p.buf.Peek(3) |
67 | 92 | if err != nil && err != io.EOF { |
70 | 95 | return nil |
71 | 96 | } |
72 | 97 | if mask[2] == 191 { |
73 | p.buf.Read(mask) | |
98 | _, err = p.buf.Read(mask) | |
99 | if err != nil { | |
100 | return err | |
101 | } | |
74 | 102 | } |
75 | 103 | } |
76 | 104 | return nil |
96 | 124 | return in[i:], true |
97 | 125 | } |
98 | 126 | |
99 | func readKeyName(in []byte) (string, int, error) { | |
127 | func readKeyName(delimiters string, in []byte) (string, int, error) { | |
100 | 128 | line := string(in) |
101 | 129 | |
102 | 130 | // Check if key name surrounded by quotes. |
112 | 140 | } |
113 | 141 | |
114 | 142 | // Get out key name |
115 | endIdx := -1 | |
143 | var endIdx int | |
116 | 144 | if len(keyQuote) > 0 { |
117 | 145 | startIdx := len(keyQuote) |
118 | 146 | // FIXME: fail case -> """"""name"""=value |
123 | 151 | pos += startIdx |
124 | 152 | |
125 | 153 | // Find key-value delimiter |
126 | i := strings.IndexAny(line[pos+startIdx:], "=:") | |
154 | i := strings.IndexAny(line[pos+startIdx:], delimiters) | |
127 | 155 | if i < 0 { |
128 | 156 | return "", -1, ErrDelimiterNotFound{line} |
129 | 157 | } |
131 | 159 | return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil |
132 | 160 | } |
133 | 161 | |
134 | endIdx = strings.IndexAny(line, "=:") | |
162 | endIdx = strings.IndexAny(line, delimiters) | |
135 | 163 | if endIdx < 0 { |
136 | 164 | return "", -1, ErrDelimiterNotFound{line} |
137 | 165 | } |
158 | 186 | } |
159 | 187 | val += next |
160 | 188 | if p.isEOF { |
161 | return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next) | |
189 | return "", fmt.Errorf("missing closing key quote from %q to %q", line, next) | |
162 | 190 | } |
163 | 191 | } |
164 | 192 | return val, nil |
192 | 220 | strings.IndexByte(in[1:], quote) == len(in)-2 |
193 | 221 | } |
194 | 222 | |
195 | func (p *parser) readValue(in []byte, | |
196 | ignoreContinuation, ignoreInlineComment, unescapeValueDoubleQuotes, unescapeValueCommentSymbols bool) (string, error) { | |
223 | func (p *parser) readValue(in []byte, bufferSize int) (string, error) { | |
197 | 224 | |
198 | 225 | line := strings.TrimLeftFunc(string(in), unicode.IsSpace) |
199 | 226 | if len(line) == 0 { |
227 | if p.options.AllowPythonMultilineValues && len(in) > 0 && in[len(in)-1] == '\n' { | |
228 | return p.readPythonMultilines(line, bufferSize) | |
229 | } | |
200 | 230 | return "", nil |
201 | 231 | } |
202 | 232 | |
205 | 235 | valQuote = `"""` |
206 | 236 | } else if line[0] == '`' { |
207 | 237 | valQuote = "`" |
208 | } else if unescapeValueDoubleQuotes && line[0] == '"' { | |
238 | } else if p.options.UnescapeValueDoubleQuotes && line[0] == '"' { | |
209 | 239 | valQuote = `"` |
210 | 240 | } |
211 | 241 | |
217 | 247 | return p.readMultilines(line, line[startIdx:], valQuote) |
218 | 248 | } |
219 | 249 | |
220 | if unescapeValueDoubleQuotes && valQuote == `"` { | |
250 | if p.options.UnescapeValueDoubleQuotes && valQuote == `"` { | |
221 | 251 | return strings.Replace(line[startIdx:pos+startIdx], `\"`, `"`, -1), nil |
222 | 252 | } |
223 | 253 | return line[startIdx : pos+startIdx], nil |
224 | 254 | } |
225 | 255 | |
256 | lastChar := line[len(line)-1] | |
226 | 257 | // Won't be able to reach here if value only contains whitespace |
227 | 258 | line = strings.TrimSpace(line) |
259 | trimmedLastChar := line[len(line)-1] | |
228 | 260 | |
229 | 261 | // Check continuation lines when desired |
230 | if !ignoreContinuation && line[len(line)-1] == '\\' { | |
262 | if !p.options.IgnoreContinuation && trimmedLastChar == '\\' { | |
231 | 263 | return p.readContinuationLines(line[:len(line)-1]) |
232 | 264 | } |
233 | 265 | |
234 | 266 | // Check if ignore inline comment |
235 | if !ignoreInlineComment { | |
236 | i := strings.IndexAny(line, "#;") | |
267 | if !p.options.IgnoreInlineComment { | |
268 | var i int | |
269 | if p.options.SpaceBeforeInlineComment { | |
270 | i = strings.Index(line, " #") | |
271 | if i == -1 { | |
272 | i = strings.Index(line, " ;") | |
273 | } | |
274 | ||
275 | } else { | |
276 | i = strings.IndexAny(line, "#;") | |
277 | } | |
278 | ||
237 | 279 | if i > -1 { |
238 | 280 | p.comment.WriteString(line[i:]) |
239 | 281 | line = strings.TrimSpace(line[:i]) |
240 | 282 | } |
283 | ||
241 | 284 | } |
242 | 285 | |
243 | 286 | // Trim single and double quotes |
244 | if hasSurroundedQuote(line, '\'') || | |
245 | hasSurroundedQuote(line, '"') { | |
287 | if (hasSurroundedQuote(line, '\'') || | |
288 | hasSurroundedQuote(line, '"')) && !p.options.PreserveSurroundedQuote { | |
246 | 289 | line = line[1 : len(line)-1] |
247 | } else if len(valQuote) == 0 && unescapeValueCommentSymbols { | |
290 | } else if len(valQuote) == 0 && p.options.UnescapeValueCommentSymbols { | |
248 | 291 | if strings.Contains(line, `\;`) { |
249 | 292 | line = strings.Replace(line, `\;`, ";", -1) |
250 | 293 | } |
251 | 294 | if strings.Contains(line, `\#`) { |
252 | 295 | line = strings.Replace(line, `\#`, "#", -1) |
253 | 296 | } |
254 | } | |
297 | } else if p.options.AllowPythonMultilineValues && lastChar == '\n' { | |
298 | return p.readPythonMultilines(line, bufferSize) | |
299 | } | |
300 | ||
255 | 301 | return line, nil |
302 | } | |
303 | ||
304 | func (p *parser) readPythonMultilines(line string, bufferSize int) (string, error) { | |
305 | parserBufferPeekResult, _ := p.buf.Peek(bufferSize) | |
306 | peekBuffer := bytes.NewBuffer(parserBufferPeekResult) | |
307 | ||
308 | indentSize := 0 | |
309 | for { | |
310 | peekData, peekErr := peekBuffer.ReadBytes('\n') | |
311 | if peekErr != nil { | |
312 | if peekErr == io.EOF { | |
313 | p.debug("readPythonMultilines: io.EOF, peekData: %q, line: %q", string(peekData), line) | |
314 | return line, nil | |
315 | } | |
316 | ||
317 | p.debug("readPythonMultilines: failed to peek with error: %v", peekErr) | |
318 | return "", peekErr | |
319 | } | |
320 | ||
321 | p.debug("readPythonMultilines: parsing %q", string(peekData)) | |
322 | ||
323 | peekMatches := pythonMultiline.FindStringSubmatch(string(peekData)) | |
324 | p.debug("readPythonMultilines: matched %d parts", len(peekMatches)) | |
325 | for n, v := range peekMatches { | |
326 | p.debug(" %d: %q", n, v) | |
327 | } | |
328 | ||
329 | // Return if not a Python multiline value. | |
330 | if len(peekMatches) != 3 { | |
331 | p.debug("readPythonMultilines: end of value, got: %q", line) | |
332 | return line, nil | |
333 | } | |
334 | ||
335 | // Determine indent size and line prefix. | |
336 | currentIndentSize := len(peekMatches[1]) | |
337 | if indentSize < 1 { | |
338 | indentSize = currentIndentSize | |
339 | p.debug("readPythonMultilines: indent size is %d", indentSize) | |
340 | } | |
341 | ||
342 | // Make sure each line is indented at least as far as first line. | |
343 | if currentIndentSize < indentSize { | |
344 | p.debug("readPythonMultilines: end of value, current indent: %d, expected indent: %d, line: %q", currentIndentSize, indentSize, line) | |
345 | return line, nil | |
346 | } | |
347 | ||
348 | // Advance the parser reader (buffer) in-sync with the peek buffer. | |
349 | _, err := p.buf.Discard(len(peekData)) | |
350 | if err != nil { | |
351 | p.debug("readPythonMultilines: failed to skip to the end, returning error") | |
352 | return "", err | |
353 | } | |
354 | ||
355 | // Handle indented empty line. | |
356 | line += "\n" + peekMatches[1][indentSize:] + peekMatches[2] | |
357 | } | |
256 | 358 | } |
257 | 359 | |
258 | 360 | // parse parses data through an io.Reader. |
259 | 361 | func (f *File) parse(reader io.Reader) (err error) { |
260 | p := newParser(reader) | |
362 | p := newParser(reader, parserOptions{ | |
363 | IgnoreContinuation: f.options.IgnoreContinuation, | |
364 | IgnoreInlineComment: f.options.IgnoreInlineComment, | |
365 | AllowPythonMultilineValues: f.options.AllowPythonMultilineValues, | |
366 | SpaceBeforeInlineComment: f.options.SpaceBeforeInlineComment, | |
367 | UnescapeValueDoubleQuotes: f.options.UnescapeValueDoubleQuotes, | |
368 | UnescapeValueCommentSymbols: f.options.UnescapeValueCommentSymbols, | |
369 | PreserveSurroundedQuote: f.options.PreserveSurroundedQuote, | |
370 | DebugFunc: f.options.DebugFunc, | |
371 | ReaderBufferSize: f.options.ReaderBufferSize, | |
372 | }) | |
261 | 373 | if err = p.BOM(); err != nil { |
262 | 374 | return fmt.Errorf("BOM: %v", err) |
263 | 375 | } |
264 | 376 | |
265 | 377 | // Ignore error because default section name is never empty string. |
266 | name := DEFAULT_SECTION | |
267 | if f.options.Insensitive { | |
268 | name = strings.ToLower(DEFAULT_SECTION) | |
378 | name := DefaultSection | |
379 | if f.options.Insensitive || f.options.InsensitiveSections { | |
380 | name = strings.ToLower(DefaultSection) | |
269 | 381 | } |
270 | 382 | section, _ := f.NewSection(name) |
271 | 383 | |
275 | 387 | |
276 | 388 | var line []byte |
277 | 389 | var inUnparseableSection bool |
390 | ||
391 | // NOTE: Iterate and increase `currentPeekSize` until | |
392 | // the size of the parser buffer is found. | |
393 | // TODO(unknwon): When Golang 1.10 is the lowest version supported, replace with `parserBufferSize := p.buf.Size()`. | |
394 | parserBufferSize := 0 | |
395 | // NOTE: Peek 4kb at a time. | |
396 | currentPeekSize := minReaderBufferSize | |
397 | ||
398 | if f.options.AllowPythonMultilineValues { | |
399 | for { | |
400 | peekBytes, _ := p.buf.Peek(currentPeekSize) | |
401 | peekBytesLength := len(peekBytes) | |
402 | ||
403 | if parserBufferSize >= peekBytesLength { | |
404 | break | |
405 | } | |
406 | ||
407 | currentPeekSize *= 2 | |
408 | parserBufferSize = peekBytesLength | |
409 | } | |
410 | } | |
411 | ||
278 | 412 | for !p.isEOF { |
279 | 413 | line, err = p.readUntil('\n') |
280 | 414 | if err != nil { |
284 | 418 | if f.options.AllowNestedValues && |
285 | 419 | isLastValueEmpty && len(line) > 0 { |
286 | 420 | if line[0] == ' ' || line[0] == '\t' { |
287 | lastRegularKey.addNestedValue(string(bytes.TrimSpace(line))) | |
421 | err = lastRegularKey.addNestedValue(string(bytes.TrimSpace(line))) | |
422 | if err != nil { | |
423 | return err | |
424 | } | |
288 | 425 | continue |
289 | 426 | } |
290 | 427 | } |
306 | 443 | // Section |
307 | 444 | if line[0] == '[' { |
308 | 445 | // Read to the next ']' (TODO: support quoted strings) |
309 | // TODO(unknwon): use LastIndexByte when stop supporting Go1.4 | |
310 | closeIdx := bytes.LastIndex(line, []byte("]")) | |
446 | closeIdx := bytes.LastIndexByte(line, ']') | |
311 | 447 | if closeIdx == -1 { |
312 | 448 | return fmt.Errorf("unclosed section: %s", line) |
313 | 449 | } |
325 | 461 | |
326 | 462 | section.Comment = strings.TrimSpace(p.comment.String()) |
327 | 463 | |
328 | // Reset aotu-counter and comments | |
464 | // Reset auto-counter and comments | |
329 | 465 | p.comment.Reset() |
330 | 466 | p.count = 1 |
331 | 467 | |
332 | 468 | inUnparseableSection = false |
333 | 469 | for i := range f.options.UnparseableSections { |
334 | 470 | if f.options.UnparseableSections[i] == name || |
335 | (f.options.Insensitive && strings.ToLower(f.options.UnparseableSections[i]) == strings.ToLower(name)) { | |
471 | ((f.options.Insensitive || f.options.InsensitiveSections) && strings.EqualFold(f.options.UnparseableSections[i], name)) { | |
336 | 472 | inUnparseableSection = true |
337 | 473 | continue |
338 | 474 | } |
346 | 482 | continue |
347 | 483 | } |
348 | 484 | |
349 | kname, offset, err := readKeyName(line) | |
485 | kname, offset, err := readKeyName(f.options.KeyValueDelimiters, line) | |
350 | 486 | if err != nil { |
351 | 487 | // Treat as boolean key when desired, and whole line is key name. |
352 | if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys { | |
353 | kname, err := p.readValue(line, | |
354 | f.options.IgnoreContinuation, | |
355 | f.options.IgnoreInlineComment, | |
356 | f.options.UnescapeValueDoubleQuotes, | |
357 | f.options.UnescapeValueCommentSymbols) | |
358 | if err != nil { | |
359 | return err | |
488 | if IsErrDelimiterNotFound(err) { | |
489 | switch { | |
490 | case f.options.AllowBooleanKeys: | |
491 | kname, err := p.readValue(line, parserBufferSize) | |
492 | if err != nil { | |
493 | return err | |
494 | } | |
495 | key, err := section.NewBooleanKey(kname) | |
496 | if err != nil { | |
497 | return err | |
498 | } | |
499 | key.Comment = strings.TrimSpace(p.comment.String()) | |
500 | p.comment.Reset() | |
501 | continue | |
502 | ||
503 | case f.options.SkipUnrecognizableLines: | |
504 | continue | |
360 | 505 | } |
361 | key, err := section.NewBooleanKey(kname) | |
362 | if err != nil { | |
363 | return err | |
364 | } | |
365 | key.Comment = strings.TrimSpace(p.comment.String()) | |
366 | p.comment.Reset() | |
367 | continue | |
368 | 506 | } |
369 | 507 | return err |
370 | 508 | } |
377 | 515 | p.count++ |
378 | 516 | } |
379 | 517 | |
380 | value, err := p.readValue(line[offset:], | |
381 | f.options.IgnoreContinuation, | |
382 | f.options.IgnoreInlineComment, | |
383 | f.options.UnescapeValueDoubleQuotes, | |
384 | f.options.UnescapeValueCommentSymbols) | |
518 | value, err := p.readValue(line[offset:], parserBufferSize) | |
385 | 519 | if err != nil { |
386 | 520 | return err |
387 | 521 | } |
27 | 27 | So(err, ShouldBeNil) |
28 | 28 | So(f, ShouldNotBeNil) |
29 | 29 | |
30 | So(f.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io") | |
30 | So(f.Section("author").Key("E-MAIL").String(), ShouldEqual, "example@email.com") | |
31 | 31 | }) |
32 | 32 | |
33 | 33 | Convey("UTF-16-LE-BOM", func() { |
65 | 65 | func (s *Section) NewKey(name, val string) (*Key, error) { |
66 | 66 | if len(name) == 0 { |
67 | 67 | return nil, errors.New("error creating new key: empty key name") |
68 | } else if s.f.options.Insensitive { | |
68 | } else if s.f.options.Insensitive || s.f.options.InsensitiveKeys { | |
69 | 69 | name = strings.ToLower(name) |
70 | 70 | } |
71 | 71 | |
81 | 81 | } |
82 | 82 | } else { |
83 | 83 | s.keys[name].value = val |
84 | s.keysHash[name] = val | |
84 | 85 | } |
85 | 86 | return s.keys[name], nil |
86 | 87 | } |
104 | 105 | |
105 | 106 | // GetKey returns key in section by given name. |
106 | 107 | func (s *Section) GetKey(name string) (*Key, error) { |
107 | // FIXME: change to section level lock? | |
108 | 108 | if s.f.BlockMode { |
109 | 109 | s.f.lock.RLock() |
110 | 110 | } |
111 | if s.f.options.Insensitive { | |
111 | if s.f.options.Insensitive || s.f.options.InsensitiveKeys { | |
112 | 112 | name = strings.ToLower(name) |
113 | 113 | } |
114 | 114 | key := s.keys[name] |
120 | 120 | // Check if it is a child-section. |
121 | 121 | sname := s.name |
122 | 122 | for { |
123 | if i := strings.LastIndex(sname, "."); i > -1 { | |
123 | if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 { | |
124 | 124 | sname = sname[:i] |
125 | 125 | sec, err := s.f.GetSection(sname) |
126 | 126 | if err != nil { |
127 | 127 | continue |
128 | 128 | } |
129 | 129 | return sec.GetKey(name) |
130 | } else { | |
131 | break | |
132 | 130 | } |
133 | } | |
134 | return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name) | |
131 | break | |
132 | } | |
133 | return nil, fmt.Errorf("error when getting key of section %q: key %q not exists", s.name, name) | |
135 | 134 | } |
136 | 135 | return key, nil |
137 | 136 | } |
142 | 141 | return key != nil |
143 | 142 | } |
144 | 143 | |
145 | // Haskey is a backwards-compatible name for HasKey. | |
146 | // TODO: delete me in v2 | |
144 | // Deprecated: Use "HasKey" instead. | |
147 | 145 | func (s *Section) Haskey(name string) bool { |
148 | 146 | return s.HasKey(name) |
149 | 147 | } |
189 | 187 | var parentKeys []*Key |
190 | 188 | sname := s.name |
191 | 189 | for { |
192 | if i := strings.LastIndex(sname, "."); i > -1 { | |
190 | if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 { | |
193 | 191 | sname = sname[:i] |
194 | 192 | sec, err := s.f.GetSection(sname) |
195 | 193 | if err != nil { |
236 | 234 | if k == name { |
237 | 235 | s.keyList = append(s.keyList[:i], s.keyList[i+1:]...) |
238 | 236 | delete(s.keys, name) |
237 | delete(s.keysHash, name) | |
239 | 238 | return |
240 | 239 | } |
241 | 240 | } |
245 | 244 | // For example, "[parent.child1]" and "[parent.child12]" are child sections |
246 | 245 | // of section "[parent]". |
247 | 246 | func (s *Section) ChildSections() []*Section { |
248 | prefix := s.name + "." | |
247 | prefix := s.name + s.f.options.ChildSectionDelimiter | |
249 | 248 | children := make([]*Section, 0, 3) |
250 | 249 | for _, name := range s.f.sectionList { |
251 | 250 | if strings.HasPrefix(name, prefix) { |
252 | children = append(children, s.f.sections[name]) | |
251 | children = append(children, s.f.sections[name]...) | |
253 | 252 | } |
254 | 253 | } |
255 | 254 | return children |
150 | 150 | So(k, ShouldNotBeNil) |
151 | 151 | |
152 | 152 | So(f.Section("").HasKey("NAME"), ShouldBeTrue) |
153 | So(f.Section("").Haskey("NAME"), ShouldBeTrue) | |
153 | So(f.Section("").HasKey("NAME"), ShouldBeTrue) | |
154 | 154 | So(f.Section("").HasKey("404"), ShouldBeFalse) |
155 | So(f.Section("").Haskey("404"), ShouldBeFalse) | |
155 | So(f.Section("").HasKey("404"), ShouldBeFalse) | |
156 | 156 | }) |
157 | 157 | } |
158 | 158 | |
271 | 271 | |
272 | 272 | func TestSection_KeyHash(t *testing.T) { |
273 | 273 | Convey("Get clone of key hash", t, func() { |
274 | f := ini.Empty() | |
275 | So(f, ShouldNotBeNil) | |
276 | ||
277 | k, err := f.Section("").NewKey("NAME", "ini") | |
278 | So(err, ShouldBeNil) | |
279 | So(k, ShouldNotBeNil) | |
280 | k, err = f.Section("").NewKey("VERSION", "v1") | |
281 | So(err, ShouldBeNil) | |
282 | So(k, ShouldNotBeNil) | |
283 | k, err = f.Section("").NewKey("IMPORT_PATH", "gopkg.in/ini.v1") | |
284 | So(err, ShouldBeNil) | |
285 | So(k, ShouldNotBeNil) | |
286 | ||
287 | hash := f.Section("").KeysHash() | |
274 | f, err := ini.Load([]byte(` | |
275 | key = one | |
276 | [log] | |
277 | name = app | |
278 | file = a.log | |
279 | `), []byte(` | |
280 | key = two | |
281 | [log] | |
282 | name = app2 | |
283 | file = b.log | |
284 | `)) | |
285 | So(err, ShouldBeNil) | |
286 | So(f, ShouldNotBeNil) | |
287 | ||
288 | So(f.Section("").Key("key").String(), ShouldEqual, "two") | |
289 | ||
290 | hash := f.Section("log").KeysHash() | |
288 | 291 | relation := map[string]string{ |
289 | "NAME": "ini", | |
290 | "VERSION": "v1", | |
291 | "IMPORT_PATH": "gopkg.in/ini.v1", | |
292 | "name": "app2", | |
293 | "file": "b.log", | |
292 | 294 | } |
293 | 295 | for k, v := range hash { |
294 | 296 | So(v, ShouldEqual, relation[k]) |
28 | 28 | |
29 | 29 | // Built-in name getters. |
30 | 30 | var ( |
31 | // AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE. | |
32 | AllCapsUnderscore NameMapper = func(raw string) string { | |
31 | // SnackCase converts to format SNACK_CASE. | |
32 | SnackCase NameMapper = func(raw string) string { | |
33 | 33 | newstr := make([]rune, 0, len(raw)) |
34 | 34 | for i, chr := range raw { |
35 | 35 | if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { |
49 | 49 | if i > 0 { |
50 | 50 | newstr = append(newstr, '_') |
51 | 51 | } |
52 | chr -= ('A' - 'a') | |
52 | chr -= 'A' - 'a' | |
53 | 53 | } |
54 | 54 | newstr = append(newstr, chr) |
55 | 55 | } |
107 | 107 | vals, err = key.parseUint64s(strs, true, false) |
108 | 108 | case reflect.Float64: |
109 | 109 | vals, err = key.parseFloat64s(strs, true, false) |
110 | case reflect.Bool: | |
111 | vals, err = key.parseBools(strs, true, false) | |
110 | 112 | case reflectTime: |
111 | 113 | vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false) |
112 | 114 | default: |
131 | 133 | slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i])) |
132 | 134 | case reflect.Float64: |
133 | 135 | slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i])) |
136 | case reflect.Bool: | |
137 | slice.Index(i).Set(reflect.ValueOf(vals.([]bool)[i])) | |
134 | 138 | case reflectTime: |
135 | 139 | slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i])) |
136 | 140 | } |
148 | 152 | |
149 | 153 | // setWithProperType sets proper value to field based on its type, |
150 | 154 | // but it does not return error for failing parsing, |
151 | // because we want to use default value that is already assigned to strcut. | |
155 | // because we want to use default value that is already assigned to struct. | |
152 | 156 | func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { |
153 | switch t.Kind() { | |
157 | vt := t | |
158 | isPtr := t.Kind() == reflect.Ptr | |
159 | if isPtr { | |
160 | vt = t.Elem() | |
161 | } | |
162 | switch vt.Kind() { | |
154 | 163 | case reflect.String: |
155 | if len(key.String()) == 0 { | |
156 | return nil | |
157 | } | |
158 | field.SetString(key.String()) | |
164 | stringVal := key.String() | |
165 | if isPtr { | |
166 | field.Set(reflect.ValueOf(&stringVal)) | |
167 | } else if len(stringVal) > 0 { | |
168 | field.SetString(key.String()) | |
169 | } | |
159 | 170 | case reflect.Bool: |
160 | 171 | boolVal, err := key.Bool() |
161 | 172 | if err != nil { |
162 | 173 | return wrapStrictError(err, isStrict) |
163 | 174 | } |
164 | field.SetBool(boolVal) | |
175 | if isPtr { | |
176 | field.Set(reflect.ValueOf(&boolVal)) | |
177 | } else { | |
178 | field.SetBool(boolVal) | |
179 | } | |
165 | 180 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
166 | durationVal, err := key.Duration() | |
167 | // Skip zero value | |
168 | if err == nil && int64(durationVal) > 0 { | |
169 | field.Set(reflect.ValueOf(durationVal)) | |
181 | // ParseDuration will not return err for `0`, so check the type name | |
182 | if vt.Name() == "Duration" { | |
183 | durationVal, err := key.Duration() | |
184 | if err != nil { | |
185 | if intVal, err := key.Int64(); err == nil { | |
186 | field.SetInt(intVal) | |
187 | return nil | |
188 | } | |
189 | return wrapStrictError(err, isStrict) | |
190 | } | |
191 | if isPtr { | |
192 | field.Set(reflect.ValueOf(&durationVal)) | |
193 | } else if int64(durationVal) > 0 { | |
194 | field.Set(reflect.ValueOf(durationVal)) | |
195 | } | |
170 | 196 | return nil |
171 | 197 | } |
172 | 198 | |
174 | 200 | if err != nil { |
175 | 201 | return wrapStrictError(err, isStrict) |
176 | 202 | } |
177 | field.SetInt(intVal) | |
203 | if isPtr { | |
204 | pv := reflect.New(t.Elem()) | |
205 | pv.Elem().SetInt(intVal) | |
206 | field.Set(pv) | |
207 | } else { | |
208 | field.SetInt(intVal) | |
209 | } | |
178 | 210 | // byte is an alias for uint8, so supporting uint8 breaks support for byte |
179 | 211 | case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
180 | 212 | durationVal, err := key.Duration() |
181 | 213 | // Skip zero value |
182 | if err == nil && int(durationVal) > 0 { | |
183 | field.Set(reflect.ValueOf(durationVal)) | |
214 | if err == nil && uint64(durationVal) > 0 { | |
215 | if isPtr { | |
216 | field.Set(reflect.ValueOf(&durationVal)) | |
217 | } else { | |
218 | field.Set(reflect.ValueOf(durationVal)) | |
219 | } | |
184 | 220 | return nil |
185 | 221 | } |
186 | 222 | |
188 | 224 | if err != nil { |
189 | 225 | return wrapStrictError(err, isStrict) |
190 | 226 | } |
191 | field.SetUint(uintVal) | |
227 | if isPtr { | |
228 | pv := reflect.New(t.Elem()) | |
229 | pv.Elem().SetUint(uintVal) | |
230 | field.Set(pv) | |
231 | } else { | |
232 | field.SetUint(uintVal) | |
233 | } | |
192 | 234 | |
193 | 235 | case reflect.Float32, reflect.Float64: |
194 | 236 | floatVal, err := key.Float64() |
195 | 237 | if err != nil { |
196 | 238 | return wrapStrictError(err, isStrict) |
197 | 239 | } |
198 | field.SetFloat(floatVal) | |
240 | if isPtr { | |
241 | pv := reflect.New(t.Elem()) | |
242 | pv.Elem().SetFloat(floatVal) | |
243 | field.Set(pv) | |
244 | } else { | |
245 | field.SetFloat(floatVal) | |
246 | } | |
199 | 247 | case reflectTime: |
200 | 248 | timeVal, err := key.Time() |
201 | 249 | if err != nil { |
202 | 250 | return wrapStrictError(err, isStrict) |
203 | 251 | } |
204 | field.Set(reflect.ValueOf(timeVal)) | |
252 | if isPtr { | |
253 | field.Set(reflect.ValueOf(&timeVal)) | |
254 | } else { | |
255 | field.Set(reflect.ValueOf(timeVal)) | |
256 | } | |
205 | 257 | case reflect.Slice: |
206 | 258 | return setSliceWithProperType(key, field, delim, allowShadow, isStrict) |
207 | 259 | default: |
208 | return fmt.Errorf("unsupported type '%s'", t) | |
260 | return fmt.Errorf("unsupported type %q", t) | |
209 | 261 | } |
210 | 262 | return nil |
211 | 263 | } |
212 | 264 | |
213 | func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool) { | |
214 | opts := strings.SplitN(tag, ",", 3) | |
265 | func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool, allowNonUnique bool, extends bool) { | |
266 | opts := strings.SplitN(tag, ",", 5) | |
215 | 267 | rawName = opts[0] |
216 | if len(opts) > 1 { | |
217 | omitEmpty = opts[1] == "omitempty" | |
218 | } | |
219 | if len(opts) > 2 { | |
220 | allowShadow = opts[2] == "allowshadow" | |
221 | } | |
222 | return rawName, omitEmpty, allowShadow | |
223 | } | |
224 | ||
225 | func (s *Section) mapTo(val reflect.Value, isStrict bool) error { | |
268 | for _, opt := range opts[1:] { | |
269 | omitEmpty = omitEmpty || (opt == "omitempty") | |
270 | allowShadow = allowShadow || (opt == "allowshadow") | |
271 | allowNonUnique = allowNonUnique || (opt == "nonunique") | |
272 | extends = extends || (opt == "extends") | |
273 | } | |
274 | return rawName, omitEmpty, allowShadow, allowNonUnique, extends | |
275 | } | |
276 | ||
277 | // mapToField maps the given value to the matching field of the given section. | |
278 | // The sectionIndex is the index (if non unique sections are enabled) to which the value should be added. | |
279 | func (s *Section) mapToField(val reflect.Value, isStrict bool, sectionIndex int, sectionName string) error { | |
226 | 280 | if val.Kind() == reflect.Ptr { |
227 | 281 | val = val.Elem() |
228 | 282 | } |
237 | 291 | continue |
238 | 292 | } |
239 | 293 | |
240 | rawName, _, allowShadow := parseTagOptions(tag) | |
294 | rawName, _, allowShadow, allowNonUnique, extends := parseTagOptions(tag) | |
241 | 295 | fieldName := s.parseFieldName(tpField.Name, rawName) |
242 | 296 | if len(fieldName) == 0 || !field.CanSet() { |
243 | 297 | continue |
244 | 298 | } |
245 | 299 | |
246 | isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous | |
247 | 300 | isStruct := tpField.Type.Kind() == reflect.Struct |
248 | if isAnonymous { | |
301 | isStructPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct | |
302 | isAnonymousPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous | |
303 | if isAnonymousPtr { | |
249 | 304 | field.Set(reflect.New(tpField.Type.Elem())) |
250 | 305 | } |
251 | 306 | |
252 | if isAnonymous || isStruct { | |
253 | if sec, err := s.f.GetSection(fieldName); err == nil { | |
254 | if err = sec.mapTo(field, isStrict); err != nil { | |
255 | return fmt.Errorf("error mapping field(%s): %v", fieldName, err) | |
307 | if extends && (isAnonymousPtr || (isStruct && tpField.Anonymous)) { | |
308 | if isStructPtr && field.IsNil() { | |
309 | field.Set(reflect.New(tpField.Type.Elem())) | |
310 | } | |
311 | fieldSection := s | |
312 | if rawName != "" { | |
313 | sectionName = s.name + s.f.options.ChildSectionDelimiter + rawName | |
314 | if secs, err := s.f.SectionsByName(sectionName); err == nil && sectionIndex < len(secs) { | |
315 | fieldSection = secs[sectionIndex] | |
316 | } | |
317 | } | |
318 | if err := fieldSection.mapToField(field, isStrict, sectionIndex, sectionName); err != nil { | |
319 | return fmt.Errorf("map to field %q: %v", fieldName, err) | |
320 | } | |
321 | } else if isAnonymousPtr || isStruct || isStructPtr { | |
322 | if secs, err := s.f.SectionsByName(fieldName); err == nil { | |
323 | if len(secs) <= sectionIndex { | |
324 | return fmt.Errorf("there are not enough sections (%d <= %d) for the field %q", len(secs), sectionIndex, fieldName) | |
325 | } | |
326 | // Only set the field to non-nil struct value if we have a section for it. | |
327 | // Otherwise, we end up with a non-nil struct ptr even though there is no data. | |
328 | if isStructPtr && field.IsNil() { | |
329 | field.Set(reflect.New(tpField.Type.Elem())) | |
330 | } | |
331 | if err = secs[sectionIndex].mapToField(field, isStrict, sectionIndex, fieldName); err != nil { | |
332 | return fmt.Errorf("map to field %q: %v", fieldName, err) | |
256 | 333 | } |
257 | 334 | continue |
258 | 335 | } |
336 | } | |
337 | ||
338 | // Map non-unique sections | |
339 | if allowNonUnique && tpField.Type.Kind() == reflect.Slice { | |
340 | newField, err := s.mapToSlice(fieldName, field, isStrict) | |
341 | if err != nil { | |
342 | return fmt.Errorf("map to slice %q: %v", fieldName, err) | |
343 | } | |
344 | ||
345 | field.Set(newField) | |
346 | continue | |
259 | 347 | } |
260 | 348 | |
261 | 349 | if key, err := s.GetKey(fieldName); err == nil { |
262 | 350 | delim := parseDelim(tpField.Tag.Get("delim")) |
263 | 351 | if err = setWithProperType(tpField.Type, key, field, delim, allowShadow, isStrict); err != nil { |
264 | return fmt.Errorf("error mapping field(%s): %v", fieldName, err) | |
352 | return fmt.Errorf("set field %q: %v", fieldName, err) | |
265 | 353 | } |
266 | 354 | } |
267 | 355 | } |
268 | 356 | return nil |
269 | 357 | } |
270 | 358 | |
271 | // MapTo maps section to given struct. | |
272 | func (s *Section) MapTo(v interface{}) error { | |
359 | // mapToSlice maps all sections with the same name and returns the new value. | |
360 | // The type of the Value must be a slice. | |
361 | func (s *Section) mapToSlice(secName string, val reflect.Value, isStrict bool) (reflect.Value, error) { | |
362 | secs, err := s.f.SectionsByName(secName) | |
363 | if err != nil { | |
364 | return reflect.Value{}, err | |
365 | } | |
366 | ||
367 | typ := val.Type().Elem() | |
368 | for i, sec := range secs { | |
369 | elem := reflect.New(typ) | |
370 | if err = sec.mapToField(elem, isStrict, i, sec.name); err != nil { | |
371 | return reflect.Value{}, fmt.Errorf("map to field from section %q: %v", secName, err) | |
372 | } | |
373 | ||
374 | val = reflect.Append(val, elem.Elem()) | |
375 | } | |
376 | return val, nil | |
377 | } | |
378 | ||
379 | // mapTo maps a section to object v. | |
380 | func (s *Section) mapTo(v interface{}, isStrict bool) error { | |
273 | 381 | typ := reflect.TypeOf(v) |
274 | 382 | val := reflect.ValueOf(v) |
275 | 383 | if typ.Kind() == reflect.Ptr { |
276 | 384 | typ = typ.Elem() |
277 | 385 | val = val.Elem() |
278 | 386 | } else { |
279 | return errors.New("cannot map to non-pointer struct") | |
280 | } | |
281 | ||
282 | return s.mapTo(val, false) | |
283 | } | |
284 | ||
285 | // MapTo maps section to given struct in strict mode, | |
387 | return errors.New("not a pointer to a struct") | |
388 | } | |
389 | ||
390 | if typ.Kind() == reflect.Slice { | |
391 | newField, err := s.mapToSlice(s.name, val, isStrict) | |
392 | if err != nil { | |
393 | return err | |
394 | } | |
395 | ||
396 | val.Set(newField) | |
397 | return nil | |
398 | } | |
399 | ||
400 | return s.mapToField(val, isStrict, 0, s.name) | |
401 | } | |
402 | ||
403 | // MapTo maps section to given struct. | |
404 | func (s *Section) MapTo(v interface{}) error { | |
405 | return s.mapTo(v, false) | |
406 | } | |
407 | ||
408 | // StrictMapTo maps section to given struct in strict mode, | |
286 | 409 | // which returns all possible error including value parsing error. |
287 | 410 | func (s *Section) StrictMapTo(v interface{}) error { |
288 | typ := reflect.TypeOf(v) | |
289 | val := reflect.ValueOf(v) | |
290 | if typ.Kind() == reflect.Ptr { | |
291 | typ = typ.Elem() | |
292 | val = val.Elem() | |
293 | } else { | |
294 | return errors.New("cannot map to non-pointer struct") | |
295 | } | |
296 | ||
297 | return s.mapTo(val, true) | |
411 | return s.mapTo(v, true) | |
298 | 412 | } |
299 | 413 | |
300 | 414 | // MapTo maps file to given struct. |
302 | 416 | return f.Section("").MapTo(v) |
303 | 417 | } |
304 | 418 | |
305 | // MapTo maps file to given struct in strict mode, | |
419 | // StrictMapTo maps file to given struct in strict mode, | |
306 | 420 | // which returns all possible error including value parsing error. |
307 | 421 | func (f *File) StrictMapTo(v interface{}) error { |
308 | 422 | return f.Section("").StrictMapTo(v) |
309 | 423 | } |
310 | 424 | |
311 | // MapTo maps data sources to given struct with name mapper. | |
425 | // MapToWithMapper maps data sources to given struct with name mapper. | |
312 | 426 | func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { |
313 | 427 | cfg, err := Load(source, others...) |
314 | 428 | if err != nil { |
341 | 455 | } |
342 | 456 | |
343 | 457 | // reflectSliceWithProperType does the opposite thing as setSliceWithProperType. |
344 | func reflectSliceWithProperType(key *Key, field reflect.Value, delim string) error { | |
458 | func reflectSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow bool) error { | |
345 | 459 | slice := field.Slice(0, field.Len()) |
346 | 460 | if field.Len() == 0 { |
347 | 461 | return nil |
348 | 462 | } |
463 | sliceOf := field.Type().Elem().Kind() | |
464 | ||
465 | if allowShadow { | |
466 | var keyWithShadows *Key | |
467 | for i := 0; i < field.Len(); i++ { | |
468 | var val string | |
469 | switch sliceOf { | |
470 | case reflect.String: | |
471 | val = slice.Index(i).String() | |
472 | case reflect.Int, reflect.Int64: | |
473 | val = fmt.Sprint(slice.Index(i).Int()) | |
474 | case reflect.Uint, reflect.Uint64: | |
475 | val = fmt.Sprint(slice.Index(i).Uint()) | |
476 | case reflect.Float64: | |
477 | val = fmt.Sprint(slice.Index(i).Float()) | |
478 | case reflect.Bool: | |
479 | val = fmt.Sprint(slice.Index(i).Bool()) | |
480 | case reflectTime: | |
481 | val = slice.Index(i).Interface().(time.Time).Format(time.RFC3339) | |
482 | default: | |
483 | return fmt.Errorf("unsupported type '[]%s'", sliceOf) | |
484 | } | |
485 | ||
486 | if i == 0 { | |
487 | keyWithShadows = newKey(key.s, key.name, val) | |
488 | } else { | |
489 | _ = keyWithShadows.AddShadow(val) | |
490 | } | |
491 | } | |
492 | *key = *keyWithShadows | |
493 | return nil | |
494 | } | |
349 | 495 | |
350 | 496 | var buf bytes.Buffer |
351 | sliceOf := field.Type().Elem().Kind() | |
352 | 497 | for i := 0; i < field.Len(); i++ { |
353 | 498 | switch sliceOf { |
354 | 499 | case reflect.String: |
359 | 504 | buf.WriteString(fmt.Sprint(slice.Index(i).Uint())) |
360 | 505 | case reflect.Float64: |
361 | 506 | buf.WriteString(fmt.Sprint(slice.Index(i).Float())) |
507 | case reflect.Bool: | |
508 | buf.WriteString(fmt.Sprint(slice.Index(i).Bool())) | |
362 | 509 | case reflectTime: |
363 | 510 | buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339)) |
364 | 511 | default: |
366 | 513 | } |
367 | 514 | buf.WriteString(delim) |
368 | 515 | } |
369 | key.SetValue(buf.String()[:buf.Len()-1]) | |
516 | key.SetValue(buf.String()[:buf.Len()-len(delim)]) | |
370 | 517 | return nil |
371 | 518 | } |
372 | 519 | |
373 | 520 | // reflectWithProperType does the opposite thing as setWithProperType. |
374 | func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error { | |
521 | func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow bool) error { | |
375 | 522 | switch t.Kind() { |
376 | 523 | case reflect.String: |
377 | 524 | key.SetValue(field.String()) |
386 | 533 | case reflectTime: |
387 | 534 | key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339))) |
388 | 535 | case reflect.Slice: |
389 | return reflectSliceWithProperType(key, field, delim) | |
536 | return reflectSliceWithProperType(key, field, delim, allowShadow) | |
537 | case reflect.Ptr: | |
538 | if !field.IsNil() { | |
539 | return reflectWithProperType(t.Elem(), key, field.Elem(), delim, allowShadow) | |
540 | } | |
390 | 541 | default: |
391 | return fmt.Errorf("unsupported type '%s'", t) | |
542 | return fmt.Errorf("unsupported type %q", t) | |
392 | 543 | } |
393 | 544 | return nil |
394 | 545 | } |
416 | 567 | return false |
417 | 568 | } |
418 | 569 | |
570 | // StructReflector is the interface implemented by struct types that can extract themselves into INI objects. | |
571 | type StructReflector interface { | |
572 | ReflectINIStruct(*File) error | |
573 | } | |
574 | ||
419 | 575 | func (s *Section) reflectFrom(val reflect.Value) error { |
420 | 576 | if val.Kind() == reflect.Ptr { |
421 | 577 | val = val.Elem() |
423 | 579 | typ := val.Type() |
424 | 580 | |
425 | 581 | for i := 0; i < typ.NumField(); i++ { |
582 | if !val.Field(i).CanInterface() { | |
583 | continue | |
584 | } | |
585 | ||
426 | 586 | field := val.Field(i) |
427 | 587 | tpField := typ.Field(i) |
428 | 588 | |
431 | 591 | continue |
432 | 592 | } |
433 | 593 | |
434 | opts := strings.SplitN(tag, ",", 2) | |
435 | if len(opts) == 2 && opts[1] == "omitempty" && isEmptyValue(field) { | |
436 | continue | |
437 | } | |
438 | ||
439 | fieldName := s.parseFieldName(tpField.Name, opts[0]) | |
594 | rawName, omitEmpty, allowShadow, allowNonUnique, extends := parseTagOptions(tag) | |
595 | if omitEmpty && isEmptyValue(field) { | |
596 | continue | |
597 | } | |
598 | ||
599 | if r, ok := field.Interface().(StructReflector); ok { | |
600 | return r.ReflectINIStruct(s.f) | |
601 | } | |
602 | ||
603 | fieldName := s.parseFieldName(tpField.Name, rawName) | |
440 | 604 | if len(fieldName) == 0 || !field.CanSet() { |
441 | 605 | continue |
442 | 606 | } |
443 | 607 | |
444 | if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) || | |
608 | if extends && tpField.Anonymous && (tpField.Type.Kind() == reflect.Ptr || tpField.Type.Kind() == reflect.Struct) { | |
609 | if err := s.reflectFrom(field); err != nil { | |
610 | return fmt.Errorf("reflect from field %q: %v", fieldName, err) | |
611 | } | |
612 | continue | |
613 | } | |
614 | ||
615 | if (tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct) || | |
445 | 616 | (tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") { |
446 | 617 | // Note: The only error here is section doesn't exist. |
447 | 618 | sec, err := s.f.GetSection(fieldName) |
456 | 627 | } |
457 | 628 | |
458 | 629 | if err = sec.reflectFrom(field); err != nil { |
459 | return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) | |
460 | } | |
461 | continue | |
462 | } | |
463 | ||
464 | // Note: Same reason as secion. | |
630 | return fmt.Errorf("reflect from field %q: %v", fieldName, err) | |
631 | } | |
632 | continue | |
633 | } | |
634 | ||
635 | if allowNonUnique && tpField.Type.Kind() == reflect.Slice { | |
636 | slice := field.Slice(0, field.Len()) | |
637 | if field.Len() == 0 { | |
638 | return nil | |
639 | } | |
640 | sliceOf := field.Type().Elem().Kind() | |
641 | ||
642 | for i := 0; i < field.Len(); i++ { | |
643 | if sliceOf != reflect.Struct && sliceOf != reflect.Ptr { | |
644 | return fmt.Errorf("field %q is not a slice of pointer or struct", fieldName) | |
645 | } | |
646 | ||
647 | sec, err := s.f.NewSection(fieldName) | |
648 | if err != nil { | |
649 | return err | |
650 | } | |
651 | ||
652 | // Add comment from comment tag | |
653 | if len(sec.Comment) == 0 { | |
654 | sec.Comment = tpField.Tag.Get("comment") | |
655 | } | |
656 | ||
657 | if err := sec.reflectFrom(slice.Index(i)); err != nil { | |
658 | return fmt.Errorf("reflect from field %q: %v", fieldName, err) | |
659 | } | |
660 | } | |
661 | continue | |
662 | } | |
663 | ||
664 | // Note: Same reason as section. | |
465 | 665 | key, err := s.GetKey(fieldName) |
466 | 666 | if err != nil { |
467 | 667 | key, _ = s.NewKey(fieldName, "") |
472 | 672 | key.Comment = tpField.Tag.Get("comment") |
473 | 673 | } |
474 | 674 | |
475 | if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { | |
476 | return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) | |
675 | delim := parseDelim(tpField.Tag.Get("delim")) | |
676 | if err = reflectWithProperType(tpField.Type, key, field, delim, allowShadow); err != nil { | |
677 | return fmt.Errorf("reflect field %q: %v", fieldName, err) | |
477 | 678 | } |
478 | 679 | |
479 | 680 | } |
480 | 681 | return nil |
481 | 682 | } |
482 | 683 | |
483 | // ReflectFrom reflects secion from given struct. | |
684 | // ReflectFrom reflects section from given struct. It overwrites existing ones. | |
484 | 685 | func (s *Section) ReflectFrom(v interface{}) error { |
485 | 686 | typ := reflect.TypeOf(v) |
486 | 687 | val := reflect.ValueOf(v) |
688 | ||
689 | if s.name != DefaultSection && s.f.options.AllowNonUniqueSections && | |
690 | (typ.Kind() == reflect.Slice || typ.Kind() == reflect.Ptr) { | |
691 | // Clear sections to make sure none exists before adding the new ones | |
692 | s.f.DeleteSection(s.name) | |
693 | ||
694 | if typ.Kind() == reflect.Ptr { | |
695 | sec, err := s.f.NewSection(s.name) | |
696 | if err != nil { | |
697 | return err | |
698 | } | |
699 | return sec.reflectFrom(val.Elem()) | |
700 | } | |
701 | ||
702 | slice := val.Slice(0, val.Len()) | |
703 | sliceOf := val.Type().Elem().Kind() | |
704 | if sliceOf != reflect.Ptr { | |
705 | return fmt.Errorf("not a slice of pointers") | |
706 | } | |
707 | ||
708 | for i := 0; i < slice.Len(); i++ { | |
709 | sec, err := s.f.NewSection(s.name) | |
710 | if err != nil { | |
711 | return err | |
712 | } | |
713 | ||
714 | err = sec.reflectFrom(slice.Index(i)) | |
715 | if err != nil { | |
716 | return fmt.Errorf("reflect from %dth field: %v", i, err) | |
717 | } | |
718 | } | |
719 | ||
720 | return nil | |
721 | } | |
722 | ||
487 | 723 | if typ.Kind() == reflect.Ptr { |
488 | typ = typ.Elem() | |
489 | 724 | val = val.Elem() |
490 | 725 | } else { |
491 | return errors.New("cannot reflect from non-pointer struct") | |
726 | return errors.New("not a pointer to a struct") | |
492 | 727 | } |
493 | 728 | |
494 | 729 | return s.reflectFrom(val) |
499 | 734 | return f.Section("").ReflectFrom(v) |
500 | 735 | } |
501 | 736 | |
502 | // ReflectFrom reflects data sources from given struct with name mapper. | |
737 | // ReflectFromWithMapper reflects data sources from given struct with name mapper. | |
503 | 738 | func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error { |
504 | 739 | cfg.NameMapper = mapper |
505 | 740 | return cfg.ReflectFrom(v) |
21 | 21 | "time" |
22 | 22 | |
23 | 23 | . "github.com/smartystreets/goconvey/convey" |
24 | ||
24 | 25 | "gopkg.in/ini.v1" |
25 | 26 | ) |
26 | 27 | |
32 | 33 | Ages []uint |
33 | 34 | Populations []uint64 |
34 | 35 | Coordinates []float64 |
36 | Flags []bool | |
35 | 37 | Note string |
36 | 38 | Unused int `ini:"-"` |
37 | 39 | } |
38 | 40 | |
39 | type testEmbeded struct { | |
41 | type TestEmbeded struct { | |
40 | 42 | GPA float64 |
41 | 43 | } |
42 | 44 | |
43 | 45 | type testStruct struct { |
44 | Name string `ini:"NAME"` | |
45 | Age int | |
46 | Male bool | |
47 | Money float64 | |
48 | Born time.Time | |
49 | Time time.Duration `ini:"Duration"` | |
50 | Others testNested | |
51 | *testEmbeded `ini:"grade"` | |
52 | Unused int `ini:"-"` | |
53 | Unsigned uint | |
54 | Omitted bool `ini:"omitthis,omitempty"` | |
55 | Shadows []string `ini:",,allowshadow"` | |
56 | ShadowInts []int `ini:"Shadows,,allowshadow"` | |
57 | } | |
58 | ||
59 | const _CONF_DATA_STRUCT = ` | |
46 | Name string `ini:"NAME"` | |
47 | Age int | |
48 | Male bool | |
49 | Money float64 | |
50 | Born time.Time | |
51 | Time time.Duration `ini:"Duration"` | |
52 | OldVersionTime time.Duration | |
53 | Others testNested | |
54 | OthersPtr *testNested | |
55 | NilPtr *testNested | |
56 | *TestEmbeded `ini:"grade"` | |
57 | Unused int `ini:"-"` | |
58 | Unsigned uint | |
59 | Omitted bool `ini:"omitthis,omitempty"` | |
60 | Shadows []string `ini:",allowshadow"` | |
61 | ShadowInts []int `ini:"Shadows,allowshadow"` | |
62 | BoolPtr *bool | |
63 | BoolPtrNil *bool | |
64 | FloatPtr *float64 | |
65 | FloatPtrNil *float64 | |
66 | IntPtr *int | |
67 | IntPtrNil *int | |
68 | UintPtr *uint | |
69 | UintPtrNil *uint | |
70 | StringPtr *string | |
71 | StringPtrNil *string | |
72 | TimePtr *time.Time | |
73 | TimePtrNil *time.Time | |
74 | DurationPtr *time.Duration | |
75 | DurationPtrNil *time.Duration | |
76 | } | |
77 | ||
78 | type testInterface struct { | |
79 | Address string | |
80 | ListenPort int | |
81 | PrivateKey string | |
82 | } | |
83 | ||
84 | type testPeer struct { | |
85 | PublicKey string | |
86 | PresharedKey string | |
87 | AllowedIPs []string `delim:","` | |
88 | } | |
89 | ||
90 | type testNonUniqueSectionsStruct struct { | |
91 | Interface testInterface | |
92 | Peer []testPeer `ini:",nonunique"` | |
93 | } | |
94 | ||
95 | type BaseStruct struct { | |
96 | Base bool | |
97 | } | |
98 | ||
99 | type testExtend struct { | |
100 | BaseStruct `ini:",extends"` | |
101 | Extend bool | |
102 | } | |
103 | ||
104 | const confDataStruct = ` | |
60 | 105 | NAME = Unknwon |
61 | 106 | Age = 21 |
62 | 107 | Male = true |
63 | 108 | Money = 1.25 |
64 | 109 | Born = 1993-10-07T20:17:05Z |
65 | 110 | Duration = 2h45m |
111 | OldVersionTime = 30 | |
66 | 112 | Unsigned = 3 |
67 | 113 | omitthis = true |
68 | 114 | Shadows = 1, 2 |
69 | 115 | Shadows = 3, 4 |
116 | BoolPtr = false | |
117 | FloatPtr = 0 | |
118 | IntPtr = 0 | |
119 | UintPtr = 0 | |
120 | StringPtr = "" | |
121 | TimePtr = 0001-01-01T00:00:00Z | |
122 | DurationPtr = 0s | |
70 | 123 | |
71 | 124 | [Others] |
72 | 125 | Cities = HangZhou|Boston |
76 | 129 | Ages = 18,19 |
77 | 130 | Populations = 12345678,98765432 |
78 | 131 | Coordinates = 192.168,10.11 |
132 | Flags = true,false | |
133 | Note = Hello world! | |
134 | ||
135 | [OthersPtr] | |
136 | Cities = HangZhou|Boston | |
137 | Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z | |
138 | Years = 1993,1994 | |
139 | Numbers = 10010,10086 | |
140 | Ages = 18,19 | |
141 | Populations = 12345678,98765432 | |
142 | Coordinates = 192.168,10.11 | |
143 | Flags = true,false | |
79 | 144 | Note = Hello world! |
80 | 145 | |
81 | 146 | [grade] |
84 | 149 | [foo.bar] |
85 | 150 | Here = there |
86 | 151 | When = then |
152 | ||
153 | [extended] | |
154 | Base = true | |
155 | Extend = true | |
156 | ` | |
157 | ||
158 | const confNonUniqueSectionDataStruct = `[Interface] | |
159 | Address = 10.2.0.1/24 | |
160 | ListenPort = 34777 | |
161 | PrivateKey = privServerKey | |
162 | ||
163 | [Peer] | |
164 | PublicKey = pubClientKey | |
165 | PresharedKey = psKey | |
166 | AllowedIPs = 10.2.0.2/32,fd00:2::2/128 | |
167 | ||
168 | [Peer] | |
169 | PublicKey = pubClientKey2 | |
170 | PresharedKey = psKey2 | |
171 | AllowedIPs = 10.2.0.3/32,fd00:2::3/128 | |
172 | ||
87 | 173 | ` |
88 | 174 | |
89 | 175 | type unsupport struct { |
96 | 182 | } |
97 | 183 | } |
98 | 184 | |
99 | type unsupport3 struct { | |
185 | type Unsupport3 struct { | |
100 | 186 | Cities byte |
101 | 187 | } |
102 | 188 | |
103 | 189 | type unsupport4 struct { |
104 | *unsupport3 `ini:"Others"` | |
190 | *Unsupport3 `ini:"Others"` | |
105 | 191 | } |
106 | 192 | |
107 | 193 | type defaultValue struct { |
108 | Name string | |
109 | Age int | |
110 | Male bool | |
111 | Money float64 | |
112 | Born time.Time | |
113 | Cities []string | |
194 | Name string | |
195 | Age int | |
196 | Male bool | |
197 | Optional *bool | |
198 | Money float64 | |
199 | Born time.Time | |
200 | Cities []string | |
114 | 201 | } |
115 | 202 | |
116 | 203 | type fooBar struct { |
117 | 204 | Here, When string |
118 | 205 | } |
119 | 206 | |
120 | const _INVALID_DATA_CONF_STRUCT = ` | |
207 | const invalidDataConfStruct = ` | |
121 | 208 | Name = |
122 | 209 | Age = age |
123 | 210 | Male = 123 |
130 | 217 | Convey("Map to struct", t, func() { |
131 | 218 | Convey("Map file to struct", func() { |
132 | 219 | ts := new(testStruct) |
133 | So(ini.MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil) | |
220 | So(ini.MapTo(ts, []byte(confDataStruct)), ShouldBeNil) | |
134 | 221 | |
135 | 222 | So(ts.Name, ShouldEqual, "Unknwon") |
136 | 223 | So(ts.Age, ShouldEqual, 21) |
145 | 232 | dur, err := time.ParseDuration("2h45m") |
146 | 233 | So(err, ShouldBeNil) |
147 | 234 | So(ts.Time.Seconds(), ShouldEqual, dur.Seconds()) |
235 | ||
236 | So(ts.OldVersionTime*time.Second, ShouldEqual, 30*time.Second) | |
148 | 237 | |
149 | 238 | So(strings.Join(ts.Others.Cities, ","), ShouldEqual, "HangZhou,Boston") |
150 | 239 | So(ts.Others.Visits[0].String(), ShouldEqual, t.String()) |
153 | 242 | So(fmt.Sprint(ts.Others.Ages), ShouldEqual, "[18 19]") |
154 | 243 | So(fmt.Sprint(ts.Others.Populations), ShouldEqual, "[12345678 98765432]") |
155 | 244 | So(fmt.Sprint(ts.Others.Coordinates), ShouldEqual, "[192.168 10.11]") |
245 | So(fmt.Sprint(ts.Others.Flags), ShouldEqual, "[true false]") | |
156 | 246 | So(ts.Others.Note, ShouldEqual, "Hello world!") |
157 | So(ts.testEmbeded.GPA, ShouldEqual, 2.8) | |
247 | So(ts.TestEmbeded.GPA, ShouldEqual, 2.8) | |
248 | ||
249 | So(strings.Join(ts.OthersPtr.Cities, ","), ShouldEqual, "HangZhou,Boston") | |
250 | So(ts.OthersPtr.Visits[0].String(), ShouldEqual, t.String()) | |
251 | So(fmt.Sprint(ts.OthersPtr.Years), ShouldEqual, "[1993 1994]") | |
252 | So(fmt.Sprint(ts.OthersPtr.Numbers), ShouldEqual, "[10010 10086]") | |
253 | So(fmt.Sprint(ts.OthersPtr.Ages), ShouldEqual, "[18 19]") | |
254 | So(fmt.Sprint(ts.OthersPtr.Populations), ShouldEqual, "[12345678 98765432]") | |
255 | So(fmt.Sprint(ts.OthersPtr.Coordinates), ShouldEqual, "[192.168 10.11]") | |
256 | So(fmt.Sprint(ts.OthersPtr.Flags), ShouldEqual, "[true false]") | |
257 | So(ts.OthersPtr.Note, ShouldEqual, "Hello world!") | |
258 | ||
259 | So(ts.NilPtr, ShouldBeNil) | |
260 | ||
261 | So(*ts.BoolPtr, ShouldEqual, false) | |
262 | So(ts.BoolPtrNil, ShouldEqual, nil) | |
263 | So(*ts.FloatPtr, ShouldEqual, 0) | |
264 | So(ts.FloatPtrNil, ShouldEqual, nil) | |
265 | So(*ts.IntPtr, ShouldEqual, 0) | |
266 | So(ts.IntPtrNil, ShouldEqual, nil) | |
267 | So(*ts.UintPtr, ShouldEqual, 0) | |
268 | So(ts.UintPtrNil, ShouldEqual, nil) | |
269 | So(*ts.StringPtr, ShouldEqual, "") | |
270 | So(ts.StringPtrNil, ShouldEqual, nil) | |
271 | So(*ts.TimePtr, ShouldNotEqual, nil) | |
272 | So(ts.TimePtrNil, ShouldEqual, nil) | |
273 | So(*ts.DurationPtr, ShouldEqual, 0) | |
274 | So(ts.DurationPtrNil, ShouldEqual, nil) | |
158 | 275 | }) |
159 | 276 | |
160 | 277 | Convey("Map section to struct", func() { |
161 | 278 | foobar := new(fooBar) |
162 | f, err := ini.Load([]byte(_CONF_DATA_STRUCT)) | |
279 | f, err := ini.Load([]byte(confDataStruct)) | |
163 | 280 | So(err, ShouldBeNil) |
164 | 281 | |
165 | 282 | So(f.Section("foo.bar").MapTo(foobar), ShouldBeNil) |
168 | 285 | }) |
169 | 286 | |
170 | 287 | Convey("Map to non-pointer struct", func() { |
171 | f, err := ini.Load([]byte(_CONF_DATA_STRUCT)) | |
288 | f, err := ini.Load([]byte(confDataStruct)) | |
172 | 289 | So(err, ShouldBeNil) |
173 | 290 | So(f, ShouldNotBeNil) |
174 | 291 | |
176 | 293 | }) |
177 | 294 | |
178 | 295 | Convey("Map to unsupported type", func() { |
179 | f, err := ini.Load([]byte(_CONF_DATA_STRUCT)) | |
296 | f, err := ini.Load([]byte(confDataStruct)) | |
180 | 297 | So(err, ShouldBeNil) |
181 | 298 | So(f, ShouldNotBeNil) |
182 | 299 | |
193 | 310 | |
194 | 311 | Convey("Map to omitempty field", func() { |
195 | 312 | ts := new(testStruct) |
196 | So(ini.MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil) | |
313 | So(ini.MapTo(ts, []byte(confDataStruct)), ShouldBeNil) | |
197 | 314 | |
198 | 315 | So(ts.Omitted, ShouldEqual, true) |
199 | 316 | }) |
200 | 317 | |
201 | 318 | Convey("Map with shadows", func() { |
202 | f, err := ini.LoadSources(ini.LoadOptions{AllowShadows: true}, []byte(_CONF_DATA_STRUCT)) | |
319 | f, err := ini.LoadSources(ini.LoadOptions{AllowShadows: true}, []byte(confDataStruct)) | |
203 | 320 | So(err, ShouldBeNil) |
204 | 321 | ts := new(testStruct) |
205 | 322 | So(f.MapTo(ts), ShouldBeNil) |
213 | 330 | }) |
214 | 331 | |
215 | 332 | Convey("Map to wrong types and gain default values", func() { |
216 | f, err := ini.Load([]byte(_INVALID_DATA_CONF_STRUCT)) | |
333 | f, err := ini.Load([]byte(invalidDataConfStruct)) | |
217 | 334 | So(err, ShouldBeNil) |
218 | 335 | |
219 | 336 | t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z") |
220 | 337 | So(err, ShouldBeNil) |
221 | dv := &defaultValue{"Joe", 10, true, 1.25, t, []string{"HangZhou", "Boston"}} | |
338 | dv := &defaultValue{"Joe", 10, true, nil, 1.25, t, []string{"HangZhou", "Boston"}} | |
222 | 339 | So(f.MapTo(dv), ShouldBeNil) |
223 | 340 | So(dv.Name, ShouldEqual, "Joe") |
224 | 341 | So(dv.Age, ShouldEqual, 10) |
227 | 344 | So(dv.Born.String(), ShouldEqual, t.String()) |
228 | 345 | So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston") |
229 | 346 | }) |
347 | ||
348 | Convey("Map to extended base", func() { | |
349 | f, err := ini.Load([]byte(confDataStruct)) | |
350 | So(err, ShouldBeNil) | |
351 | So(f, ShouldNotBeNil) | |
352 | te := testExtend{} | |
353 | So(f.Section("extended").MapTo(&te), ShouldBeNil) | |
354 | So(te.Base, ShouldBeTrue) | |
355 | So(te.Extend, ShouldBeTrue) | |
356 | }) | |
230 | 357 | }) |
231 | 358 | |
232 | 359 | Convey("Map to struct in strict mode", t, func() { |
256 | 383 | |
257 | 384 | So(f.Section("").StrictMapTo(s), ShouldBeNil) |
258 | 385 | So(fmt.Sprint(s.Names), ShouldEqual, "[alice bruce]") |
386 | }) | |
387 | } | |
388 | ||
389 | func Test_MapToStructNonUniqueSections(t *testing.T) { | |
390 | Convey("Map to struct non unique", t, func() { | |
391 | Convey("Map file to struct non unique", func() { | |
392 | f, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct)) | |
393 | So(err, ShouldBeNil) | |
394 | ts := new(testNonUniqueSectionsStruct) | |
395 | ||
396 | So(f.MapTo(ts), ShouldBeNil) | |
397 | ||
398 | So(ts.Interface.Address, ShouldEqual, "10.2.0.1/24") | |
399 | So(ts.Interface.ListenPort, ShouldEqual, 34777) | |
400 | So(ts.Interface.PrivateKey, ShouldEqual, "privServerKey") | |
401 | ||
402 | So(ts.Peer[0].PublicKey, ShouldEqual, "pubClientKey") | |
403 | So(ts.Peer[0].PresharedKey, ShouldEqual, "psKey") | |
404 | So(ts.Peer[0].AllowedIPs[0], ShouldEqual, "10.2.0.2/32") | |
405 | So(ts.Peer[0].AllowedIPs[1], ShouldEqual, "fd00:2::2/128") | |
406 | ||
407 | So(ts.Peer[1].PublicKey, ShouldEqual, "pubClientKey2") | |
408 | So(ts.Peer[1].PresharedKey, ShouldEqual, "psKey2") | |
409 | So(ts.Peer[1].AllowedIPs[0], ShouldEqual, "10.2.0.3/32") | |
410 | So(ts.Peer[1].AllowedIPs[1], ShouldEqual, "fd00:2::3/128") | |
411 | }) | |
412 | ||
413 | Convey("Map non unique section to struct", func() { | |
414 | newPeer := new(testPeer) | |
415 | newPeerSlice := make([]testPeer, 0) | |
416 | ||
417 | f, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct)) | |
418 | So(err, ShouldBeNil) | |
419 | ||
420 | // try only first one | |
421 | So(f.Section("Peer").MapTo(newPeer), ShouldBeNil) | |
422 | So(newPeer.PublicKey, ShouldEqual, "pubClientKey") | |
423 | So(newPeer.PresharedKey, ShouldEqual, "psKey") | |
424 | So(newPeer.AllowedIPs[0], ShouldEqual, "10.2.0.2/32") | |
425 | So(newPeer.AllowedIPs[1], ShouldEqual, "fd00:2::2/128") | |
426 | ||
427 | // try all | |
428 | So(f.Section("Peer").MapTo(&newPeerSlice), ShouldBeNil) | |
429 | So(newPeerSlice[0].PublicKey, ShouldEqual, "pubClientKey") | |
430 | So(newPeerSlice[0].PresharedKey, ShouldEqual, "psKey") | |
431 | So(newPeerSlice[0].AllowedIPs[0], ShouldEqual, "10.2.0.2/32") | |
432 | So(newPeerSlice[0].AllowedIPs[1], ShouldEqual, "fd00:2::2/128") | |
433 | ||
434 | So(newPeerSlice[1].PublicKey, ShouldEqual, "pubClientKey2") | |
435 | So(newPeerSlice[1].PresharedKey, ShouldEqual, "psKey2") | |
436 | So(newPeerSlice[1].AllowedIPs[0], ShouldEqual, "10.2.0.3/32") | |
437 | So(newPeerSlice[1].AllowedIPs[1], ShouldEqual, "fd00:2::3/128") | |
438 | }) | |
439 | ||
440 | Convey("Map non unique sections with subsections to struct", func() { | |
441 | iniFile, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, strings.NewReader(` | |
442 | [Section] | |
443 | FieldInSubSection = 1 | |
444 | FieldInSubSection2 = 2 | |
445 | FieldInSection = 3 | |
446 | ||
447 | [Section] | |
448 | FieldInSubSection = 4 | |
449 | FieldInSubSection2 = 5 | |
450 | FieldInSection = 6 | |
451 | `)) | |
452 | So(err, ShouldBeNil) | |
453 | ||
454 | type SubSection struct { | |
455 | FieldInSubSection string `ini:"FieldInSubSection"` | |
456 | } | |
457 | type SubSection2 struct { | |
458 | FieldInSubSection2 string `ini:"FieldInSubSection2"` | |
459 | } | |
460 | ||
461 | type Section struct { | |
462 | SubSection `ini:"Section"` | |
463 | SubSection2 `ini:"Section"` | |
464 | FieldInSection string `ini:"FieldInSection"` | |
465 | } | |
466 | ||
467 | type File struct { | |
468 | Sections []Section `ini:"Section,nonunique"` | |
469 | } | |
470 | ||
471 | f := new(File) | |
472 | err = iniFile.MapTo(f) | |
473 | So(err, ShouldBeNil) | |
474 | ||
475 | So(f.Sections[0].FieldInSubSection, ShouldEqual, "1") | |
476 | So(f.Sections[0].FieldInSubSection2, ShouldEqual, "2") | |
477 | So(f.Sections[0].FieldInSection, ShouldEqual, "3") | |
478 | ||
479 | So(f.Sections[1].FieldInSubSection, ShouldEqual, "4") | |
480 | So(f.Sections[1].FieldInSubSection2, ShouldEqual, "5") | |
481 | So(f.Sections[1].FieldInSection, ShouldEqual, "6") | |
482 | }) | |
259 | 483 | }) |
260 | 484 | } |
261 | 485 | |
269 | 493 | Ages []uint |
270 | 494 | Populations []uint64 |
271 | 495 | Coordinates []float64 |
496 | Flags []bool | |
272 | 497 | None []int |
273 | 498 | } |
274 | 499 | type Author struct { |
275 | 500 | Name string `ini:"NAME"` |
276 | 501 | Male bool |
502 | Optional *bool | |
277 | 503 | Age int `comment:"Author's age"` |
278 | 504 | Height uint |
279 | 505 | GPA float64 |
280 | 506 | Date time.Time |
281 | 507 | NeverMind string `ini:"-"` |
508 | ignored string | |
282 | 509 | *Embeded `ini:"infos" comment:"Embeded section"` |
283 | 510 | } |
284 | 511 | |
285 | 512 | t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z") |
286 | 513 | So(err, ShouldBeNil) |
287 | a := &Author{"Unknwon", true, 21, 100, 2.8, t, "", | |
514 | a := &Author{"Unknwon", true, nil, 21, 100, 2.8, t, "", "ignored", | |
288 | 515 | &Embeded{ |
289 | 516 | []time.Time{t, t}, |
290 | 517 | []string{"HangZhou", "Boston"}, |
293 | 520 | []uint{18, 19}, |
294 | 521 | []uint64{12345678, 98765432}, |
295 | 522 | []float64{192.168, 10.11}, |
523 | []bool{true, false}, | |
296 | 524 | []int{}, |
297 | 525 | }} |
298 | 526 | cfg := ini.Empty() |
301 | 529 | var buf bytes.Buffer |
302 | 530 | _, err = cfg.WriteTo(&buf) |
303 | 531 | So(err, ShouldBeNil) |
304 | So(buf.String(), ShouldEqual, `NAME = Unknwon | |
305 | Male = true | |
532 | So(buf.String(), ShouldEqual, `NAME = Unknwon | |
533 | Male = true | |
534 | Optional = | |
306 | 535 | ; Author's age |
307 | Age = 21 | |
308 | Height = 100 | |
309 | GPA = 2.8 | |
310 | Date = 1993-10-07T20:17:05Z | |
536 | Age = 21 | |
537 | Height = 100 | |
538 | GPA = 2.8 | |
539 | Date = 1993-10-07T20:17:05Z | |
311 | 540 | |
312 | 541 | ; Embeded section |
313 | 542 | [infos] |
319 | 548 | Ages = 18,19 |
320 | 549 | Populations = 12345678,98765432 |
321 | 550 | Coordinates = 192.168,10.11 |
551 | Flags = true,false | |
322 | 552 | None = |
323 | 553 | |
324 | 554 | `) |
331 | 561 | cfg := ini.Empty() |
332 | 562 | type SpecialStruct struct { |
333 | 563 | FirstName string `ini:"first_name"` |
334 | LastName string `ini:"last_name"` | |
564 | LastName string `ini:"last_name,omitempty"` | |
335 | 565 | JustOmitMe string `ini:"omitempty"` |
336 | 566 | LastLogin time.Time `ini:"last_login,omitempty"` |
337 | 567 | LastLogin2 time.Time `ini:",omitempty"` |
338 | 568 | NotEmpty int `ini:"omitempty"` |
339 | } | |
340 | ||
341 | So(ini.ReflectFrom(cfg, &SpecialStruct{FirstName: "John", LastName: "Doe", NotEmpty: 9}), ShouldBeNil) | |
569 | Number int64 `ini:",omitempty"` | |
570 | Ages uint `ini:",omitempty"` | |
571 | Population uint64 `ini:",omitempty"` | |
572 | Coordinate float64 `ini:",omitempty"` | |
573 | Flag bool `ini:",omitempty"` | |
574 | Note *string `ini:",omitempty"` | |
575 | } | |
576 | special := &SpecialStruct{ | |
577 | FirstName: "John", | |
578 | LastName: "Doe", | |
579 | NotEmpty: 9, | |
580 | } | |
581 | ||
582 | So(ini.ReflectFrom(cfg, special), ShouldBeNil) | |
342 | 583 | |
343 | 584 | var buf bytes.Buffer |
344 | 585 | _, err = cfg.WriteTo(&buf) |
348 | 589 | |
349 | 590 | `) |
350 | 591 | }) |
592 | ||
593 | Convey("Reflect from struct with non-anonymous structure pointer", func() { | |
594 | cfg := ini.Empty() | |
595 | type Rpc struct { | |
596 | Enable bool `ini:"enable"` | |
597 | Type string `ini:"type"` | |
598 | Address string `ini:"addr"` | |
599 | Name string `ini:"name"` | |
600 | } | |
601 | type Cfg struct { | |
602 | Rpc *Rpc `ini:"rpc"` | |
603 | } | |
604 | ||
605 | config := &Cfg{ | |
606 | Rpc: &Rpc{ | |
607 | Enable: true, | |
608 | Type: "type", | |
609 | Address: "address", | |
610 | Name: "name", | |
611 | }, | |
612 | } | |
613 | So(cfg.ReflectFrom(config), ShouldBeNil) | |
614 | ||
615 | var buf bytes.Buffer | |
616 | _, err = cfg.WriteTo(&buf) | |
617 | So(buf.String(), ShouldEqual, `[rpc] | |
618 | enable = true | |
619 | type = type | |
620 | addr = address | |
621 | name = name | |
622 | ||
623 | `) | |
624 | }) | |
625 | }) | |
626 | } | |
627 | ||
628 | func Test_ReflectFromStructNonUniqueSections(t *testing.T) { | |
629 | Convey("Reflect from struct with non unique sections", t, func() { | |
630 | nonUnique := &testNonUniqueSectionsStruct{ | |
631 | Interface: testInterface{ | |
632 | Address: "10.2.0.1/24", | |
633 | ListenPort: 34777, | |
634 | PrivateKey: "privServerKey", | |
635 | }, | |
636 | Peer: []testPeer{ | |
637 | { | |
638 | PublicKey: "pubClientKey", | |
639 | PresharedKey: "psKey", | |
640 | AllowedIPs: []string{"10.2.0.2/32,fd00:2::2/128"}, | |
641 | }, | |
642 | { | |
643 | PublicKey: "pubClientKey2", | |
644 | PresharedKey: "psKey2", | |
645 | AllowedIPs: []string{"10.2.0.3/32,fd00:2::3/128"}, | |
646 | }, | |
647 | }, | |
648 | } | |
649 | ||
650 | cfg := ini.Empty(ini.LoadOptions{ | |
651 | AllowNonUniqueSections: true, | |
652 | }) | |
653 | ||
654 | So(ini.ReflectFrom(cfg, nonUnique), ShouldBeNil) | |
655 | ||
656 | var buf bytes.Buffer | |
657 | _, err := cfg.WriteTo(&buf) | |
658 | So(err, ShouldBeNil) | |
659 | So(buf.String(), ShouldEqual, confNonUniqueSectionDataStruct) | |
660 | ||
661 | // note: using ReflectFrom from should overwrite the existing sections | |
662 | err = cfg.Section("Peer").ReflectFrom([]*testPeer{ | |
663 | { | |
664 | PublicKey: "pubClientKey3", | |
665 | PresharedKey: "psKey3", | |
666 | AllowedIPs: []string{"10.2.0.4/32,fd00:2::4/128"}, | |
667 | }, | |
668 | { | |
669 | PublicKey: "pubClientKey4", | |
670 | PresharedKey: "psKey4", | |
671 | AllowedIPs: []string{"10.2.0.5/32,fd00:2::5/128"}, | |
672 | }, | |
673 | }) | |
674 | ||
675 | So(err, ShouldBeNil) | |
676 | ||
677 | buf = bytes.Buffer{} | |
678 | _, err = cfg.WriteTo(&buf) | |
679 | So(err, ShouldBeNil) | |
680 | So(buf.String(), ShouldEqual, `[Interface] | |
681 | Address = 10.2.0.1/24 | |
682 | ListenPort = 34777 | |
683 | PrivateKey = privServerKey | |
684 | ||
685 | [Peer] | |
686 | PublicKey = pubClientKey3 | |
687 | PresharedKey = psKey3 | |
688 | AllowedIPs = 10.2.0.4/32,fd00:2::4/128 | |
689 | ||
690 | [Peer] | |
691 | PublicKey = pubClientKey4 | |
692 | PresharedKey = psKey4 | |
693 | AllowedIPs = 10.2.0.5/32,fd00:2::5/128 | |
694 | ||
695 | `) | |
696 | ||
697 | // note: using ReflectFrom from should overwrite the existing sections | |
698 | err = cfg.Section("Peer").ReflectFrom(&testPeer{ | |
699 | PublicKey: "pubClientKey5", | |
700 | PresharedKey: "psKey5", | |
701 | AllowedIPs: []string{"10.2.0.6/32,fd00:2::6/128"}, | |
702 | }) | |
703 | ||
704 | So(err, ShouldBeNil) | |
705 | ||
706 | buf = bytes.Buffer{} | |
707 | _, err = cfg.WriteTo(&buf) | |
708 | So(err, ShouldBeNil) | |
709 | So(buf.String(), ShouldEqual, `[Interface] | |
710 | Address = 10.2.0.1/24 | |
711 | ListenPort = 34777 | |
712 | PrivateKey = privServerKey | |
713 | ||
714 | [Peer] | |
715 | PublicKey = pubClientKey5 | |
716 | PresharedKey = psKey5 | |
717 | AllowedIPs = 10.2.0.6/32,fd00:2::6/128 | |
718 | ||
719 | `) | |
720 | }) | |
721 | } | |
722 | ||
723 | // Inspired by https://github.com/go-ini/ini/issues/196 | |
724 | func TestMapToAndReflectFromStructWithShadows(t *testing.T) { | |
725 | Convey("Map to struct and then reflect with shadows should generate original config content", t, func() { | |
726 | type include struct { | |
727 | Paths []string `ini:"path,omitempty,allowshadow"` | |
728 | } | |
729 | ||
730 | cfg, err := ini.LoadSources(ini.LoadOptions{ | |
731 | AllowShadows: true, | |
732 | }, []byte(` | |
733 | [include] | |
734 | path = /tmp/gpm-profiles/test5.profile | |
735 | path = /tmp/gpm-profiles/test1.profile`)) | |
736 | So(err, ShouldBeNil) | |
737 | ||
738 | sec := cfg.Section("include") | |
739 | inc := new(include) | |
740 | err = sec.MapTo(inc) | |
741 | So(err, ShouldBeNil) | |
742 | ||
743 | err = sec.ReflectFrom(inc) | |
744 | So(err, ShouldBeNil) | |
745 | ||
746 | var buf bytes.Buffer | |
747 | _, err = cfg.WriteTo(&buf) | |
748 | So(err, ShouldBeNil) | |
749 | So(buf.String(), ShouldEqual, `[include] | |
750 | path = /tmp/gpm-profiles/test5.profile | |
751 | path = /tmp/gpm-profiles/test1.profile | |
752 | ||
753 | `) | |
754 | ||
755 | Convey("Reflect from struct with shadows", func() { | |
756 | cfg := ini.Empty(ini.LoadOptions{ | |
757 | AllowShadows: true, | |
758 | }) | |
759 | type ShadowStruct struct { | |
760 | StringArray []string `ini:"sa,allowshadow"` | |
761 | EmptyStringArrat []string `ini:"empty,omitempty,allowshadow"` | |
762 | Allowshadow []string `ini:"allowshadow,allowshadow"` | |
763 | Dates []time.Time `ini:",allowshadow"` | |
764 | Places []string `ini:",allowshadow"` | |
765 | Years []int `ini:",allowshadow"` | |
766 | Numbers []int64 `ini:",allowshadow"` | |
767 | Ages []uint `ini:",allowshadow"` | |
768 | Populations []uint64 `ini:",allowshadow"` | |
769 | Coordinates []float64 `ini:",allowshadow"` | |
770 | Flags []bool `ini:",allowshadow"` | |
771 | None []int `ini:",allowshadow"` | |
772 | } | |
773 | ||
774 | shadow := &ShadowStruct{ | |
775 | StringArray: []string{"s1", "s2"}, | |
776 | Allowshadow: []string{"s3", "s4"}, | |
777 | Dates: []time.Time{time.Date(2020, 9, 12, 00, 00, 00, 651387237, time.UTC), | |
778 | time.Date(2020, 9, 12, 00, 00, 00, 651387237, time.UTC)}, | |
779 | Places: []string{"HangZhou", "Boston"}, | |
780 | Years: []int{1993, 1994}, | |
781 | Numbers: []int64{10010, 10086}, | |
782 | Ages: []uint{18, 19}, | |
783 | Populations: []uint64{12345678, 98765432}, | |
784 | Coordinates: []float64{192.168, 10.11}, | |
785 | Flags: []bool{true, false}, | |
786 | None: []int{}, | |
787 | } | |
788 | ||
789 | So(ini.ReflectFrom(cfg, shadow), ShouldBeNil) | |
790 | ||
791 | var buf bytes.Buffer | |
792 | _, err := cfg.WriteTo(&buf) | |
793 | So(err, ShouldBeNil) | |
794 | So(buf.String(), ShouldEqual, `sa = s1 | |
795 | sa = s2 | |
796 | allowshadow = s3 | |
797 | allowshadow = s4 | |
798 | Dates = 2020-09-12T00:00:00Z | |
799 | Places = HangZhou | |
800 | Places = Boston | |
801 | Years = 1993 | |
802 | Years = 1994 | |
803 | Numbers = 10010 | |
804 | Numbers = 10086 | |
805 | Ages = 18 | |
806 | Ages = 19 | |
807 | Populations = 12345678 | |
808 | Populations = 98765432 | |
809 | Coordinates = 192.168 | |
810 | Coordinates = 10.11 | |
811 | Flags = true | |
812 | Flags = false | |
813 | None = | |
814 | ||
815 | `) | |
816 | }) | |
351 | 817 | }) |
352 | 818 | } |
353 | 819 | |
363 | 829 | So(err, ShouldBeNil) |
364 | 830 | So(cfg, ShouldNotBeNil) |
365 | 831 | |
366 | cfg.NameMapper = ini.AllCapsUnderscore | |
832 | cfg.NameMapper = ini.SnackCase | |
367 | 833 | tg := new(testMapper) |
368 | 834 | So(cfg.MapTo(tg), ShouldBeNil) |
369 | 835 | So(tg.PackageName, ShouldEqual, "ini") |
384 | 850 | So(ds.Duration.Seconds(), ShouldEqual, dur.Seconds()) |
385 | 851 | }) |
386 | 852 | } |
853 | ||
854 | type Employer struct { | |
855 | Name string | |
856 | Title string | |
857 | } | |
858 | ||
859 | type Employers []*Employer | |
860 | ||
861 | func (es Employers) ReflectINIStruct(f *ini.File) error { | |
862 | for _, e := range es { | |
863 | f.Section(e.Name).Key("Title").SetValue(e.Title) | |
864 | } | |
865 | return nil | |
866 | } | |
867 | ||
868 | // Inspired by https://github.com/go-ini/ini/issues/199 | |
869 | func Test_StructReflector(t *testing.T) { | |
870 | Convey("Reflect with StructReflector interface", t, func() { | |
871 | p := &struct { | |
872 | FirstName string | |
873 | Employer Employers | |
874 | }{ | |
875 | FirstName: "Andrew", | |
876 | Employer: []*Employer{ | |
877 | { | |
878 | Name: `Employer "VMware"`, | |
879 | Title: "Staff II Engineer", | |
880 | }, | |
881 | { | |
882 | Name: `Employer "EMC"`, | |
883 | Title: "Consultant Engineer", | |
884 | }, | |
885 | }, | |
886 | } | |
887 | ||
888 | f := ini.Empty() | |
889 | So(f.ReflectFrom(p), ShouldBeNil) | |
890 | ||
891 | var buf bytes.Buffer | |
892 | _, err := f.WriteTo(&buf) | |
893 | So(err, ShouldBeNil) | |
894 | ||
895 | So(buf.String(), ShouldEqual, `FirstName = Andrew | |
896 | ||
897 | [Employer "VMware"] | |
898 | Title = Staff II Engineer | |
899 | ||
900 | [Employer "EMC"] | |
901 | Title = Consultant Engineer | |
902 | ||
903 | `) | |
904 | }) | |
905 | } |
0 | ; Package name | |
1 | NAME = ini | |
2 | ; Package version | |
3 | VERSION = v1 | |
4 | ; Package import path | |
5 | IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s | |
6 | ||
7 | ; Information about package author | |
8 | # Bio can be written in multiple lines. | |
9 | [author] | |
10 | ; This is author name | |
11 | NAME = Unknwon | |
12 | E-MAIL = u@gogs.io | |
13 | GITHUB = https://github.com/%(NAME)s | |
14 | # Succeeding comment | |
15 | BIO = """Gopher. | |
16 | Coding addict. | |
17 | Good man. | |
18 | """ | |
19 | ||
20 | [package] | |
21 | CLONE_URL = https://%(IMPORT_PATH)s | |
22 | ||
23 | [package.sub] | |
24 | UNUSED_KEY = should be deleted | |
25 | ||
26 | [features] | |
27 | - = Support read/write comments of keys and sections | |
28 | - = Support auto-increment of key names | |
29 | - = Support load multiple files to overwrite key values | |
30 | ||
31 | [types] | |
32 | STRING = str | |
33 | BOOL = true | |
34 | BOOL_FALSE = false | |
35 | FLOAT64 = 1.25 | |
36 | INT = 10 | |
37 | TIME = 2015-01-01T20:17:05Z | |
38 | DURATION = 2h45m | |
39 | UINT = 3 | |
40 | HEX_NUMBER = 0x3000 | |
41 | ||
42 | [array] | |
43 | STRINGS = en, zh, de | |
44 | FLOAT64S = 1.1, 2.2, 3.3 | |
45 | INTS = 1, 2, 3 | |
46 | UINTS = 1, 2, 3 | |
47 | TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z | |
48 | BOOLS = true, false, false | |
49 | ||
50 | [note] | |
51 | empty_lines = next line is empty | |
52 | boolean_key | |
53 | more = notes | |
54 | ||
55 | ; Comment before the section | |
56 | ; This is a comment for the section too | |
57 | [comments] | |
58 | ; Comment before key | |
59 | key = value | |
60 | ; This is a comment for key2 | |
61 | key2 = value2 | |
62 | key3 = "one", "two", "three" | |
63 | ||
64 | [string escapes] | |
65 | key1 = value1, value2, value3 | |
66 | key2 = value1\, value2 | |
67 | key3 = val\ue1, value2 | |
68 | key4 = value1\\, value\\\\2 | |
69 | key5 = value1\,, value2 | |
70 | key6 = aaa bbb\ and\ space ccc | |
71 | ||
72 | [advance] | |
73 | value with quotes = some value | |
74 | value quote2 again = some value | |
75 | includes comment sign = `my#password` | |
76 | includes comment sign2 = `my;password` | |
77 | true = 2+3=5 | |
78 | `1+1=2` = true | |
79 | `6+1=7` = true | |
80 | """`5+5`""" = 10 | |
81 | `"6+6"` = 12 | |
82 | `7-2=4` = false | |
83 | ADDRESS = """404 road, | |
84 | NotFound, State, 50000""" | |
85 | two_lines = how about continuation lines? | |
86 | lots_of_lines = "1 2 3 4 " | |
87 |
Binary diff not shown
Binary diff not shown
35 | 35 | TIME = 2015-01-01T20:17:05Z |
36 | 36 | DURATION = 2h45m |
37 | 37 | UINT = 3 |
38 | HEX_NUMBER = 0x3000 | |
38 | 39 | |
39 | 40 | [array] |
40 | 41 | STRINGS = en, zh, de |
42 | 43 | INTS = 1, 2, 3 |
43 | 44 | UINTS = 1, 2, 3 |
44 | 45 | TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z |
46 | BOOLS = true, false, false | |
45 | 47 | |
46 | 48 | [note] |
47 | 49 | empty_lines = next line is empty\ |
0 | value1 = some text here | |
1 | some more text here | |
2 | ||
3 | there is an empty line above and below | |
4 | ||
5 | ||
6 | value2 = there is an empty line above | |
7 | that is not indented so it should not be part | |
8 | of the value | |
9 | ||
10 | value3 = . | |
11 | ||
12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eu consequat ac felis donec et odio pellentesque diam volutpat. Mauris commodo quis imperdiet massa tincidunt nunc. Interdum velit euismod in pellentesque. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Nascetur ridiculus mus mauris vitae. Posuere urna nec tincidunt praesent semper feugiat. Lorem donec massa sapien faucibus et molestie ac feugiat sed. Ipsum dolor sit amet consectetur adipiscing elit. Enim sed faucibus turpis in eu mi. A diam sollicitudin tempor id. Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit. | |
13 | ||
14 | Lectus sit amet est placerat in egestas. At risus viverra adipiscing at in tellus integer. Tristique senectus et netus et malesuada fames ac. In hac habitasse platea dictumst. Purus in mollis nunc sed. Pellentesque sit amet porttitor eget dolor morbi. Elit at imperdiet dui accumsan sit amet nulla. Cursus in hac habitasse platea dictumst. Bibendum arcu vitae elementum curabitur. Faucibus ornare suspendisse sed nisi lacus. In vitae turpis massa sed. Libero nunc consequat interdum varius sit amet. Molestie a iaculis at erat pellentesque. | |
15 | ||
16 | Dui faucibus in ornare quam viverra orci sagittis eu. Purus in mollis nunc sed id semper. Sed arcu non odio euismod lacinia at. Quis commodo odio aenean sed adipiscing diam donec. Quisque id diam vel quam elementum pulvinar. Lorem ipsum dolor sit amet. Purus ut faucibus pulvinar elementum integer enim neque volutpat ac. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh sed. Gravida rutrum quisque non tellus orci. Ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant. Et sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Eget gravida cum sociis natoque penatibus et magnis. Elementum eu facilisis sed odio morbi quis commodo. Mollis nunc sed id semper risus in hendrerit gravida rutrum. Lorem dolor sed viverra ipsum. | |
17 | ||
18 | Pellentesque adipiscing commodo elit at imperdiet dui accumsan sit amet. Justo eget magna fermentum iaculis eu non diam. Condimentum mattis pellentesque id nibh tortor id aliquet lectus. Tellus molestie nunc non blandit massa enim. Mauris ultrices eros in cursus turpis. Purus viverra accumsan in nisl nisi scelerisque. Quis lectus nulla at volutpat. Purus ut faucibus pulvinar elementum integer enim. In pellentesque massa placerat duis ultricies lacus sed turpis. Elit sed vulputate mi sit amet mauris commodo. Tellus elementum sagittis vitae et. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi nullam. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Libero id faucibus nisl tincidunt eget nullam. Mattis aliquam faucibus purus in massa tempor. Fames ac turpis egestas sed tempus urna. Gravida in fermentum et sollicitudin ac orci phasellus egestas. | |
19 | ||
20 | Blandit turpis cursus in hac habitasse. Sed id semper risus in. Amet porttitor eget dolor morbi non arcu. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Ut morbi tincidunt augue interdum velit. Lorem mollis aliquam ut porttitor leo a. Nunc eget lorem dolor sed viverra. Scelerisque mauris pellentesque pulvinar pellentesque. Elit at imperdiet dui accumsan sit amet. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Laoreet non curabitur gravida arcu ac tortor dignissim. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus. Lacus sed viverra tellus in hac habitasse platea dictumst vestibulum. Viverra adipiscing at in tellus. Duis at tellus at urna condimentum. Eget gravida cum sociis natoque penatibus et magnis dis parturient. Pharetra massa massa ultricies mi quis hendrerit. | |
21 | ||
22 | Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Maecenas volutpat blandit aliquam etiam. Sed turpis tincidunt id aliquet. Eget duis at tellus at urna condimentum. Pellentesque habitant morbi tristique senectus et. Amet aliquam id diam maecenas. Volutpat est velit egestas dui id. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel orci. Massa sed elementum tempus egestas sed sed risus pretium. Quam quisque id diam vel quam elementum pulvinar etiam non. Sapien faucibus et molestie ac. Ipsum dolor sit amet consectetur adipiscing. Viverra orci sagittis eu volutpat. Leo urna molestie at elementum. Commodo viverra maecenas accumsan lacus. Non sodales neque sodales ut etiam sit amet. Habitant morbi tristique senectus et netus et malesuada fames. Habitant morbi tristique senectus et netus et malesuada. Blandit aliquam etiam erat velit scelerisque in. Varius duis at consectetur lorem donec massa sapien faucibus et. | |
23 | ||
24 | Augue mauris augue neque gravida in. Odio ut sem nulla pharetra diam sit amet nisl suscipit. Nulla aliquet enim tortor at auctor urna nunc id. Morbi tristique senectus et netus et malesuada fames ac. Quam id leo in vitae turpis massa sed elementum tempus. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus nullam. Maecenas volutpat blandit aliquam etiam erat velit scelerisque in. Sagittis nisl rhoncus mattis rhoncus urna neque viverra justo. Massa tempor nec feugiat nisl pretium. Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum. Enim lobortis scelerisque fermentum dui faucibus in ornare. Faucibus ornare suspendisse sed nisi lacus. Morbi tristique senectus et netus et malesuada fames. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Dictum non consectetur a erat nam at. Leo urna molestie at elementum eu facilisis sed odio morbi. Quam id leo in vitae turpis massa. Neque egestas congue quisque egestas diam in arcu. Varius morbi enim nunc faucibus a pellentesque sit. Aliquet enim tortor at auctor urna. | |
25 | ||
26 | Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Luctus accumsan tortor posuere ac. Eu ultrices vitae auctor eu augue ut lectus arcu bibendum. Pretium nibh ipsum consequat nisl vel pretium lectus. Aliquam etiam erat velit scelerisque in dictum. Sem et tortor consequat id porta nibh venenatis cras sed. A scelerisque purus semper eget duis at tellus at urna. At auctor urna nunc id. Ornare quam viverra orci sagittis eu volutpat odio. Nisl purus in mollis nunc sed id semper. Ornare suspendisse sed nisi lacus sed. Consectetur lorem donec massa sapien faucibus et. Ipsum dolor sit amet consectetur adipiscing elit ut. Porta nibh venenatis cras sed. Dignissim diam quis enim lobortis scelerisque. Quam nulla porttitor massa id. Tellus molestie nunc non blandit massa. | |
27 | ||
28 | Malesuada fames ac turpis egestas. Suscipit tellus mauris a diam maecenas. Turpis in eu mi bibendum neque egestas. Venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Quis imperdiet massa tincidunt nunc pulvinar sapien et. Urna duis convallis convallis tellus id. Velit egestas dui id ornare arcu odio. Consectetur purus ut faucibus pulvinar elementum integer enim neque. Aenean sed adipiscing diam donec adipiscing tristique. Tortor aliquam nulla facilisi cras fermentum odio eu. Diam in arcu cursus euismod quis viverra nibh cras. | |
29 | ||
30 | Id ornare arcu odio ut sem. Arcu dictum varius duis at consectetur lorem donec massa sapien. Proin libero nunc consequat interdum varius sit. Ut eu sem integer vitae justo. Vitae elementum curabitur vitae nunc. Diam quam nulla porttitor massa. Lectus mauris ultrices eros in cursus turpis massa tincidunt dui. Natoque penatibus et magnis dis parturient montes. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Libero nunc consequat interdum varius sit. Rhoncus dolor purus non enim praesent. Pellentesque sit amet porttitor eget. Nibh tortor id aliquet lectus proin nibh. Fermentum iaculis eu non diam phasellus vestibulum lorem sed. | |
31 | ||
32 | Eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus. Habitant morbi tristique senectus et netus et malesuada fames ac. Urna condimentum mattis pellentesque id. Lorem sed risus ultricies tristique nulla aliquet enim tortor at. Ipsum dolor sit amet consectetur adipiscing elit. Convallis a cras semper auctor neque vitae tempus quam. A diam sollicitudin tempor id eu nisl nunc mi ipsum. Maecenas sed enim ut sem viverra aliquet eget. Massa enim nec dui nunc mattis enim. Nam aliquam sem et tortor consequat. Adipiscing commodo elit at imperdiet dui accumsan sit amet nulla. Nullam eget felis eget nunc lobortis. Mauris a diam maecenas sed enim ut sem viverra. Ornare massa eget egestas purus. In hac habitasse platea dictumst. Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Nisl nunc mi ipsum faucibus. At varius vel pharetra vel. Mauris ultrices eros in cursus turpis massa tincidunt. |