Codebase list golang-github-go-ini-ini / 6a94948
New upstream release. Debian Janitor 2 years ago
37 changed file(s) with 3661 addition(s) and 2295 deletion(s). Raw diff Collapse all Expand all
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
-5
.github/ISSUE_TEMPLATE.md less more
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
-15
.travis.yml less more
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
55 go test -v -cover -race
66
77 bench:
8 go test -v -cover -race -test.bench=. -test.benchmem
8 go test -v -cover -test.bench=. -test.benchmem
99
1010 vet:
1111 go vet
1212
1313 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)
26
37 ![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200)
48
59 Package ini provides INI file read and write functionality in Go.
610
7 [简体中文](README_ZH.md)
11 ## Features
812
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.
1214 - Read with recursion values.
1315 - Read with parent-child sections.
1416 - Read with auto-increment key names.
2123
2224 ## Installation
2325
24 To use a tagged revision:
26 The minimum requirement of Go is **1.6**.
2527
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 ```
3131
3232 Please add `-u` flag to update in the future.
3333
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
74334 ## Getting Help
74435
36 - [Getting Started](https://ini.unknwon.io/docs/intro/getting_started)
74537 - [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
75939
76040 ## License
76141
+0
-750
README_ZH.md less more
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` 来进行版本化发布。(其实真相是导入路径更短了)
2020 )
2121
2222 func newTestFile(block bool) *ini.File {
23 c, _ := ini.Load([]byte(_CONF_DATA))
23 c, _ := ini.Load([]byte(confData))
2424 c.BlockMode = block
2525 return c
2626 }
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
06 golang-github-go-ini-ini (1.32.0-2) unstable; urgency=medium
17
28 * 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 )
1717 "fmt"
1818 )
1919
20 // ErrDelimiterNotFound indicates the error type of no delimiter is found which there should be one.
2021 type ErrDelimiterNotFound struct {
2122 Line string
2223 }
2324
25 // IsErrDelimiterNotFound returns true if the given error is an instance of ErrDelimiterNotFound.
2426 func IsErrDelimiterNotFound(err error) bool {
2527 _, ok := err.(ErrDelimiterNotFound)
2628 return ok
2424 "sync"
2525 )
2626
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.
2828 type File struct {
2929 options LoadOptions
3030 dataSources []dataSource
3535
3636 // To keep data in order.
3737 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
3842 // Actual data is stored here.
39 sections map[string]*Section
43 sections map[string][]*Section
4044
4145 NameMapper
4246 ValueMapper
4448
4549 // newFile initializes File object with given data sources.
4650 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
4761 return &File{
4862 BlockMode: true,
4963 dataSources: dataSources,
50 sections: make(map[string]*Section),
51 sectionList: make([]string, 0, 10),
64 sections: make(map[string][]*Section),
5265 options: opts,
5366 }
5467 }
5568
5669 // 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(""))
6078 return f
6179 }
6280
6381 // NewSection creates a new section.
6482 func (f *File) NewSection(name string) (*Section, error) {
6583 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 {
6888 name = strings.ToLower(name)
6989 }
7090
7393 defer f.lock.Unlock()
7494 }
7595
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
7898 }
7999
80100 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
83110 }
84111
85112 // NewRawSection creates a new section with an unparseable body.
106133
107134 // GetSection returns section by given name.
108135 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) {
109146 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 {
113150 name = strings.ToLower(name)
114151 }
115152
118155 defer f.lock.RUnlock()
119156 }
120157
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
126164 }
127165
128166 // Section assumes named section exists and returns a zero-value when not.
137175 return sec
138176 }
139177
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.
141192 func (f *File) Sections() []*Section {
193 if f.BlockMode {
194 f.lock.RLock()
195 defer f.lock.RUnlock()
196 }
197
142198 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]]
145201 }
146202 return sections
147203 }
158214 return list
159215 }
160216
161 // DeleteSection deletes a section.
217 // DeleteSection deletes a section or all sections with given name.
162218 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
163245 if f.BlockMode {
164246 f.lock.Lock()
165247 defer f.lock.Unlock()
166248 }
167249
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
174269 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
179281 }
180282
181283 func (f *File) reload(s dataSource) error {
194296 if err = f.reload(s); err != nil {
195297 // In loose mode, we create an empty default section for nonexistent files.
196298 if os.IsNotExist(err) && f.options.Loose {
197 f.parse(bytes.NewBuffer(nil))
299 _ = f.parse(bytes.NewBuffer(nil))
198300 continue
199301 }
200302 return err
303 }
304 if f.options.ShortCircuit {
305 return nil
201306 }
202307 }
203308 return nil
221326 }
222327
223328 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)
227333 }
228334
229335 // Use buffer to make sure target is safe until finish encoding.
230336 buf := bytes.NewBuffer(nil)
231337 for i, sname := range f.sectionList {
232 sec := f.Section(sname)
338 sec := f.SectionWithIndex(sname, f.sectionIndexes[i])
233339 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) {
245356 if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
246357 return nil, err
247358 }
267378 }
268379
269380 // 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
271382 // we need to take that into account in our calculation.
272383 alignLength := 0
273384 if PrettyFormat {
274385 for _, kname := range sec.keyList {
275386 keyLength := len(kname)
276387 // 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) {
278389 keyLength += 2
279390 } else if strings.Contains(kname, "`") {
280391 keyLength += 6
287398 }
288399 alignSpaces := bytes.Repeat([]byte(" "), alignLength)
289400
290 KEY_LIST:
401 KeyList:
291402 for _, kname := range sec.keyList {
292403 key := sec.Key(kname)
293404 if len(key.Comment) > 0 {
294 if len(indent) > 0 && sname != DEFAULT_SECTION {
405 if len(indent) > 0 && sname != DefaultSection {
295406 buf.WriteString(indent)
296407 }
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 {
308425 buf.WriteString(indent)
309426 }
310427
311428 switch {
312429 case key.isAutoIncrement:
313430 kname = "-"
314 case strings.ContainsAny(kname, "\"=:"):
431 case strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters):
315432 kname = "`" + kname + "`"
316433 case strings.Contains(kname, "`"):
317434 kname = `"""` + kname + `"""`
326443 if kname != sec.keyList[len(sec.keyList)-1] {
327444 buf.WriteString(LineBreak)
328445 }
329 continue KEY_LIST
446 continue KeyList
330447 }
331448
332449 // Write out alignment spaces before "=" sign
339456 val = `"""` + val + `"""`
340457 } else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") {
341458 val = "`" + val + "`"
459 } else if len(strings.TrimSpace(val)) != len(val) {
460 val = `"` + val + `"`
342461 }
343462 if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil {
344463 return nil, err
382501 // SaveToIndent writes content to file system with given value indention.
383502 func (f *File) SaveToIndent(filename, indent string) error {
384503 // 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.
386505 buf, err := f.writeToBuffer(indent)
387506 if err != nil {
388507 return err
1515
1616 import (
1717 "bytes"
18 "io/ioutil"
19 "runtime"
1820 "testing"
1921
2022 . "github.com/smartystreets/goconvey/convey"
4446 So(sec, ShouldNotBeNil)
4547 So(sec.Name(), ShouldEqual, "author")
4648
47 So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "author"})
49 So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "author"})
4850
4951 Convey("With duplicated name", func() {
5052 sec, err := f.NewSection("author")
5254 So(sec, ShouldNotBeNil)
5355
5456 // 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"})
5658 })
5759
5860 Convey("With empty string", func() {
6264 })
6365 }
6466
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
65177 func TestFile_NewRawSection(t *testing.T) {
66178 Convey("Create a new raw section", t, func() {
67179 f := ini.Empty()
73185 So(sec, ShouldNotBeNil)
74186 So(sec.Name(), ShouldEqual, "comments")
75187
76 So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "comments"})
188 So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "comments"})
77189 So(f.Section("comments").Body(), ShouldEqual, `1111111111111111111000000000000000001110000
78190 111111111111111111100000000000111000000000`)
79191
81193 sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000`)
82194 So(err, ShouldBeNil)
83195 So(sec, ShouldNotBeNil)
84 So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "comments"})
196 So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "comments"})
85197
86198 // Overwrite previous existed section
87199 So(f.Section("comments").Body(), ShouldEqual, `1111111111111111111000000000000000001110000`)
100212 So(f, ShouldNotBeNil)
101213
102214 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"})
104216
105217 Convey("With duplicated name", func() {
106218 So(f.NewSections("author", "features"), ShouldBeNil)
107219
108220 // 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"})
110222 })
111223
112224 Convey("With empty string", func() {
117229
118230 func TestFile_GetSection(t *testing.T) {
119231 Convey("Get a section", t, func() {
120 f, err := ini.Load(_FULL_CONF)
232 f, err := ini.Load(fullConf)
121233 So(err, ShouldBeNil)
122234 So(f, ShouldNotBeNil)
123235
135247
136248 func TestFile_Section(t *testing.T) {
137249 Convey("Get a section", t, func() {
138 f, err := ini.Load(_FULL_CONF)
250 f, err := ini.Load(fullConf)
139251 So(err, ShouldBeNil)
140252 So(f, ShouldNotBeNil)
141253
165277
166278 func TestFile_Sections(t *testing.T) {
167279 Convey("Get all sections", t, func() {
168 f, err := ini.Load(_FULL_CONF)
280 f, err := ini.Load(fullConf)
169281 So(err, ShouldBeNil)
170282 So(f, ShouldNotBeNil)
171283
172284 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"}
174286 So(len(secs), ShouldEqual, len(names))
175287 for i, name := range names {
176288 So(secs[i].Name(), ShouldEqual, name)
201313
202314 func TestFile_SectionStrings(t *testing.T) {
203315 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"})
209321 })
210322 }
211323
214326 f := ini.Empty()
215327 So(f, ShouldNotBeNil)
216328
217 f.NewSections("author", "package", "features")
329 _ = f.NewSections("author", "package", "features")
218330 f.DeleteSection("features")
219331 f.DeleteSection("")
220332 So(f.SectionStrings(), ShouldResemble, []string{"author", "package"})
221333 })
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 })
222363 }
223364
224365 func TestFile_Append(t *testing.T) {
226367 f := ini.Empty()
227368 So(f, ShouldNotBeNil)
228369
229 So(f.Append(_MINIMAL_CONF, []byte(`
370 So(f.Append(minimalConf, []byte(`
230371 [author]
231372 NAME = Unknwon`)), ShouldBeNil)
232373
233374 Convey("With bad input", func() {
234375 So(f.Append(123), ShouldNotBeNil)
235 So(f.Append(_MINIMAL_CONF, 123), ShouldNotBeNil)
376 So(f.Append(minimalConf, 123), ShouldNotBeNil)
236377 })
237378 })
238379 }
239380
240381 func TestFile_WriteTo(t *testing.T) {
382 if runtime.GOOS == "windows" {
383 t.Skip("Skipping testing on Windows")
384 }
385
241386 Convey("Write content to somewhere", t, func() {
242 f, err := ini.Load(_FULL_CONF)
387 f, err := ini.Load(fullConf)
243388 So(err, ShouldBeNil)
244389 So(f, ShouldNotBeNil)
245390
246391 f.Section("author").Comment = `Information about package author
247392 # Bio can be written in multiple lines.`
248393 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")
251396
252397 var buf bytes.Buffer
253398 _, err = f.WriteTo(&buf)
254399 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 "
340455
341456 `)
342457 })
344459
345460 func TestFile_SaveTo(t *testing.T) {
346461 Convey("Write content to somewhere", t, func() {
347 f, err := ini.Load(_FULL_CONF)
462 f, err := ini.Load(fullConf)
348463 So(err, ShouldBeNil)
349464 So(f, ShouldNotBeNil)
350465
352467 So(f.SaveToIndent("testdata/conf_out.ini", "\t"), ShouldBeNil)
353468 })
354469 }
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
02 // Copyright 2014 Unknwon
13 //
24 // Licensed under the Apache License, Version 2.0 (the "License"): you may
1517 package ini
1618
1719 import (
18 "bytes"
19 "fmt"
20 "io"
21 "io/ioutil"
2220 "os"
2321 "regexp"
2422 "runtime"
23 "strings"
2524 )
2625
2726 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.
2928 // In most of cases, an empty string is all you need to access the section.
30 DEFAULT_SECTION = "DEFAULT"
29 DefaultSection = "DEFAULT"
3130
3231 // Maximum allowed depth when recursively substituing variable names.
33 _DEPTH_VALUES = 99
34 _VERSION = "1.32.0"
32 depthValues = 99
3533 )
3634
37 // Version returns current package version literal.
38 func Version() string {
39 return _VERSION
40 }
41
4235 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.
4638 LineBreak = "\n"
4739
4840 // Variable regexp pattern: %(variable)s
49 varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`)
41 varPattern = regexp.MustCompile(`%\(([^)]+)\)s`)
5042
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
5249 // or reduce all possible spaces for compact format.
5350 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 = ""
6057 )
6158
59 var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")
60
6261 func init() {
63 if runtime.GOOS == "windows" {
62 if runtime.GOOS == "windows" && !inTest {
6463 LineBreak = "\r\n"
6564 }
6665 }
6766
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).
12268 type LoadOptions struct {
12369 // Loose indicates whether the parser should ignore nonexistent files or return error.
12470 Loose bool
12571 // Insensitive indicates whether the parser forces all section and key names to lowercase.
12672 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
12777 // IgnoreContinuation indicates whether to ignore continuation lines while parsing.
12878 IgnoreContinuation bool
12979 // IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value.
13080 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
13185 // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing.
13286 // This type of keys are mostly used in my.cnf.
13387 AllowBooleanKeys bool
13690 // AllowNestedValues indicates whether to allow AWS-like nested values.
13791 // Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values
13892 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
139103 // UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format
140104 // when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value"
141105 UnescapeValueDoubleQuotes bool
143107 // when value is NOT surrounded by any quotes.
144108 // Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all.
145109 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
147111 // conform to key/value pairs. Specify the names of those blocks here.
148112 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
149127 }
150128
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).
151133 func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) {
152134 sources := make([]dataSource, len(others)+1)
153135 sources[0], err = parseDataSource(source)
186168 return LoadSources(LoadOptions{Insensitive: true}, source, others...)
187169 }
188170
189 // InsensitiveLoad has exactly same functionality as Load function
171 // ShadowLoad has exactly same functionality as Load function
190172 // except it allows have shadow keys.
191173 func ShadowLoad(source interface{}, others ...interface{}) (*File, error) {
192174 return LoadSources(LoadOptions{AllowShadows: true}, source, others...)
+0
-35
ini_internal_test.go less more
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 }
1515
1616 import (
1717 "bytes"
18 "flag"
1819 "io/ioutil"
1920 "testing"
2021
2324 )
2425
2526 const (
26 _CONF_DATA = `
27 confData = `
2728 ; Package name
2829 NAME = ini
2930 ; Package version
4142 Coding addict.
4243 Good man.
4344 """ # 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"
4748 )
49
50 var update = flag.Bool("update", false, "Update .golden files")
4851
4952 func TestLoad(t *testing.T) {
5053 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(
5555 "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"))),
6059 )
6160 So(err, ShouldBeNil)
6261 So(f, ShouldNotBeNil)
6362
64 // Vaildate values make sure all sources are loaded correctly
63 // Validate values make sure all sources are loaded correctly
6564 sec := f.Section("")
6665 So(sec.Key("NAME").String(), ShouldEqual, "ini")
6766 So(sec.Key("VERSION").String(), ShouldEqual, "v1")
7473
7574 Convey("Load from bad data sources", t, func() {
7675 Convey("Invalid input", func() {
77 _, err := ini.Load(_NOT_FOUND_CONF)
76 _, err := ini.Load(notFoundConf)
7877 So(err, ShouldNotBeNil)
7978 })
8079
8382 So(err, ShouldNotBeNil)
8483 })
8584 })
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 })
86277 }
87278
88279 func TestLoadSources(t *testing.T) {
89280 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]
113305 e-mail = u@gogs.io
114306
115307 `)
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(`
131376 key1=a\b\
132377 key2=c\d\
133378 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(`
143388 key1=a\b\
144389 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(`
156414 key1=value ;comment
157415 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(`
182454 key1=hello
183455 #key2
184456 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
196468 # key2
197469 key3
198470 `)
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(`
203475 key1=hello
204476 #key2
205477 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(`
212484 [remote "origin"]
213485 url = https://github.com/Antergone/test1.git
214486 url = https://github.com/Antergone/test2.git
215487 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"]
231503 url = https://github.com/Antergone/test1.git
232504 url = https://github.com/Antergone/test2.git
233505 fetch = +refs/heads/*:refs/remotes/origin/*
234506
235507 `)
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(`
240512 [remote "origin"]
241513 url = https://github.com/Antergone/test1.git
242514 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(`
254527 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(`
262535 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(`
275549 key = test value <span style="color: %s\; background: %s">more text</span>
276550 `))
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(`
288797 Lesson_Location = 87
289798 Lesson_Status = C
290799 Score = 3
296805
297806 [COMMENTS]
298807 <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
305814 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
313822 lesson_status = C
314823 score = 3
315824 time = 00:02:30
321830 [comments]
322831 <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>
323832 `)
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(`
328837 [CORE_LESSON]
329838 my lesson state data – 1111111111111111111000000000000000001110000
330839 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 `))
3311104 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 `)
3321416 })
3331417 })
3341418 })
3351419 }
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 }
+138
-60
key.go less more
5353 return errors.New("cannot add shadow to auto-increment or boolean key")
5454 }
5555
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
5666 shadow := newKey(k.s, k.name, val)
5767 shadow.isShadow = true
5868 k.shadows = append(k.shadows, shadow)
7686 return nil
7787 }
7888
89 // AddNestedValue adds a nested value to the key.
7990 func (k *Key) AddNestedValue(val string) error {
8091 if !k.s.f.options.AllowNestedValues {
8192 return errors.New("nested value is not allowed")
125136 if !strings.Contains(val, "%") {
126137 return val
127138 }
128 for i := 0; i < _DEPTH_VALUES; i++ {
139 for i := 0; i < depthValues; i++ {
129140 vr := varPattern.FindString(val)
130141 if len(vr) == 0 {
131142 break
132143 }
133144
134145 // Take off leading '%(' and trailing ')s'.
135 noption := strings.TrimLeft(vr, "%(")
136 noption = strings.TrimRight(noption, ")s")
146 noption := vr[2 : len(vr)-2]
137147
138148 // Search in the same section.
149 // If not found or found the key itself, then search again in default section.
139150 nk, err := k.s.GetKey(noption)
140151 if err != nil || k == nk {
141 // Search again in default section.
142152 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 }
143158 }
144159
145160 // Substitute by new value and take off leading '%(' and trailing ')s'.
186201
187202 // Int returns int type value.
188203 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
190206 }
191207
192208 // Int64 returns int64 type value.
193209 func (k *Key) Int64() (int64, error) {
194 return strconv.ParseInt(k.String(), 10, 64)
210 return strconv.ParseInt(k.String(), 0, 64)
195211 }
196212
197213 // Uint returns uint type valued.
198214 func (k *Key) Uint() (uint, error) {
199 u, e := strconv.ParseUint(k.String(), 10, 64)
215 u, e := strconv.ParseUint(k.String(), 0, 64)
200216 return uint(u), e
201217 }
202218
203219 // Uint64 returns uint64 type value.
204220 func (k *Key) Uint64() (uint64, error) {
205 return strconv.ParseUint(k.String(), 10, 64)
221 return strconv.ParseUint(k.String(), 0, 64)
206222 }
207223
208224 // Duration returns time.Duration type value.
490506 buf.WriteRune(runes[idx])
491507 }
492508 }
493 idx += 1
509 idx++
494510 if idx == len(runes) {
495511 break
496512 }
552568 return vals
553569 }
554570
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
555577 // TimesFormat parses with given format and returns list of time.Time divided by given delimiter.
556578 // Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
557579 func (k *Key) TimesFormat(format, delim string) []time.Time {
600622 return vals
601623 }
602624
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
603632 // ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter.
604633 func (k *Key) ValidTimesFormat(format, delim string) []time.Time {
605634 vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false)
634663 // StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input.
635664 func (k *Key) StrictUint64s(delim string) ([]uint64, error) {
636665 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)
637671 }
638672
639673 // StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter
648682 return k.StrictTimesFormat(time.RFC3339, delim)
649683 }
650684
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
651701 // parseFloat64s transforms strings to float64s.
652702 func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) {
653703 vals := make([]float64, 0, len(strs))
654 for _, str := range strs {
704 parser := func(str string) (interface{}, error) {
655705 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
664715 }
665716
666717 // parseInts transforms strings to ints.
667718 func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) {
668719 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
679731 }
680732
681733 // parseInt64s transforms strings to int64s.
682734 func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) {
683735 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
694748 }
695749
696750 // parseUints transforms strings to uints.
697751 func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) {
698752 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
709765 }
710766
711767 // parseUint64s transforms strings to uint64s.
712768 func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) {
713769 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
725786
726787 // parseTimesFormat transforms strings to times in given format.
727788 func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) {
728789 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))
729807 for _, str := range strs {
730 val, err := time.Parse(format, str)
808 val, err := parser(str)
731809 if err != nil && returnOnInvalid {
732810 return nil, err
733811 }
1616 import (
1717 "bytes"
1818 "fmt"
19 "runtime"
1920 "strings"
2021 "testing"
2122 "time"
99100 }
100101 }
101102
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
102110 func timesEqual(values []time.Time, expected ...time.Time) {
103111 So(values, ShouldHaveLength, len(expected))
104112 for i, v := range expected {
108116
109117 func TestKey_Helpers(t *testing.T) {
110118 Convey("Getting and setting values", t, func() {
111 f, err := ini.Load(_FULL_CONF)
119 f, err := ini.Load(fullConf)
112120 So(err, ShouldBeNil)
113121 So(f, ShouldNotBeNil)
114122
165173
166174 Convey("Get sections", func() {
167175 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"} {
169177 So(sections[i].Name(), ShouldEqual, name)
170178 }
171179 })
176184 })
177185
178186 Convey("Get multiple line value", func() {
187 if runtime.GOOS == "windows" {
188 t.Skip("Skipping testing on Windows")
189 }
190
179191 So(f.Section("author").Key("BIO").String(), ShouldEqual, "Gopher.\nCoding addict.\nGood man.\n")
180192 })
181193
214226 v7, err := sec.Key("TIME").Time()
215227 So(err, ShouldBeNil)
216228 So(v7.String(), ShouldEqual, t.String())
229
230 v8, err := sec.Key("HEX_NUMBER").Int()
231 So(err, ShouldBeNil)
232 So(v8, ShouldEqual, 0x3000)
217233
218234 Convey("Must get values with type", func() {
219235 So(sec.Key("STRING").MustString("404"), ShouldEqual, "str")
224240 So(sec.Key("UINT").MustUint(), ShouldEqual, 3)
225241 So(sec.Key("UINT").MustUint64(), ShouldEqual, 3)
226242 So(sec.Key("TIME").MustTime().String(), ShouldEqual, t.String())
243 So(sec.Key("HEX_NUMBER").MustInt(), ShouldEqual, 0x3000)
227244
228245 dur, err := time.ParseDuration("2h45m")
229246 So(err, ShouldBeNil)
237254 So(sec.Key("INT64_404").MustInt64(15), ShouldEqual, 15)
238255 So(sec.Key("UINT_404").MustUint(6), ShouldEqual, 6)
239256 So(sec.Key("UINT64_404").MustUint64(6), ShouldEqual, 6)
257 So(sec.Key("HEX_NUMBER_404").MustInt(0x3001), ShouldEqual, 0x3001)
240258
241259 t, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z")
242260 So(err, ShouldBeNil)
254272 So(sec.Key("UINT64_404").String(), ShouldEqual, "6")
255273 So(sec.Key("TIME_404").String(), ShouldEqual, "2014-01-01T20:17:05Z")
256274 So(sec.Key("DURATION_404").String(), ShouldEqual, "2h45m0s")
275 So(sec.Key("HEX_NUMBER_404").String(), ShouldEqual, "12289")
257276 })
258277 })
259278 })
329348 vals5 := sec.Key("UINTS").Uint64s(",")
330349 uint64sEqual(vals5, 1, 2, 3)
331350
351 vals6 := sec.Key("BOOLS").Bools(",")
352 boolsEqual(vals6, true, false, false)
353
332354 t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
333355 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)
336358 })
337359
338360 Convey("Test string slice escapes", func() {
362384 vals5 := sec.Key("UINTS").ValidUint64s(",")
363385 uint64sEqual(vals5, 1, 2, 3)
364386
387 vals6 := sec.Key("BOOLS").ValidBools(",")
388 boolsEqual(vals6, true, false, false)
389
365390 t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
366391 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)
369394 })
370395
371396 Convey("Get values one type into slice of another type", func() {
385410 vals5 := sec.Key("STRINGS").ValidUint64s(",")
386411 So(vals5, ShouldBeEmpty)
387412
388 vals6 := sec.Key("STRINGS").ValidTimes(",")
413 vals6 := sec.Key("STRINGS").ValidBools(",")
389414 So(vals6, ShouldBeEmpty)
415
416 vals7 := sec.Key("STRINGS").ValidTimes(",")
417 So(vals7, ShouldBeEmpty)
390418 })
391419
392420 Convey("Get valid values into slice without errors", func() {
411439 So(err, ShouldBeNil)
412440 uint64sEqual(vals5, 1, 2, 3)
413441
442 vals6, err := sec.Key("BOOLS").StrictBools(",")
443 So(err, ShouldBeNil)
444 boolsEqual(vals6, true, false, false)
445
414446 t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
415447 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)
419451 })
420452
421453 Convey("Get invalid values into slice", func() {
440472 So(vals5, ShouldBeEmpty)
441473 So(err, ShouldNotBeNil)
442474
443 vals6, err := sec.Key("STRINGS").StrictTimes(",")
475 vals6, err := sec.Key("STRINGS").StrictBools(",")
444476 So(vals6, ShouldBeEmpty)
477 So(err, ShouldNotBeNil)
478
479 vals7, err := sec.Key("STRINGS").StrictTimes(",")
480 So(vals7, ShouldBeEmpty)
445481 So(err, ShouldNotBeNil)
446482 })
447483 })
513549 Convey("Recursive values should not reflect on same key", t, func() {
514550 f, err := ini.Load([]byte(`
515551 NAME = ini
552 expires = yes
516553 [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
520559 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 }
1818 "bytes"
1919 "fmt"
2020 "io"
21 "regexp"
2122 "strconv"
2223 "strings"
2324 "unicode"
2425 )
2526
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 }
3442
3543 type parser struct {
3644 buf *bufio.Reader
45 options parserOptions
46
3747 isEOF bool
3848 count int
3949 comment *bytes.Buffer
4050 }
4151
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
4364 return &parser{
44 buf: bufio.NewReader(r),
65 buf: bufio.NewReaderSize(r, size),
66 options: opts,
4567 count: 1,
4668 comment: &bytes.Buffer{},
4769 }
6183 case mask[0] == 254 && mask[1] == 255:
6284 fallthrough
6385 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 }
6590 case mask[0] == 239 && mask[1] == 187:
6691 mask, err := p.buf.Peek(3)
6792 if err != nil && err != io.EOF {
7095 return nil
7196 }
7297 if mask[2] == 191 {
73 p.buf.Read(mask)
98 _, err = p.buf.Read(mask)
99 if err != nil {
100 return err
101 }
74102 }
75103 }
76104 return nil
96124 return in[i:], true
97125 }
98126
99 func readKeyName(in []byte) (string, int, error) {
127 func readKeyName(delimiters string, in []byte) (string, int, error) {
100128 line := string(in)
101129
102130 // Check if key name surrounded by quotes.
112140 }
113141
114142 // Get out key name
115 endIdx := -1
143 var endIdx int
116144 if len(keyQuote) > 0 {
117145 startIdx := len(keyQuote)
118146 // FIXME: fail case -> """"""name"""=value
123151 pos += startIdx
124152
125153 // Find key-value delimiter
126 i := strings.IndexAny(line[pos+startIdx:], "=:")
154 i := strings.IndexAny(line[pos+startIdx:], delimiters)
127155 if i < 0 {
128156 return "", -1, ErrDelimiterNotFound{line}
129157 }
131159 return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil
132160 }
133161
134 endIdx = strings.IndexAny(line, "=:")
162 endIdx = strings.IndexAny(line, delimiters)
135163 if endIdx < 0 {
136164 return "", -1, ErrDelimiterNotFound{line}
137165 }
158186 }
159187 val += next
160188 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)
162190 }
163191 }
164192 return val, nil
192220 strings.IndexByte(in[1:], quote) == len(in)-2
193221 }
194222
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) {
197224
198225 line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
199226 if len(line) == 0 {
227 if p.options.AllowPythonMultilineValues && len(in) > 0 && in[len(in)-1] == '\n' {
228 return p.readPythonMultilines(line, bufferSize)
229 }
200230 return "", nil
201231 }
202232
205235 valQuote = `"""`
206236 } else if line[0] == '`' {
207237 valQuote = "`"
208 } else if unescapeValueDoubleQuotes && line[0] == '"' {
238 } else if p.options.UnescapeValueDoubleQuotes && line[0] == '"' {
209239 valQuote = `"`
210240 }
211241
217247 return p.readMultilines(line, line[startIdx:], valQuote)
218248 }
219249
220 if unescapeValueDoubleQuotes && valQuote == `"` {
250 if p.options.UnescapeValueDoubleQuotes && valQuote == `"` {
221251 return strings.Replace(line[startIdx:pos+startIdx], `\"`, `"`, -1), nil
222252 }
223253 return line[startIdx : pos+startIdx], nil
224254 }
225255
256 lastChar := line[len(line)-1]
226257 // Won't be able to reach here if value only contains whitespace
227258 line = strings.TrimSpace(line)
259 trimmedLastChar := line[len(line)-1]
228260
229261 // Check continuation lines when desired
230 if !ignoreContinuation && line[len(line)-1] == '\\' {
262 if !p.options.IgnoreContinuation && trimmedLastChar == '\\' {
231263 return p.readContinuationLines(line[:len(line)-1])
232264 }
233265
234266 // 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
237279 if i > -1 {
238280 p.comment.WriteString(line[i:])
239281 line = strings.TrimSpace(line[:i])
240282 }
283
241284 }
242285
243286 // Trim single and double quotes
244 if hasSurroundedQuote(line, '\'') ||
245 hasSurroundedQuote(line, '"') {
287 if (hasSurroundedQuote(line, '\'') ||
288 hasSurroundedQuote(line, '"')) && !p.options.PreserveSurroundedQuote {
246289 line = line[1 : len(line)-1]
247 } else if len(valQuote) == 0 && unescapeValueCommentSymbols {
290 } else if len(valQuote) == 0 && p.options.UnescapeValueCommentSymbols {
248291 if strings.Contains(line, `\;`) {
249292 line = strings.Replace(line, `\;`, ";", -1)
250293 }
251294 if strings.Contains(line, `\#`) {
252295 line = strings.Replace(line, `\#`, "#", -1)
253296 }
254 }
297 } else if p.options.AllowPythonMultilineValues && lastChar == '\n' {
298 return p.readPythonMultilines(line, bufferSize)
299 }
300
255301 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 }
256358 }
257359
258360 // parse parses data through an io.Reader.
259361 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 })
261373 if err = p.BOM(); err != nil {
262374 return fmt.Errorf("BOM: %v", err)
263375 }
264376
265377 // 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)
269381 }
270382 section, _ := f.NewSection(name)
271383
275387
276388 var line []byte
277389 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
278412 for !p.isEOF {
279413 line, err = p.readUntil('\n')
280414 if err != nil {
284418 if f.options.AllowNestedValues &&
285419 isLastValueEmpty && len(line) > 0 {
286420 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 }
288425 continue
289426 }
290427 }
306443 // Section
307444 if line[0] == '[' {
308445 // 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, ']')
311447 if closeIdx == -1 {
312448 return fmt.Errorf("unclosed section: %s", line)
313449 }
325461
326462 section.Comment = strings.TrimSpace(p.comment.String())
327463
328 // Reset aotu-counter and comments
464 // Reset auto-counter and comments
329465 p.comment.Reset()
330466 p.count = 1
331467
332468 inUnparseableSection = false
333469 for i := range f.options.UnparseableSections {
334470 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)) {
336472 inUnparseableSection = true
337473 continue
338474 }
346482 continue
347483 }
348484
349 kname, offset, err := readKeyName(line)
485 kname, offset, err := readKeyName(f.options.KeyValueDelimiters, line)
350486 if err != nil {
351487 // 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
360505 }
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
368506 }
369507 return err
370508 }
377515 p.count++
378516 }
379517
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)
385519 if err != nil {
386520 return err
387521 }
2727 So(err, ShouldBeNil)
2828 So(f, ShouldNotBeNil)
2929
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")
3131 })
3232
3333 Convey("UTF-16-LE-BOM", func() {
6565 func (s *Section) NewKey(name, val string) (*Key, error) {
6666 if len(name) == 0 {
6767 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 {
6969 name = strings.ToLower(name)
7070 }
7171
8181 }
8282 } else {
8383 s.keys[name].value = val
84 s.keysHash[name] = val
8485 }
8586 return s.keys[name], nil
8687 }
104105
105106 // GetKey returns key in section by given name.
106107 func (s *Section) GetKey(name string) (*Key, error) {
107 // FIXME: change to section level lock?
108108 if s.f.BlockMode {
109109 s.f.lock.RLock()
110110 }
111 if s.f.options.Insensitive {
111 if s.f.options.Insensitive || s.f.options.InsensitiveKeys {
112112 name = strings.ToLower(name)
113113 }
114114 key := s.keys[name]
120120 // Check if it is a child-section.
121121 sname := s.name
122122 for {
123 if i := strings.LastIndex(sname, "."); i > -1 {
123 if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 {
124124 sname = sname[:i]
125125 sec, err := s.f.GetSection(sname)
126126 if err != nil {
127127 continue
128128 }
129129 return sec.GetKey(name)
130 } else {
131 break
132130 }
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)
135134 }
136135 return key, nil
137136 }
142141 return key != nil
143142 }
144143
145 // Haskey is a backwards-compatible name for HasKey.
146 // TODO: delete me in v2
144 // Deprecated: Use "HasKey" instead.
147145 func (s *Section) Haskey(name string) bool {
148146 return s.HasKey(name)
149147 }
189187 var parentKeys []*Key
190188 sname := s.name
191189 for {
192 if i := strings.LastIndex(sname, "."); i > -1 {
190 if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 {
193191 sname = sname[:i]
194192 sec, err := s.f.GetSection(sname)
195193 if err != nil {
236234 if k == name {
237235 s.keyList = append(s.keyList[:i], s.keyList[i+1:]...)
238236 delete(s.keys, name)
237 delete(s.keysHash, name)
239238 return
240239 }
241240 }
245244 // For example, "[parent.child1]" and "[parent.child12]" are child sections
246245 // of section "[parent]".
247246 func (s *Section) ChildSections() []*Section {
248 prefix := s.name + "."
247 prefix := s.name + s.f.options.ChildSectionDelimiter
249248 children := make([]*Section, 0, 3)
250249 for _, name := range s.f.sectionList {
251250 if strings.HasPrefix(name, prefix) {
252 children = append(children, s.f.sections[name])
251 children = append(children, s.f.sections[name]...)
253252 }
254253 }
255254 return children
150150 So(k, ShouldNotBeNil)
151151
152152 So(f.Section("").HasKey("NAME"), ShouldBeTrue)
153 So(f.Section("").Haskey("NAME"), ShouldBeTrue)
153 So(f.Section("").HasKey("NAME"), ShouldBeTrue)
154154 So(f.Section("").HasKey("404"), ShouldBeFalse)
155 So(f.Section("").Haskey("404"), ShouldBeFalse)
155 So(f.Section("").HasKey("404"), ShouldBeFalse)
156156 })
157157 }
158158
271271
272272 func TestSection_KeyHash(t *testing.T) {
273273 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()
288291 relation := map[string]string{
289 "NAME": "ini",
290 "VERSION": "v1",
291 "IMPORT_PATH": "gopkg.in/ini.v1",
292 "name": "app2",
293 "file": "b.log",
292294 }
293295 for k, v := range hash {
294296 So(v, ShouldEqual, relation[k])
2828
2929 // Built-in name getters.
3030 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 {
3333 newstr := make([]rune, 0, len(raw))
3434 for i, chr := range raw {
3535 if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
4949 if i > 0 {
5050 newstr = append(newstr, '_')
5151 }
52 chr -= ('A' - 'a')
52 chr -= 'A' - 'a'
5353 }
5454 newstr = append(newstr, chr)
5555 }
107107 vals, err = key.parseUint64s(strs, true, false)
108108 case reflect.Float64:
109109 vals, err = key.parseFloat64s(strs, true, false)
110 case reflect.Bool:
111 vals, err = key.parseBools(strs, true, false)
110112 case reflectTime:
111113 vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false)
112114 default:
131133 slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i]))
132134 case reflect.Float64:
133135 slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i]))
136 case reflect.Bool:
137 slice.Index(i).Set(reflect.ValueOf(vals.([]bool)[i]))
134138 case reflectTime:
135139 slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i]))
136140 }
148152
149153 // setWithProperType sets proper value to field based on its type,
150154 // 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.
152156 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() {
154163 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 }
159170 case reflect.Bool:
160171 boolVal, err := key.Bool()
161172 if err != nil {
162173 return wrapStrictError(err, isStrict)
163174 }
164 field.SetBool(boolVal)
175 if isPtr {
176 field.Set(reflect.ValueOf(&boolVal))
177 } else {
178 field.SetBool(boolVal)
179 }
165180 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 }
170196 return nil
171197 }
172198
174200 if err != nil {
175201 return wrapStrictError(err, isStrict)
176202 }
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 }
178210 // byte is an alias for uint8, so supporting uint8 breaks support for byte
179211 case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
180212 durationVal, err := key.Duration()
181213 // 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 }
184220 return nil
185221 }
186222
188224 if err != nil {
189225 return wrapStrictError(err, isStrict)
190226 }
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 }
192234
193235 case reflect.Float32, reflect.Float64:
194236 floatVal, err := key.Float64()
195237 if err != nil {
196238 return wrapStrictError(err, isStrict)
197239 }
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 }
199247 case reflectTime:
200248 timeVal, err := key.Time()
201249 if err != nil {
202250 return wrapStrictError(err, isStrict)
203251 }
204 field.Set(reflect.ValueOf(timeVal))
252 if isPtr {
253 field.Set(reflect.ValueOf(&timeVal))
254 } else {
255 field.Set(reflect.ValueOf(timeVal))
256 }
205257 case reflect.Slice:
206258 return setSliceWithProperType(key, field, delim, allowShadow, isStrict)
207259 default:
208 return fmt.Errorf("unsupported type '%s'", t)
260 return fmt.Errorf("unsupported type %q", t)
209261 }
210262 return nil
211263 }
212264
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)
215267 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 {
226280 if val.Kind() == reflect.Ptr {
227281 val = val.Elem()
228282 }
237291 continue
238292 }
239293
240 rawName, _, allowShadow := parseTagOptions(tag)
294 rawName, _, allowShadow, allowNonUnique, extends := parseTagOptions(tag)
241295 fieldName := s.parseFieldName(tpField.Name, rawName)
242296 if len(fieldName) == 0 || !field.CanSet() {
243297 continue
244298 }
245299
246 isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous
247300 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 {
249304 field.Set(reflect.New(tpField.Type.Elem()))
250305 }
251306
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)
256333 }
257334 continue
258335 }
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
259347 }
260348
261349 if key, err := s.GetKey(fieldName); err == nil {
262350 delim := parseDelim(tpField.Tag.Get("delim"))
263351 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)
265353 }
266354 }
267355 }
268356 return nil
269357 }
270358
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 {
273381 typ := reflect.TypeOf(v)
274382 val := reflect.ValueOf(v)
275383 if typ.Kind() == reflect.Ptr {
276384 typ = typ.Elem()
277385 val = val.Elem()
278386 } 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,
286409 // which returns all possible error including value parsing error.
287410 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)
298412 }
299413
300414 // MapTo maps file to given struct.
302416 return f.Section("").MapTo(v)
303417 }
304418
305 // MapTo maps file to given struct in strict mode,
419 // StrictMapTo maps file to given struct in strict mode,
306420 // which returns all possible error including value parsing error.
307421 func (f *File) StrictMapTo(v interface{}) error {
308422 return f.Section("").StrictMapTo(v)
309423 }
310424
311 // MapTo maps data sources to given struct with name mapper.
425 // MapToWithMapper maps data sources to given struct with name mapper.
312426 func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error {
313427 cfg, err := Load(source, others...)
314428 if err != nil {
341455 }
342456
343457 // 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 {
345459 slice := field.Slice(0, field.Len())
346460 if field.Len() == 0 {
347461 return nil
348462 }
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 }
349495
350496 var buf bytes.Buffer
351 sliceOf := field.Type().Elem().Kind()
352497 for i := 0; i < field.Len(); i++ {
353498 switch sliceOf {
354499 case reflect.String:
359504 buf.WriteString(fmt.Sprint(slice.Index(i).Uint()))
360505 case reflect.Float64:
361506 buf.WriteString(fmt.Sprint(slice.Index(i).Float()))
507 case reflect.Bool:
508 buf.WriteString(fmt.Sprint(slice.Index(i).Bool()))
362509 case reflectTime:
363510 buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339))
364511 default:
366513 }
367514 buf.WriteString(delim)
368515 }
369 key.SetValue(buf.String()[:buf.Len()-1])
516 key.SetValue(buf.String()[:buf.Len()-len(delim)])
370517 return nil
371518 }
372519
373520 // 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 {
375522 switch t.Kind() {
376523 case reflect.String:
377524 key.SetValue(field.String())
386533 case reflectTime:
387534 key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339)))
388535 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 }
390541 default:
391 return fmt.Errorf("unsupported type '%s'", t)
542 return fmt.Errorf("unsupported type %q", t)
392543 }
393544 return nil
394545 }
416567 return false
417568 }
418569
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
419575 func (s *Section) reflectFrom(val reflect.Value) error {
420576 if val.Kind() == reflect.Ptr {
421577 val = val.Elem()
423579 typ := val.Type()
424580
425581 for i := 0; i < typ.NumField(); i++ {
582 if !val.Field(i).CanInterface() {
583 continue
584 }
585
426586 field := val.Field(i)
427587 tpField := typ.Field(i)
428588
431591 continue
432592 }
433593
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)
440604 if len(fieldName) == 0 || !field.CanSet() {
441605 continue
442606 }
443607
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) ||
445616 (tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") {
446617 // Note: The only error here is section doesn't exist.
447618 sec, err := s.f.GetSection(fieldName)
456627 }
457628
458629 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.
465665 key, err := s.GetKey(fieldName)
466666 if err != nil {
467667 key, _ = s.NewKey(fieldName, "")
472672 key.Comment = tpField.Tag.Get("comment")
473673 }
474674
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)
477678 }
478679
479680 }
480681 return nil
481682 }
482683
483 // ReflectFrom reflects secion from given struct.
684 // ReflectFrom reflects section from given struct. It overwrites existing ones.
484685 func (s *Section) ReflectFrom(v interface{}) error {
485686 typ := reflect.TypeOf(v)
486687 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
487723 if typ.Kind() == reflect.Ptr {
488 typ = typ.Elem()
489724 val = val.Elem()
490725 } else {
491 return errors.New("cannot reflect from non-pointer struct")
726 return errors.New("not a pointer to a struct")
492727 }
493728
494729 return s.reflectFrom(val)
499734 return f.Section("").ReflectFrom(v)
500735 }
501736
502 // ReflectFrom reflects data sources from given struct with name mapper.
737 // ReflectFromWithMapper reflects data sources from given struct with name mapper.
503738 func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error {
504739 cfg.NameMapper = mapper
505740 return cfg.ReflectFrom(v)
2121 "time"
2222
2323 . "github.com/smartystreets/goconvey/convey"
24
2425 "gopkg.in/ini.v1"
2526 )
2627
3233 Ages []uint
3334 Populations []uint64
3435 Coordinates []float64
36 Flags []bool
3537 Note string
3638 Unused int `ini:"-"`
3739 }
3840
39 type testEmbeded struct {
41 type TestEmbeded struct {
4042 GPA float64
4143 }
4244
4345 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 = `
60105 NAME = Unknwon
61106 Age = 21
62107 Male = true
63108 Money = 1.25
64109 Born = 1993-10-07T20:17:05Z
65110 Duration = 2h45m
111 OldVersionTime = 30
66112 Unsigned = 3
67113 omitthis = true
68114 Shadows = 1, 2
69115 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
70123
71124 [Others]
72125 Cities = HangZhou|Boston
76129 Ages = 18,19
77130 Populations = 12345678,98765432
78131 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
79144 Note = Hello world!
80145
81146 [grade]
84149 [foo.bar]
85150 Here = there
86151 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
87173 `
88174
89175 type unsupport struct {
96182 }
97183 }
98184
99 type unsupport3 struct {
185 type Unsupport3 struct {
100186 Cities byte
101187 }
102188
103189 type unsupport4 struct {
104 *unsupport3 `ini:"Others"`
190 *Unsupport3 `ini:"Others"`
105191 }
106192
107193 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
114201 }
115202
116203 type fooBar struct {
117204 Here, When string
118205 }
119206
120 const _INVALID_DATA_CONF_STRUCT = `
207 const invalidDataConfStruct = `
121208 Name =
122209 Age = age
123210 Male = 123
130217 Convey("Map to struct", t, func() {
131218 Convey("Map file to struct", func() {
132219 ts := new(testStruct)
133 So(ini.MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil)
220 So(ini.MapTo(ts, []byte(confDataStruct)), ShouldBeNil)
134221
135222 So(ts.Name, ShouldEqual, "Unknwon")
136223 So(ts.Age, ShouldEqual, 21)
145232 dur, err := time.ParseDuration("2h45m")
146233 So(err, ShouldBeNil)
147234 So(ts.Time.Seconds(), ShouldEqual, dur.Seconds())
235
236 So(ts.OldVersionTime*time.Second, ShouldEqual, 30*time.Second)
148237
149238 So(strings.Join(ts.Others.Cities, ","), ShouldEqual, "HangZhou,Boston")
150239 So(ts.Others.Visits[0].String(), ShouldEqual, t.String())
153242 So(fmt.Sprint(ts.Others.Ages), ShouldEqual, "[18 19]")
154243 So(fmt.Sprint(ts.Others.Populations), ShouldEqual, "[12345678 98765432]")
155244 So(fmt.Sprint(ts.Others.Coordinates), ShouldEqual, "[192.168 10.11]")
245 So(fmt.Sprint(ts.Others.Flags), ShouldEqual, "[true false]")
156246 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)
158275 })
159276
160277 Convey("Map section to struct", func() {
161278 foobar := new(fooBar)
162 f, err := ini.Load([]byte(_CONF_DATA_STRUCT))
279 f, err := ini.Load([]byte(confDataStruct))
163280 So(err, ShouldBeNil)
164281
165282 So(f.Section("foo.bar").MapTo(foobar), ShouldBeNil)
168285 })
169286
170287 Convey("Map to non-pointer struct", func() {
171 f, err := ini.Load([]byte(_CONF_DATA_STRUCT))
288 f, err := ini.Load([]byte(confDataStruct))
172289 So(err, ShouldBeNil)
173290 So(f, ShouldNotBeNil)
174291
176293 })
177294
178295 Convey("Map to unsupported type", func() {
179 f, err := ini.Load([]byte(_CONF_DATA_STRUCT))
296 f, err := ini.Load([]byte(confDataStruct))
180297 So(err, ShouldBeNil)
181298 So(f, ShouldNotBeNil)
182299
193310
194311 Convey("Map to omitempty field", func() {
195312 ts := new(testStruct)
196 So(ini.MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil)
313 So(ini.MapTo(ts, []byte(confDataStruct)), ShouldBeNil)
197314
198315 So(ts.Omitted, ShouldEqual, true)
199316 })
200317
201318 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))
203320 So(err, ShouldBeNil)
204321 ts := new(testStruct)
205322 So(f.MapTo(ts), ShouldBeNil)
213330 })
214331
215332 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))
217334 So(err, ShouldBeNil)
218335
219336 t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
220337 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"}}
222339 So(f.MapTo(dv), ShouldBeNil)
223340 So(dv.Name, ShouldEqual, "Joe")
224341 So(dv.Age, ShouldEqual, 10)
227344 So(dv.Born.String(), ShouldEqual, t.String())
228345 So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston")
229346 })
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 })
230357 })
231358
232359 Convey("Map to struct in strict mode", t, func() {
256383
257384 So(f.Section("").StrictMapTo(s), ShouldBeNil)
258385 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 })
259483 })
260484 }
261485
269493 Ages []uint
270494 Populations []uint64
271495 Coordinates []float64
496 Flags []bool
272497 None []int
273498 }
274499 type Author struct {
275500 Name string `ini:"NAME"`
276501 Male bool
502 Optional *bool
277503 Age int `comment:"Author's age"`
278504 Height uint
279505 GPA float64
280506 Date time.Time
281507 NeverMind string `ini:"-"`
508 ignored string
282509 *Embeded `ini:"infos" comment:"Embeded section"`
283510 }
284511
285512 t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
286513 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",
288515 &Embeded{
289516 []time.Time{t, t},
290517 []string{"HangZhou", "Boston"},
293520 []uint{18, 19},
294521 []uint64{12345678, 98765432},
295522 []float64{192.168, 10.11},
523 []bool{true, false},
296524 []int{},
297525 }}
298526 cfg := ini.Empty()
301529 var buf bytes.Buffer
302530 _, err = cfg.WriteTo(&buf)
303531 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 =
306535 ; 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
311540
312541 ; Embeded section
313542 [infos]
319548 Ages = 18,19
320549 Populations = 12345678,98765432
321550 Coordinates = 192.168,10.11
551 Flags = true,false
322552 None =
323553
324554 `)
331561 cfg := ini.Empty()
332562 type SpecialStruct struct {
333563 FirstName string `ini:"first_name"`
334 LastName string `ini:"last_name"`
564 LastName string `ini:"last_name,omitempty"`
335565 JustOmitMe string `ini:"omitempty"`
336566 LastLogin time.Time `ini:"last_login,omitempty"`
337567 LastLogin2 time.Time `ini:",omitempty"`
338568 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)
342583
343584 var buf bytes.Buffer
344585 _, err = cfg.WriteTo(&buf)
348589
349590 `)
350591 })
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 })
351817 })
352818 }
353819
363829 So(err, ShouldBeNil)
364830 So(cfg, ShouldNotBeNil)
365831
366 cfg.NameMapper = ini.AllCapsUnderscore
832 cfg.NameMapper = ini.SnackCase
367833 tg := new(testMapper)
368834 So(cfg.MapTo(tg), ShouldBeNil)
369835 So(tg.PackageName, ShouldEqual, "ini")
384850 So(ds.Duration.Seconds(), ShouldEqual, dur.Seconds())
385851 })
386852 }
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
00 [author]
1 E-MAIL = u@gogs.io
1 E-MAIL = example@email.com
3535 TIME = 2015-01-01T20:17:05Z
3636 DURATION = 2h45m
3737 UINT = 3
38 HEX_NUMBER = 0x3000
3839
3940 [array]
4041 STRINGS = en, zh, de
4243 INTS = 1, 2, 3
4344 UINTS = 1, 2, 3
4445 TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z
46 BOOLS = true, false, false
4547
4648 [note]
4749 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.