pax_global_header00006660000000000000000000000064132042137350014512gustar00rootroot0000000000000052 comment=32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a ini-1.32.0/000077500000000000000000000000001320421373500123545ustar00rootroot00000000000000ini-1.32.0/.github/000077500000000000000000000000001320421373500137145ustar00rootroot00000000000000ini-1.32.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000002611320421373500164200ustar00rootroot00000000000000### Please give general description of the problem ### Please provide code snippets to reproduce the problem described above ### Do you have any suggestion to fix the problem?ini-1.32.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000001271320421373500175150ustar00rootroot00000000000000### What problem should be fixed? ### Have you added test cases to catch the problem? ini-1.32.0/.gitignore000066400000000000000000000001511320421373500143410ustar00rootroot00000000000000testdata/conf_out.ini ini.sublime-project ini.sublime-workspace testdata/conf_reflect.ini .idea /.vscode ini-1.32.0/.travis.yml000066400000000000000000000005031320421373500144630ustar00rootroot00000000000000sudo: false language: go go: - 1.5.x - 1.6.x - 1.7.x - 1.8.x - 1.9.x script: - go get golang.org/x/tools/cmd/cover - go get github.com/smartystreets/goconvey - mkdir -p $HOME/gopath/src/gopkg.in - ln -s $HOME/gopath/src/github.com/go-ini/ini $HOME/gopath/src/gopkg.in/ini.v1 - go test -v -cover -race ini-1.32.0/LICENSE000066400000000000000000000240151320421373500133630ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2014 Unknwon Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ini-1.32.0/Makefile000066400000000000000000000003641320421373500140170ustar00rootroot00000000000000.PHONY: build test bench vet coverage build: vet bench test: go test -v -cover -race bench: go test -v -cover -race -test.bench=. -test.benchmem vet: go vet coverage: go test -coverprofile=c.out && go tool cover -html=c.out && rm c.outini-1.32.0/README.md000066400000000000000000000453051320421373500136420ustar00rootroot00000000000000INI [![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) === ![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200) Package ini provides INI file read and write functionality in Go. [简体中文](README_ZH.md) ## Feature - Load multiple data sources(`[]byte`, file and `io.ReadCloser`) with overwrites. - Read with recursion values. - Read with parent-child sections. - Read with auto-increment key names. - Read with multiple-line values. - Read with tons of helper methods. - Read and convert values to Go types. - Read and **WRITE** comments of sections and keys. - Manipulate sections, keys and comments with ease. - Keep sections and keys in order as you parse and save. ## Installation To use a tagged revision: go get gopkg.in/ini.v1 To use with latest changes: go get github.com/go-ini/ini Please add `-u` flag to update in the future. ### Testing If you want to test on your machine, please apply `-t` flag: go get -t gopkg.in/ini.v1 Please add `-u` flag to update in the future. ## Getting Started ### Loading from data sources 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. ```go cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) ``` Or start with an empty object: ```go cfg := ini.Empty() ``` When you cannot decide how many data sources to load at the beginning, you will still be able to **Append()** them later. ```go err := cfg.Append("other file", []byte("other raw data")) ``` 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. ```go cfg, err := ini.LooseLoad("filename", "filename_404") ``` The cool thing is, whenever the file is available to load while you're calling `Reload` method, it will be counted as usual. #### Ignore cases of key name 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. ```go cfg, err := ini.InsensitiveLoad("filename") //... // sec1 and sec2 are the exactly same section object sec1, err := cfg.GetSection("Section") sec2, err := cfg.GetSection("SecTIOn") // key1 and key2 are the exactly same key object key1, err := sec1.GetKey("Key") key2, err := sec2.GetKey("KeY") ``` #### MySQL-like boolean key MySQL's configuration allows a key without value as follows: ```ini [mysqld] ... skip-host-cache skip-name-resolve ``` 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: ```go cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true}, "my.cnf")) ``` 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. To generate such keys in your program, you could use `NewBooleanKey`: ```go key, err := sec.NewBooleanKey("skip-host-cache") ``` #### Comment Take care that following format will be treated as comment: 1. Line begins with `#` or `;` 2. Words after `#` or `;` 3. Words after section name (i.e words after `[some section name]`) If you want to save a value with `#` or `;`, please quote them with ``` ` ``` or ``` """ ```. Alternatively, you can use following `LoadOptions` to completely ignore inline comments: ```go cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, "app.ini")) ``` ### Working with sections To get a section, you would need to: ```go section, err := cfg.GetSection("section name") ``` For a shortcut for default section, just give an empty string as name: ```go section, err := cfg.GetSection("") ``` When you're pretty sure the section exists, following code could make your life easier: ```go section := cfg.Section("section name") ``` What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you. To create a new section: ```go err := cfg.NewSection("new section") ``` To get a list of sections or section names: ```go sections := cfg.Sections() names := cfg.SectionStrings() ``` ### Working with keys To get a key under a section: ```go key, err := cfg.Section("").GetKey("key name") ``` Same rule applies to key operations: ```go key := cfg.Section("").Key("key name") ``` To check if a key exists: ```go yes := cfg.Section("").HasKey("key name") ``` To create a new key: ```go err := cfg.Section("").NewKey("name", "value") ``` To get a list of keys or key names: ```go keys := cfg.Section("").Keys() names := cfg.Section("").KeyStrings() ``` To get a clone hash of keys and corresponding values: ```go hash := cfg.Section("").KeysHash() ``` ### Working with values To get a string value: ```go val := cfg.Section("").Key("key name").String() ``` To validate key value on the fly: ```go val := cfg.Section("").Key("key name").Validate(func(in string) string { if len(in) == 0 { return "default" } return in }) ``` 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): ```go val := cfg.Section("").Key("key name").Value() ``` To check if raw value exists: ```go yes := cfg.Section("").HasValue("test value") ``` To get value with types: ```go // For boolean values: // true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On // false when value is: 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off v, err = cfg.Section("").Key("BOOL").Bool() v, err = cfg.Section("").Key("FLOAT64").Float64() v, err = cfg.Section("").Key("INT").Int() v, err = cfg.Section("").Key("INT64").Int64() v, err = cfg.Section("").Key("UINT").Uint() v, err = cfg.Section("").Key("UINT64").Uint64() v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) v, err = cfg.Section("").Key("TIME").Time() // RFC3339 v = cfg.Section("").Key("BOOL").MustBool() v = cfg.Section("").Key("FLOAT64").MustFloat64() v = cfg.Section("").Key("INT").MustInt() v = cfg.Section("").Key("INT64").MustInt64() v = cfg.Section("").Key("UINT").MustUint() v = cfg.Section("").Key("UINT64").MustUint64() v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) v = cfg.Section("").Key("TIME").MustTime() // RFC3339 // Methods start with Must also accept one argument for default value // when key not found or fail to parse value to given type. // Except method MustString, which you have to pass a default value. v = cfg.Section("").Key("String").MustString("default") v = cfg.Section("").Key("BOOL").MustBool(true) v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) v = cfg.Section("").Key("INT").MustInt(10) v = cfg.Section("").Key("INT64").MustInt64(99) v = cfg.Section("").Key("UINT").MustUint(3) v = cfg.Section("").Key("UINT64").MustUint64(6) v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 ``` What if my value is three-line long? ```ini [advance] ADDRESS = """404 road, NotFound, State, 5000 Earth""" ``` Not a problem! ```go cfg.Section("advance").Key("ADDRESS").String() /* --- start --- 404 road, NotFound, State, 5000 Earth ------ end --- */ ``` That's cool, how about continuation lines? ```ini [advance] two_lines = how about \ continuation lines? lots_of_lines = 1 \ 2 \ 3 \ 4 ``` Piece of cake! ```go cfg.Section("advance").Key("two_lines").String() // how about continuation lines? cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 ``` Well, I hate continuation lines, how do I disable that? ```go cfg, err := ini.LoadSources(ini.LoadOptions{ IgnoreContinuation: true, }, "filename") ``` Holy crap! Note that single quotes around values will be stripped: ```ini foo = "some value" // foo: some value bar = 'some value' // bar: some value ``` 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): ```ini create_repo="created repository %s" ``` How do you transform this to regular format automatically? ```go cfg, err := ini.LoadSources(ini.LoadOptions{UnescapeValueDoubleQuotes: true}, "en-US.ini")) cfg.Section("").Key("create_repo").String() // You got: created repository %s ``` That's all? Hmm, no. #### Helper methods of working with values To get value with given candidates: ```go v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 ``` 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. To validate value in a given range: ```go vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 ``` ##### Auto-split values into a slice To use zero value of type for invalid inputs: ```go // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] // Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] vals = cfg.Section("").Key("STRINGS").Strings(",") vals = cfg.Section("").Key("FLOAT64S").Float64s(",") vals = cfg.Section("").Key("INTS").Ints(",") vals = cfg.Section("").Key("INT64S").Int64s(",") vals = cfg.Section("").Key("UINTS").Uints(",") vals = cfg.Section("").Key("UINT64S").Uint64s(",") vals = cfg.Section("").Key("TIMES").Times(",") ``` To exclude invalid values out of result slice: ```go // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] // Input: how, 2.2, are, you -> [2.2] vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") vals = cfg.Section("").Key("INTS").ValidInts(",") vals = cfg.Section("").Key("INT64S").ValidInt64s(",") vals = cfg.Section("").Key("UINTS").ValidUints(",") vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") vals = cfg.Section("").Key("TIMES").ValidTimes(",") ``` Or to return nothing but error when have invalid inputs: ```go // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] // Input: how, 2.2, are, you -> error vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") vals = cfg.Section("").Key("INTS").StrictInts(",") vals = cfg.Section("").Key("INT64S").StrictInt64s(",") vals = cfg.Section("").Key("UINTS").StrictUints(",") vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") vals = cfg.Section("").Key("TIMES").StrictTimes(",") ``` ### Save your configuration Finally, it's time to save your configuration to somewhere. A typical way to save configuration is writing it to a file: ```go // ... err = cfg.SaveTo("my.ini") err = cfg.SaveToIndent("my.ini", "\t") ``` Another way to save is writing to a `io.Writer` interface: ```go // ... cfg.WriteTo(writer) cfg.WriteToIndent(writer, "\t") ``` By default, spaces are used to align "=" sign between key and values, to disable that: ```go ini.PrettyFormat = false ``` ## Advanced Usage ### Recursive Values For all value of keys, there is a special syntax `%()s`, where `` is the key name in same section or default section, and `%()s` will be replaced by corresponding value(empty string if key not found). You can use this syntax at most 99 level of recursions. ```ini NAME = ini [author] NAME = Unknwon GITHUB = https://github.com/%(NAME)s [package] FULL_NAME = github.com/go-ini/%(NAME)s ``` ```go cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini ``` ### Parent-child Sections 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. ```ini NAME = ini VERSION = v1 IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s [package] CLONE_URL = https://%(IMPORT_PATH)s [package.sub] ``` ```go cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 ``` #### Retrieve parent keys available to a child section ```go cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] ``` ### Unparseable Sections Sometimes, you have sections that do not contain key-value pairs but raw content, to handle such case, you can use `LoadOptions.UnparsableSections`: ```go cfg, err := ini.LoadSources(ini.LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] <1> This slide has the fuel listed in the wrong units `)) body := cfg.Section("COMMENTS").Body() /* --- start --- <1> This slide has the fuel listed in the wrong units ------ end --- */ ``` ### Auto-increment Key Names 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. ```ini [features] -: Support read/write comments of keys and sections -: Support auto-increment of key names -: Support load multiple files to overwrite key values ``` ```go cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"} ``` ### Map To Struct Want more objective way to play with INI? Cool. ```ini Name = Unknwon age = 21 Male = true Born = 1993-01-01T20:17:05Z [Note] Content = Hi is a good man! Cities = HangZhou, Boston ``` ```go type Note struct { Content string Cities []string } type Person struct { Name string Age int `ini:"age"` Male bool Born time.Time Note Created time.Time `ini:"-"` } func main() { cfg, err := ini.Load("path/to/ini") // ... p := new(Person) err = cfg.MapTo(p) // ... // Things can be simpler. err = ini.MapTo(p, "path/to/ini") // ... // Just map a section? Fine. n := new(Note) err = cfg.Section("Note").MapTo(n) // ... } ``` Can I have default value for field? Absolutely. 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. ```go // ... p := &Person{ Name: "Joe", } // ... ``` It's really cool, but what's the point if you can't give me my file back from struct? ### Reflect From Struct Why not? ```go type Embeded struct { Dates []time.Time `delim:"|" comment:"Time data"` Places []string `ini:"places,omitempty"` None []int `ini:",omitempty"` } type Author struct { Name string `ini:"NAME"` Male bool Age int `comment:"Author's age"` GPA float64 NeverMind string `ini:"-"` *Embeded `comment:"Embeded section"` } func main() { a := &Author{"Unknwon", true, 21, 2.8, "", &Embeded{ []time.Time{time.Now(), time.Now()}, []string{"HangZhou", "Boston"}, []int{}, }} cfg := ini.Empty() err = ini.ReflectFrom(cfg, a) // ... } ``` So, what do I get? ```ini NAME = Unknwon Male = true ; Author's age Age = 21 GPA = 2.8 ; Embeded section [Embeded] ; Time data Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 places = HangZhou,Boston ``` #### Name Mapper 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. There are 2 built-in name mappers: - `AllCapsUnderscore`: it converts to format `ALL_CAPS_UNDERSCORE` then match section or key. - `TitleUnderscore`: it converts to format `title_underscore` then match section or key. To use them: ```go type Info struct { PackageName string } func main() { err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) // ... cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) // ... info := new(Info) cfg.NameMapper = ini.AllCapsUnderscore err = cfg.MapTo(info) // ... } ``` Same rules of name mapper apply to `ini.ReflectFromWithMapper` function. #### Value Mapper To expand values (e.g. from environment variables), you can use the `ValueMapper` to transform values: ```go type Env struct { Foo string `ini:"foo"` } func main() { cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") cfg.ValueMapper = os.ExpandEnv // ... env := &Env{} err = cfg.Section("env").MapTo(env) } ``` This would set the value of `env.Foo` to the value of the environment variable `MY_VAR`. #### Other Notes On Map/Reflect Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature: ```go type Child struct { Age string } type Parent struct { Name string Child } type Config struct { City string Parent } ``` Example configuration: ```ini City = Boston [Parent] Name = Unknwon [Child] Age = 21 ``` What if, yes, I'm paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome. ```go type Child struct { Age string } type Parent struct { Name string Child `ini:"Parent"` } type Config struct { City string Parent } ``` Example configuration: ```ini City = Boston [Parent] Name = Unknwon Age = 21 ``` ## Getting Help - [API Documentation](https://gowalker.org/gopkg.in/ini.v1) - [File An Issue](https://github.com/go-ini/ini/issues/new) ## FAQs ### What does `BlockMode` field do? 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. ### Why another INI library? 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. 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) ## License This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text. ini-1.32.0/README_ZH.md000066400000000000000000000455571320421373500142540ustar00rootroot00000000000000本包提供了 Go 语言中读写 INI 文件的功能。 ## 功能特性 - 支持覆盖加载多个数据源(`[]byte`、文件和 `io.ReadCloser`) - 支持递归读取键值 - 支持读取父子分区 - 支持读取自增键名 - 支持读取多行的键值 - 支持大量辅助方法 - 支持在读取时直接转换为 Go 语言类型 - 支持读取和 **写入** 分区和键的注释 - 轻松操作分区、键值和注释 - 在保存文件时分区和键值会保持原有的顺序 ## 下载安装 使用一个特定版本: go get gopkg.in/ini.v1 使用最新版: go get github.com/go-ini/ini 如需更新请添加 `-u` 选项。 ### 测试安装 如果您想要在自己的机器上运行测试,请使用 `-t` 标记: go get -t gopkg.in/ini.v1 如需更新请添加 `-u` 选项。 ## 开始使用 ### 从数据源加载 一个 **数据源** 可以是 `[]byte` 类型的原始数据,`string` 类型的文件路径或 `io.ReadCloser`。您可以加载 **任意多个** 数据源。如果您传递其它类型的数据源,则会直接返回错误。 ```go cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) ``` 或者从一个空白的文件开始: ```go cfg := ini.Empty() ``` 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。 ```go err := cfg.Append("other file", []byte("other raw data")) ``` 当您想要加载一系列文件,但是不能够确定其中哪些文件是不存在的,可以通过调用函数 `LooseLoad` 来忽略它们(`Load` 会因为文件不存在而返回错误): ```go cfg, err := ini.LooseLoad("filename", "filename_404") ``` 更牛逼的是,当那些之前不存在的文件在重新调用 `Reload` 方法的时候突然出现了,那么它们会被正常加载。 #### 忽略键名的大小写 有时候分区和键的名称大小写混合非常烦人,这个时候就可以通过 `InsensitiveLoad` 将所有分区和键名在读取里强制转换为小写: ```go cfg, err := ini.InsensitiveLoad("filename") //... // sec1 和 sec2 指向同一个分区对象 sec1, err := cfg.GetSection("Section") sec2, err := cfg.GetSection("SecTIOn") // key1 和 key2 指向同一个键对象 key1, err := sec1.GetKey("Key") key2, err := sec2.GetKey("KeY") ``` #### 类似 MySQL 配置中的布尔值键 MySQL 的配置文件中会出现没有具体值的布尔类型的键: ```ini [mysqld] ... skip-host-cache skip-name-resolve ``` 默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理: ```go cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true}, "my.cnf")) ``` 这些键的值永远为 `true`,且在保存到文件时也只会输出键名。 如果您想要通过程序来生成此类键,则可以使用 `NewBooleanKey`: ```go key, err := sec.NewBooleanKey("skip-host-cache") ``` #### 关于注释 下述几种情况的内容将被视为注释: 1. 所有以 `#` 或 `;` 开头的行 2. 所有在 `#` 或 `;` 之后的内容 3. 分区标签后的文字 (即 `[分区名]` 之后的内容) 如果你希望使用包含 `#` 或 `;` 的值,请使用 ``` ` ``` 或 ``` """ ``` 进行包覆。 除此之外,您还可以通过 `LoadOptions` 完全忽略行内注释: ```go cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, "app.ini")) ``` ### 操作分区(Section) 获取指定分区: ```go section, err := cfg.GetSection("section name") ``` 如果您想要获取默认分区,则可以用空字符串代替分区名: ```go section, err := cfg.GetSection("") ``` 当您非常确定某个分区是存在的,可以使用以下简便方法: ```go section := cfg.Section("section name") ``` 如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。 创建一个分区: ```go err := cfg.NewSection("new section") ``` 获取所有分区对象或名称: ```go sections := cfg.Sections() names := cfg.SectionStrings() ``` ### 操作键(Key) 获取某个分区下的键: ```go key, err := cfg.Section("").GetKey("key name") ``` 和分区一样,您也可以直接获取键而忽略错误处理: ```go key := cfg.Section("").Key("key name") ``` 判断某个键是否存在: ```go yes := cfg.Section("").HasKey("key name") ``` 创建一个新的键: ```go err := cfg.Section("").NewKey("name", "value") ``` 获取分区下的所有键或键名: ```go keys := cfg.Section("").Keys() names := cfg.Section("").KeyStrings() ``` 获取分区下的所有键值对的克隆: ```go hash := cfg.Section("").KeysHash() ``` ### 操作键值(Value) 获取一个类型为字符串(string)的值: ```go val := cfg.Section("").Key("key name").String() ``` 获取值的同时通过自定义函数进行处理验证: ```go val := cfg.Section("").Key("key name").Validate(func(in string) string { if len(in) == 0 { return "default" } return in }) ``` 如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳): ```go val := cfg.Section("").Key("key name").Value() ``` 判断某个原值是否存在: ```go yes := cfg.Section("").HasValue("test value") ``` 获取其它类型的值: ```go // 布尔值的规则: // true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On // false 当值为:0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off v, err = cfg.Section("").Key("BOOL").Bool() v, err = cfg.Section("").Key("FLOAT64").Float64() v, err = cfg.Section("").Key("INT").Int() v, err = cfg.Section("").Key("INT64").Int64() v, err = cfg.Section("").Key("UINT").Uint() v, err = cfg.Section("").Key("UINT64").Uint64() v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) v, err = cfg.Section("").Key("TIME").Time() // RFC3339 v = cfg.Section("").Key("BOOL").MustBool() v = cfg.Section("").Key("FLOAT64").MustFloat64() v = cfg.Section("").Key("INT").MustInt() v = cfg.Section("").Key("INT64").MustInt64() v = cfg.Section("").Key("UINT").MustUint() v = cfg.Section("").Key("UINT64").MustUint64() v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) v = cfg.Section("").Key("TIME").MustTime() // RFC3339 // 由 Must 开头的方法名允许接收一个相同类型的参数来作为默认值, // 当键不存在或者转换失败时,则会直接返回该默认值。 // 但是,MustString 方法必须传递一个默认值。 v = cfg.Seciont("").Key("String").MustString("default") v = cfg.Section("").Key("BOOL").MustBool(true) v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) v = cfg.Section("").Key("INT").MustInt(10) v = cfg.Section("").Key("INT64").MustInt64(99) v = cfg.Section("").Key("UINT").MustUint(3) v = cfg.Section("").Key("UINT64").MustUint64(6) v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 ``` 如果我的值有好多行怎么办? ```ini [advance] ADDRESS = """404 road, NotFound, State, 5000 Earth""" ``` 嗯哼?小 case! ```go cfg.Section("advance").Key("ADDRESS").String() /* --- start --- 404 road, NotFound, State, 5000 Earth ------ end --- */ ``` 赞爆了!那要是我属于一行的内容写不下想要写到第二行怎么办? ```ini [advance] two_lines = how about \ continuation lines? lots_of_lines = 1 \ 2 \ 3 \ 4 ``` 简直是小菜一碟! ```go cfg.Section("advance").Key("two_lines").String() // how about continuation lines? cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 ``` 可是我有时候觉得两行连在一起特别没劲,怎么才能不自动连接两行呢? ```go cfg, err := ini.LoadSources(ini.LoadOptions{ IgnoreContinuation: true, }, "filename") ``` 哇靠给力啊! 需要注意的是,值两侧的单引号会被自动剔除: ```ini foo = "some value" // foo: some value bar = 'some value' // bar: some value ``` 有时您会获得像从 [Crowdin](https://crowdin.com/) 网站下载的文件那样具有特殊格式的值(值使用双引号括起来,内部的双引号被转义): ```ini create_repo="创建了仓库 %s" ``` 那么,怎么自动地将这类值进行处理呢? ```go cfg, err := ini.LoadSources(ini.LoadOptions{UnescapeValueDoubleQuotes: true}, "en-US.ini")) cfg.Section("").Key("create_repo").String() // You got: 创建了仓库 %s ``` 这就是全部了?哈哈,当然不是。 #### 操作键值的辅助方法 获取键值时设定候选值: ```go v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 ``` 如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。 验证获取的值是否在指定范围内: ```go vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 ``` ##### 自动分割键值到切片(slice) 当存在无效输入时,使用零值代替: ```go // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] // Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] vals = cfg.Section("").Key("STRINGS").Strings(",") vals = cfg.Section("").Key("FLOAT64S").Float64s(",") vals = cfg.Section("").Key("INTS").Ints(",") vals = cfg.Section("").Key("INT64S").Int64s(",") vals = cfg.Section("").Key("UINTS").Uints(",") vals = cfg.Section("").Key("UINT64S").Uint64s(",") vals = cfg.Section("").Key("TIMES").Times(",") ``` 从结果切片中剔除无效输入: ```go // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] // Input: how, 2.2, are, you -> [2.2] vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") vals = cfg.Section("").Key("INTS").ValidInts(",") vals = cfg.Section("").Key("INT64S").ValidInt64s(",") vals = cfg.Section("").Key("UINTS").ValidUints(",") vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") vals = cfg.Section("").Key("TIMES").ValidTimes(",") ``` 当存在无效输入时,直接返回错误: ```go // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] // Input: how, 2.2, are, you -> error vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") vals = cfg.Section("").Key("INTS").StrictInts(",") vals = cfg.Section("").Key("INT64S").StrictInt64s(",") vals = cfg.Section("").Key("UINTS").StrictUints(",") vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") vals = cfg.Section("").Key("TIMES").StrictTimes(",") ``` ### 保存配置 终于到了这个时刻,是时候保存一下配置了。 比较原始的做法是输出配置到某个文件: ```go // ... err = cfg.SaveTo("my.ini") err = cfg.SaveToIndent("my.ini", "\t") ``` 另一个比较高级的做法是写入到任何实现 `io.Writer` 接口的对象中: ```go // ... cfg.WriteTo(writer) cfg.WriteToIndent(writer, "\t") ``` 默认情况下,空格将被用于对齐键值之间的等号以美化输出结果,以下代码可以禁用该功能: ```go ini.PrettyFormat = false ``` ## 高级用法 ### 递归读取键值 在获取所有键值的过程中,特殊语法 `%()s` 会被应用,其中 `` 可以是相同分区或者默认分区下的键名。字符串 `%()s` 会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代。您可以最多使用 99 层的递归嵌套。 ```ini NAME = ini [author] NAME = Unknwon GITHUB = https://github.com/%(NAME)s [package] FULL_NAME = github.com/go-ini/%(NAME)s ``` ```go cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini ``` ### 读取父子分区 您可以在分区名称中使用 `.` 来表示两个或多个分区之间的父子关系。如果某个键在子分区中不存在,则会去它的父分区中再次寻找,直到没有父分区为止。 ```ini NAME = ini VERSION = v1 IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s [package] CLONE_URL = https://%(IMPORT_PATH)s [package.sub] ``` ```go cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 ``` #### 获取上级父分区下的所有键名 ```go cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] ``` ### 无法解析的分区 如果遇到一些比较特殊的分区,它们不包含常见的键值对,而是没有固定格式的纯文本,则可以使用 `LoadOptions.UnparsableSections` 进行处理: ```go cfg, err := LoadSources(ini.LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] <1> This slide has the fuel listed in the wrong units `)) body := cfg.Section("COMMENTS").Body() /* --- start --- <1> This slide has the fuel listed in the wrong units ------ end --- */ ``` ### 读取自增键名 如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。 ```ini [features] -: Support read/write comments of keys and sections -: Support auto-increment of key names -: Support load multiple files to overwrite key values ``` ```go cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"} ``` ### 映射到结构 想要使用更加面向对象的方式玩转 INI 吗?好主意。 ```ini Name = Unknwon age = 21 Male = true Born = 1993-01-01T20:17:05Z [Note] Content = Hi is a good man! Cities = HangZhou, Boston ``` ```go type Note struct { Content string Cities []string } type Person struct { Name string Age int `ini:"age"` Male bool Born time.Time Note Created time.Time `ini:"-"` } func main() { cfg, err := ini.Load("path/to/ini") // ... p := new(Person) err = cfg.MapTo(p) // ... // 一切竟可以如此的简单。 err = ini.MapTo(p, "path/to/ini") // ... // 嗯哼?只需要映射一个分区吗? n := new(Note) err = cfg.Section("Note").MapTo(n) // ... } ``` 结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。 ```go // ... p := &Person{ Name: "Joe", } // ... ``` 这样玩 INI 真的好酷啊!然而,如果不能还给我原来的配置文件,有什么卵用? ### 从结构反射 可是,我有说不能吗? ```go type Embeded struct { Dates []time.Time `delim:"|" comment:"Time data"` Places []string `ini:"places,omitempty"` None []int `ini:",omitempty"` } type Author struct { Name string `ini:"NAME"` Male bool Age int `comment:"Author's age"` GPA float64 NeverMind string `ini:"-"` *Embeded `comment:"Embeded section"` } func main() { a := &Author{"Unknwon", true, 21, 2.8, "", &Embeded{ []time.Time{time.Now(), time.Now()}, []string{"HangZhou", "Boston"}, []int{}, }} cfg := ini.Empty() err = ini.ReflectFrom(cfg, a) // ... } ``` 瞧瞧,奇迹发生了。 ```ini NAME = Unknwon Male = true ; Author's age Age = 21 GPA = 2.8 ; Embeded section [Embeded] ; Time data Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 places = HangZhou,Boston ``` #### 名称映射器(Name Mapper) 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。 目前有 2 款内置的映射器: - `AllCapsUnderscore`:该映射器将字段名转换至格式 `ALL_CAPS_UNDERSCORE` 后再去匹配分区名和键名。 - `TitleUnderscore`:该映射器将字段名转换至格式 `title_underscore` 后再去匹配分区名和键名。 使用方法: ```go type Info struct{ PackageName string } func main() { err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) // ... cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) // ... info := new(Info) cfg.NameMapper = ini.AllCapsUnderscore err = cfg.MapTo(info) // ... } ``` 使用函数 `ini.ReflectFromWithMapper` 时也可应用相同的规则。 #### 值映射器(Value Mapper) 值映射器允许使用一个自定义函数自动展开值的具体内容,例如:运行时获取环境变量: ```go type Env struct { Foo string `ini:"foo"` } func main() { cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") cfg.ValueMapper = os.ExpandEnv // ... env := &Env{} err = cfg.Section("env").MapTo(env) } ``` 本例中,`env.Foo` 将会是运行时所获取到环境变量 `MY_VAR` 的值。 #### 映射/反射的其它说明 任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联: ```go type Child struct { Age string } type Parent struct { Name string Child } type Config struct { City string Parent } ``` 示例配置文件: ```ini City = Boston [Parent] Name = Unknwon [Child] Age = 21 ``` 很好,但是,我就是要嵌入结构也在同一个分区。好吧,你爹是李刚! ```go type Child struct { Age string } type Parent struct { Name string Child `ini:"Parent"` } type Config struct { City string Parent } ``` 示例配置文件: ```ini City = Boston [Parent] Name = Unknwon Age = 21 ``` ## 获取帮助 - [API 文档](https://gowalker.org/gopkg.in/ini.v1) - [创建工单](https://github.com/go-ini/ini/issues/new) ## 常见问题 ### 字段 `BlockMode` 是什么? 默认情况下,本库会在您进行读写操作时采用锁机制来确保数据时间。但在某些情况下,您非常确定只进行读操作。此时,您可以通过设置 `cfg.BlockMode = false` 来将读操作提升大约 **50-70%** 的性能。 ### 为什么要写另一个 INI 解析库? 许多人都在使用我的 [goconfig](https://github.com/Unknwon/goconfig) 来完成对 INI 文件的操作,但我希望使用更加 Go 风格的代码。并且当您设置 `cfg.BlockMode = false` 时,会有大约 **10-30%** 的性能提升。 为了做出这些改变,我必须对 API 进行破坏,所以新开一个仓库是最安全的做法。除此之外,本库直接使用 `gopkg.in` 来进行版本化发布。(其实真相是导入路径更短了) ini-1.32.0/bench_test.go000066400000000000000000000051011320421373500150160ustar00rootroot00000000000000// Copyright 2017 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini_test import ( "testing" "gopkg.in/ini.v1" ) func newTestFile(block bool) *ini.File { c, _ := ini.Load([]byte(_CONF_DATA)) c.BlockMode = block return c } func Benchmark_Key_Value(b *testing.B) { c := newTestFile(true) for i := 0; i < b.N; i++ { c.Section("").Key("NAME").Value() } } func Benchmark_Key_Value_NonBlock(b *testing.B) { c := newTestFile(false) for i := 0; i < b.N; i++ { c.Section("").Key("NAME").Value() } } func Benchmark_Key_Value_ViaSection(b *testing.B) { c := newTestFile(true) sec := c.Section("") for i := 0; i < b.N; i++ { sec.Key("NAME").Value() } } func Benchmark_Key_Value_ViaSection_NonBlock(b *testing.B) { c := newTestFile(false) sec := c.Section("") for i := 0; i < b.N; i++ { sec.Key("NAME").Value() } } func Benchmark_Key_Value_Direct(b *testing.B) { c := newTestFile(true) key := c.Section("").Key("NAME") for i := 0; i < b.N; i++ { key.Value() } } func Benchmark_Key_Value_Direct_NonBlock(b *testing.B) { c := newTestFile(false) key := c.Section("").Key("NAME") for i := 0; i < b.N; i++ { key.Value() } } func Benchmark_Key_String(b *testing.B) { c := newTestFile(true) for i := 0; i < b.N; i++ { _ = c.Section("").Key("NAME").String() } } func Benchmark_Key_String_NonBlock(b *testing.B) { c := newTestFile(false) for i := 0; i < b.N; i++ { _ = c.Section("").Key("NAME").String() } } func Benchmark_Key_String_ViaSection(b *testing.B) { c := newTestFile(true) sec := c.Section("") for i := 0; i < b.N; i++ { _ = sec.Key("NAME").String() } } func Benchmark_Key_String_ViaSection_NonBlock(b *testing.B) { c := newTestFile(false) sec := c.Section("") for i := 0; i < b.N; i++ { _ = sec.Key("NAME").String() } } func Benchmark_Key_SetValue(b *testing.B) { c := newTestFile(true) for i := 0; i < b.N; i++ { c.Section("").Key("NAME").SetValue("10") } } func Benchmark_Key_SetValue_VisSection(b *testing.B) { c := newTestFile(true) sec := c.Section("") for i := 0; i < b.N; i++ { sec.Key("NAME").SetValue("10") } } ini-1.32.0/error.go000066400000000000000000000015631320421373500140410ustar00rootroot00000000000000// Copyright 2016 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini import ( "fmt" ) type ErrDelimiterNotFound struct { Line string } func IsErrDelimiterNotFound(err error) bool { _, ok := err.(ErrDelimiterNotFound) return ok } func (err ErrDelimiterNotFound) Error() string { return fmt.Sprintf("key-value delimiter not found: %s", err.Line) } ini-1.32.0/file.go000066400000000000000000000231351320421373500136260ustar00rootroot00000000000000// Copyright 2017 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini import ( "bytes" "errors" "fmt" "io" "io/ioutil" "os" "strings" "sync" ) // File represents a combination of a or more INI file(s) in memory. type File struct { options LoadOptions dataSources []dataSource // Should make things safe, but sometimes doesn't matter. BlockMode bool lock sync.RWMutex // To keep data in order. sectionList []string // Actual data is stored here. sections map[string]*Section NameMapper ValueMapper } // newFile initializes File object with given data sources. func newFile(dataSources []dataSource, opts LoadOptions) *File { return &File{ BlockMode: true, dataSources: dataSources, sections: make(map[string]*Section), sectionList: make([]string, 0, 10), options: opts, } } // Empty returns an empty file object. func Empty() *File { // Ignore error here, we sure our data is good. f, _ := Load([]byte("")) return f } // NewSection creates a new section. func (f *File) NewSection(name string) (*Section, error) { if len(name) == 0 { return nil, errors.New("error creating new section: empty section name") } else if f.options.Insensitive && name != DEFAULT_SECTION { name = strings.ToLower(name) } if f.BlockMode { f.lock.Lock() defer f.lock.Unlock() } if inSlice(name, f.sectionList) { return f.sections[name], nil } f.sectionList = append(f.sectionList, name) f.sections[name] = newSection(f, name) return f.sections[name], nil } // NewRawSection creates a new section with an unparseable body. func (f *File) NewRawSection(name, body string) (*Section, error) { section, err := f.NewSection(name) if err != nil { return nil, err } section.isRawSection = true section.rawBody = body return section, nil } // NewSections creates a list of sections. func (f *File) NewSections(names ...string) (err error) { for _, name := range names { if _, err = f.NewSection(name); err != nil { return err } } return nil } // GetSection returns section by given name. func (f *File) GetSection(name string) (*Section, error) { if len(name) == 0 { name = DEFAULT_SECTION } if f.options.Insensitive { name = strings.ToLower(name) } if f.BlockMode { f.lock.RLock() defer f.lock.RUnlock() } sec := f.sections[name] if sec == nil { return nil, fmt.Errorf("section '%s' does not exist", name) } return sec, nil } // Section assumes named section exists and returns a zero-value when not. func (f *File) Section(name string) *Section { sec, err := f.GetSection(name) if err != nil { // Note: It's OK here because the only possible error is empty section name, // but if it's empty, this piece of code won't be executed. sec, _ = f.NewSection(name) return sec } return sec } // Section returns list of Section. func (f *File) Sections() []*Section { sections := make([]*Section, len(f.sectionList)) for i := range f.sectionList { sections[i] = f.Section(f.sectionList[i]) } return sections } // ChildSections returns a list of child sections of given section name. func (f *File) ChildSections(name string) []*Section { return f.Section(name).ChildSections() } // SectionStrings returns list of section names. func (f *File) SectionStrings() []string { list := make([]string, len(f.sectionList)) copy(list, f.sectionList) return list } // DeleteSection deletes a section. func (f *File) DeleteSection(name string) { if f.BlockMode { f.lock.Lock() defer f.lock.Unlock() } if len(name) == 0 { name = DEFAULT_SECTION } for i, s := range f.sectionList { if s == name { f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...) delete(f.sections, name) return } } } func (f *File) reload(s dataSource) error { r, err := s.ReadCloser() if err != nil { return err } defer r.Close() return f.parse(r) } // Reload reloads and parses all data sources. func (f *File) Reload() (err error) { for _, s := range f.dataSources { if err = f.reload(s); err != nil { // In loose mode, we create an empty default section for nonexistent files. if os.IsNotExist(err) && f.options.Loose { f.parse(bytes.NewBuffer(nil)) continue } return err } } return nil } // Append appends one or more data sources and reloads automatically. func (f *File) Append(source interface{}, others ...interface{}) error { ds, err := parseDataSource(source) if err != nil { return err } f.dataSources = append(f.dataSources, ds) for _, s := range others { ds, err = parseDataSource(s) if err != nil { return err } f.dataSources = append(f.dataSources, ds) } return f.Reload() } func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) { equalSign := "=" if PrettyFormat { equalSign = " = " } // Use buffer to make sure target is safe until finish encoding. buf := bytes.NewBuffer(nil) for i, sname := range f.sectionList { sec := f.Section(sname) if len(sec.Comment) > 0 { if sec.Comment[0] != '#' && sec.Comment[0] != ';' { sec.Comment = "; " + sec.Comment } else { sec.Comment = sec.Comment[:1] + " " + strings.TrimSpace(sec.Comment[1:]) } if _, err := buf.WriteString(sec.Comment + LineBreak); err != nil { return nil, err } } if i > 0 || DefaultHeader { if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil { return nil, err } } else { // Write nothing if default section is empty if len(sec.keyList) == 0 { continue } } if sec.isRawSection { if _, err := buf.WriteString(sec.rawBody); err != nil { return nil, err } if PrettySection { // Put a line between sections if _, err := buf.WriteString(LineBreak); err != nil { return nil, err } } continue } // Count and generate alignment length and buffer spaces using the // longest key. Keys may be modifed if they contain certain characters so // we need to take that into account in our calculation. alignLength := 0 if PrettyFormat { for _, kname := range sec.keyList { keyLength := len(kname) // First case will surround key by ` and second by """ if strings.ContainsAny(kname, "\"=:") { keyLength += 2 } else if strings.Contains(kname, "`") { keyLength += 6 } if keyLength > alignLength { alignLength = keyLength } } } alignSpaces := bytes.Repeat([]byte(" "), alignLength) KEY_LIST: for _, kname := range sec.keyList { key := sec.Key(kname) if len(key.Comment) > 0 { if len(indent) > 0 && sname != DEFAULT_SECTION { buf.WriteString(indent) } if key.Comment[0] != '#' && key.Comment[0] != ';' { key.Comment = "; " + key.Comment } else { key.Comment = key.Comment[:1] + " " + strings.TrimSpace(key.Comment[1:]) } if _, err := buf.WriteString(key.Comment + LineBreak); err != nil { return nil, err } } if len(indent) > 0 && sname != DEFAULT_SECTION { buf.WriteString(indent) } switch { case key.isAutoIncrement: kname = "-" case strings.ContainsAny(kname, "\"=:"): kname = "`" + kname + "`" case strings.Contains(kname, "`"): kname = `"""` + kname + `"""` } for _, val := range key.ValueWithShadows() { if _, err := buf.WriteString(kname); err != nil { return nil, err } if key.isBooleanType { if kname != sec.keyList[len(sec.keyList)-1] { buf.WriteString(LineBreak) } continue KEY_LIST } // Write out alignment spaces before "=" sign if PrettyFormat { buf.Write(alignSpaces[:alignLength-len(kname)]) } // In case key value contains "\n", "`", "\"", "#" or ";" if strings.ContainsAny(val, "\n`") { val = `"""` + val + `"""` } else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") { val = "`" + val + "`" } if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil { return nil, err } } for _, val := range key.nestedValues { if _, err := buf.WriteString(indent + " " + val + LineBreak); err != nil { return nil, err } } } if PrettySection { // Put a line between sections if _, err := buf.WriteString(LineBreak); err != nil { return nil, err } } } return buf, nil } // WriteToIndent writes content into io.Writer with given indention. // If PrettyFormat has been set to be true, // it will align "=" sign with spaces under each section. func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) { buf, err := f.writeToBuffer(indent) if err != nil { return 0, err } return buf.WriteTo(w) } // WriteTo writes file content into io.Writer. func (f *File) WriteTo(w io.Writer) (int64, error) { return f.WriteToIndent(w, "") } // SaveToIndent writes content to file system with given value indention. func (f *File) SaveToIndent(filename, indent string) error { // Note: Because we are truncating with os.Create, // so it's safer to save to a temporary file location and rename afte done. buf, err := f.writeToBuffer(indent) if err != nil { return err } return ioutil.WriteFile(filename, buf.Bytes(), 0666) } // SaveTo writes content to file system. func (f *File) SaveTo(filename string) error { return f.SaveToIndent(filename, "") } ini-1.32.0/file_test.go000066400000000000000000000222341320421373500146640ustar00rootroot00000000000000// Copyright 2017 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini_test import ( "bytes" "testing" . "github.com/smartystreets/goconvey/convey" "gopkg.in/ini.v1" ) func TestEmpty(t *testing.T) { Convey("Create an empty object", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) // Should only have the default section So(len(f.Sections()), ShouldEqual, 1) // Default section should not contain any key So(len(f.Section("").Keys()), ShouldBeZeroValue) }) } func TestFile_NewSection(t *testing.T) { Convey("Create a new section", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) sec, err := f.NewSection("author") So(err, ShouldBeNil) So(sec, ShouldNotBeNil) So(sec.Name(), ShouldEqual, "author") So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "author"}) Convey("With duplicated name", func() { sec, err := f.NewSection("author") So(err, ShouldBeNil) So(sec, ShouldNotBeNil) // Does nothing if section already exists So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "author"}) }) Convey("With empty string", func() { _, err := f.NewSection("") So(err, ShouldNotBeNil) }) }) } func TestFile_NewRawSection(t *testing.T) { Convey("Create a new raw section", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000 111111111111111111100000000000111000000000`) So(err, ShouldBeNil) So(sec, ShouldNotBeNil) So(sec.Name(), ShouldEqual, "comments") So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "comments"}) So(f.Section("comments").Body(), ShouldEqual, `1111111111111111111000000000000000001110000 111111111111111111100000000000111000000000`) Convey("With duplicated name", func() { sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000`) So(err, ShouldBeNil) So(sec, ShouldNotBeNil) So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "comments"}) // Overwrite previous existed section So(f.Section("comments").Body(), ShouldEqual, `1111111111111111111000000000000000001110000`) }) Convey("With empty string", func() { _, err := f.NewRawSection("", "") So(err, ShouldNotBeNil) }) }) } func TestFile_NewSections(t *testing.T) { Convey("Create new sections", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) So(f.NewSections("package", "author"), ShouldBeNil) So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "package", "author"}) Convey("With duplicated name", func() { So(f.NewSections("author", "features"), ShouldBeNil) // Ignore section already exists So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "package", "author", "features"}) }) Convey("With empty string", func() { So(f.NewSections("", ""), ShouldNotBeNil) }) }) } func TestFile_GetSection(t *testing.T) { Convey("Get a section", t, func() { f, err := ini.Load(_FULL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) sec, err := f.GetSection("author") So(err, ShouldBeNil) So(sec, ShouldNotBeNil) So(sec.Name(), ShouldEqual, "author") Convey("Section not exists", func() { _, err := f.GetSection("404") So(err, ShouldNotBeNil) }) }) } func TestFile_Section(t *testing.T) { Convey("Get a section", t, func() { f, err := ini.Load(_FULL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) sec := f.Section("author") So(sec, ShouldNotBeNil) So(sec.Name(), ShouldEqual, "author") Convey("Section not exists", func() { sec := f.Section("404") So(sec, ShouldNotBeNil) So(sec.Name(), ShouldEqual, "404") }) }) Convey("Get default section in lower case with insensitive load", t, func() { f, err := ini.InsensitiveLoad([]byte(` [default] NAME = ini VERSION = v1`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("name").String(), ShouldEqual, "ini") So(f.Section("").Key("version").String(), ShouldEqual, "v1") }) } func TestFile_Sections(t *testing.T) { Convey("Get all sections", t, func() { f, err := ini.Load(_FULL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) secs := f.Sections() names := []string{ini.DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} So(len(secs), ShouldEqual, len(names)) for i, name := range names { So(secs[i].Name(), ShouldEqual, name) } }) } func TestFile_ChildSections(t *testing.T) { Convey("Get child sections by parent name", t, func() { f, err := ini.Load([]byte(` [node] [node.biz1] [node.biz2] [node.biz3] [node.bizN] `)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) children := f.ChildSections("node") names := []string{"node.biz1", "node.biz2", "node.biz3", "node.bizN"} So(len(children), ShouldEqual, len(names)) for i, name := range names { So(children[i].Name(), ShouldEqual, name) } }) } func TestFile_SectionStrings(t *testing.T) { Convey("Get all section names", t, func() { f, err := ini.Load(_FULL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.SectionStrings(), ShouldResemble, []string{ini.DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"}) }) } func TestFile_DeleteSection(t *testing.T) { Convey("Delete a section", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) f.NewSections("author", "package", "features") f.DeleteSection("features") f.DeleteSection("") So(f.SectionStrings(), ShouldResemble, []string{"author", "package"}) }) } func TestFile_Append(t *testing.T) { Convey("Append a data source", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) So(f.Append(_MINIMAL_CONF, []byte(` [author] NAME = Unknwon`)), ShouldBeNil) Convey("With bad input", func() { So(f.Append(123), ShouldNotBeNil) So(f.Append(_MINIMAL_CONF, 123), ShouldNotBeNil) }) }) } func TestFile_WriteTo(t *testing.T) { Convey("Write content to somewhere", t, func() { f, err := ini.Load(_FULL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) f.Section("author").Comment = `Information about package author # Bio can be written in multiple lines.` f.Section("author").Key("NAME").Comment = "This is author name" f.Section("note").NewBooleanKey("boolean_key") f.Section("note").NewKey("more", "notes") var buf bytes.Buffer _, err = f.WriteTo(&buf) So(err, ShouldBeNil) So(buf.String(), ShouldEqual, `; Package name NAME = ini ; Package version VERSION = v1 ; Package import path IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s ; Information about package author # Bio can be written in multiple lines. [author] ; This is author name NAME = Unknwon E-MAIL = u@gogs.io GITHUB = https://github.com/%(NAME)s # Succeeding comment BIO = """Gopher. Coding addict. Good man. """ [package] CLONE_URL = https://%(IMPORT_PATH)s [package.sub] UNUSED_KEY = should be deleted [features] - = Support read/write comments of keys and sections - = Support auto-increment of key names - = Support load multiple files to overwrite key values [types] STRING = str BOOL = true BOOL_FALSE = false FLOAT64 = 1.25 INT = 10 TIME = 2015-01-01T20:17:05Z DURATION = 2h45m UINT = 3 [array] STRINGS = en, zh, de FLOAT64S = 1.1, 2.2, 3.3 INTS = 1, 2, 3 UINTS = 1, 2, 3 TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z [note] empty_lines = next line is empty boolean_key more = notes ; Comment before the section ; This is a comment for the section too [comments] ; Comment before key key = value ; This is a comment for key2 key2 = value2 key3 = "one", "two", "three" [string escapes] key1 = value1, value2, value3 key2 = value1\, value2 key3 = val\ue1, value2 key4 = value1\\, value\\\\2 key5 = value1\,, value2 key6 = aaa bbb\ and\ space ccc [advance] value with quotes = some value value quote2 again = some value includes comment sign = `+"`"+"my#password"+"`"+` includes comment sign2 = `+"`"+"my;password"+"`"+` true = 2+3=5 `+"`"+`1+1=2`+"`"+` = true `+"`"+`6+1=7`+"`"+` = true """`+"`"+`5+5`+"`"+`""" = 10 `+"`"+`"6+6"`+"`"+` = 12 `+"`"+`7-2=4`+"`"+` = false ADDRESS = """404 road, NotFound, State, 50000""" two_lines = how about continuation lines? lots_of_lines = 1 2 3 4 `) }) } func TestFile_SaveTo(t *testing.T) { Convey("Write content to somewhere", t, func() { f, err := ini.Load(_FULL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.SaveTo("testdata/conf_out.ini"), ShouldBeNil) So(f.SaveToIndent("testdata/conf_out.ini", "\t"), ShouldBeNil) }) } ini-1.32.0/ini.go000066400000000000000000000142151320421373500134650ustar00rootroot00000000000000// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // Package ini provides INI file read and write functionality in Go. package ini import ( "bytes" "fmt" "io" "io/ioutil" "os" "regexp" "runtime" ) const ( // Name for default section. You can use this constant or the string literal. // In most of cases, an empty string is all you need to access the section. DEFAULT_SECTION = "DEFAULT" // Maximum allowed depth when recursively substituing variable names. _DEPTH_VALUES = 99 _VERSION = "1.32.0" ) // Version returns current package version literal. func Version() string { return _VERSION } var ( // Delimiter to determine or compose a new line. // This variable will be changed to "\r\n" automatically on Windows // at package init time. LineBreak = "\n" // Variable regexp pattern: %(variable)s varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`) // Indicate whether to align "=" sign with spaces to produce pretty output // or reduce all possible spaces for compact format. PrettyFormat = true // Explicitly write DEFAULT section header DefaultHeader = false // Indicate whether to put a line between sections PrettySection = true ) func init() { if runtime.GOOS == "windows" { LineBreak = "\r\n" } } func inSlice(str string, s []string) bool { for _, v := range s { if str == v { return true } } return false } // dataSource is an interface that returns object which can be read and closed. type dataSource interface { ReadCloser() (io.ReadCloser, error) } // sourceFile represents an object that contains content on the local file system. type sourceFile struct { name string } func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { return os.Open(s.name) } // sourceData represents an object that contains content in memory. type sourceData struct { data []byte } func (s *sourceData) ReadCloser() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(s.data)), nil } // sourceReadCloser represents an input stream with Close method. type sourceReadCloser struct { reader io.ReadCloser } func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) { return s.reader, nil } func parseDataSource(source interface{}) (dataSource, error) { switch s := source.(type) { case string: return sourceFile{s}, nil case []byte: return &sourceData{s}, nil case io.ReadCloser: return &sourceReadCloser{s}, nil default: return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s) } } type LoadOptions struct { // Loose indicates whether the parser should ignore nonexistent files or return error. Loose bool // Insensitive indicates whether the parser forces all section and key names to lowercase. Insensitive bool // IgnoreContinuation indicates whether to ignore continuation lines while parsing. IgnoreContinuation bool // IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value. IgnoreInlineComment bool // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing. // This type of keys are mostly used in my.cnf. AllowBooleanKeys bool // AllowShadows indicates whether to keep track of keys with same name under same section. AllowShadows bool // AllowNestedValues indicates whether to allow AWS-like nested values. // Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values AllowNestedValues bool // UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format // when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value" UnescapeValueDoubleQuotes bool // UnescapeValueCommentSymbols indicates to unescape comment symbols (\# and \;) inside value to regular format // when value is NOT surrounded by any quotes. // Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all. UnescapeValueCommentSymbols bool // Some INI formats allow group blocks that store a block of raw content that doesn't otherwise // conform to key/value pairs. Specify the names of those blocks here. UnparseableSections []string } func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) { sources := make([]dataSource, len(others)+1) sources[0], err = parseDataSource(source) if err != nil { return nil, err } for i := range others { sources[i+1], err = parseDataSource(others[i]) if err != nil { return nil, err } } f := newFile(sources, opts) if err = f.Reload(); err != nil { return nil, err } return f, nil } // Load loads and parses from INI data sources. // Arguments can be mixed of file name with string type, or raw data in []byte. // It will return error if list contains nonexistent files. func Load(source interface{}, others ...interface{}) (*File, error) { return LoadSources(LoadOptions{}, source, others...) } // LooseLoad has exactly same functionality as Load function // except it ignores nonexistent files instead of returning error. func LooseLoad(source interface{}, others ...interface{}) (*File, error) { return LoadSources(LoadOptions{Loose: true}, source, others...) } // InsensitiveLoad has exactly same functionality as Load function // except it forces all section and key names to be lowercased. func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) { return LoadSources(LoadOptions{Insensitive: true}, source, others...) } // InsensitiveLoad has exactly same functionality as Load function // except it allows have shadow keys. func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { return LoadSources(LoadOptions{AllowShadows: true}, source, others...) } ini-1.32.0/ini_internal_test.go000066400000000000000000000017331320421373500164210ustar00rootroot00000000000000// Copyright 2017 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini import ( "testing" . "github.com/smartystreets/goconvey/convey" ) func Test_Version(t *testing.T) { Convey("Get version", t, func() { So(Version(), ShouldEqual, _VERSION) }) } func Test_isSlice(t *testing.T) { Convey("Check if a string is in the slice", t, func() { ss := []string{"a", "b", "c"} So(inSlice("a", ss), ShouldBeTrue) So(inSlice("d", ss), ShouldBeFalse) }) } ini-1.32.0/ini_test.go000066400000000000000000000225721320421373500145310ustar00rootroot00000000000000// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini_test import ( "bytes" "io/ioutil" "testing" . "github.com/smartystreets/goconvey/convey" "gopkg.in/ini.v1" ) const ( _CONF_DATA = ` ; Package name NAME = ini ; Package version VERSION = v1 ; Package import path IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s # Information about package author # Bio can be written in multiple lines. [author] NAME = Unknwon ; Succeeding comment E-MAIL = fake@localhost GITHUB = https://github.com/%(NAME)s BIO = """Gopher. Coding addict. Good man. """ # Succeeding comment` _MINIMAL_CONF = "testdata/minimal.ini" _FULL_CONF = "testdata/full.ini" _NOT_FOUND_CONF = "testdata/404.ini" ) func TestLoad(t *testing.T) { Convey("Load from good data sources", t, func() { f, err := ini.Load([]byte(` NAME = ini VERSION = v1 IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s`), "testdata/minimal.ini", ioutil.NopCloser(bytes.NewReader([]byte(` [author] NAME = Unknwon `))), ) So(err, ShouldBeNil) So(f, ShouldNotBeNil) // Vaildate values make sure all sources are loaded correctly sec := f.Section("") So(sec.Key("NAME").String(), ShouldEqual, "ini") So(sec.Key("VERSION").String(), ShouldEqual, "v1") So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "gopkg.in/ini.v1") sec = f.Section("author") So(sec.Key("NAME").String(), ShouldEqual, "Unknwon") So(sec.Key("E-MAIL").String(), ShouldEqual, "u@gogs.io") }) Convey("Load from bad data sources", t, func() { Convey("Invalid input", func() { _, err := ini.Load(_NOT_FOUND_CONF) So(err, ShouldNotBeNil) }) Convey("Unsupported type", func() { _, err := ini.Load(123) So(err, ShouldNotBeNil) }) }) } func TestLoadSources(t *testing.T) { Convey("Load from data sources with options", t, func() { Convey("Ignore nonexistent files", func() { f, err := ini.LooseLoad(_NOT_FOUND_CONF, _MINIMAL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) Convey("Inverse case", func() { _, err = ini.Load(_NOT_FOUND_CONF) So(err, ShouldNotBeNil) }) }) Convey("Insensitive to section and key names", func() { f, err := ini.InsensitiveLoad(_MINIMAL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io") Convey("Write out", func() { var buf bytes.Buffer _, err := f.WriteTo(&buf) So(err, ShouldBeNil) So(buf.String(), ShouldEqual, `[author] e-mail = u@gogs.io `) }) Convey("Inverse case", func() { f, err := ini.Load(_MINIMAL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty) }) }) Convey("Ignore continuation lines", func() { f, err := ini.LoadSources(ini.LoadOptions{ IgnoreContinuation: true, }, []byte(` key1=a\b\ key2=c\d\ key3=value`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("key1").String(), ShouldEqual, `a\b\`) So(f.Section("").Key("key2").String(), ShouldEqual, `c\d\`) So(f.Section("").Key("key3").String(), ShouldEqual, "value") Convey("Inverse case", func() { f, err := ini.Load([]byte(` key1=a\b\ key2=c\d\`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("key1").String(), ShouldEqual, `a\bkey2=c\d`) }) }) Convey("Ignore inline comments", func() { f, err := ini.LoadSources(ini.LoadOptions{ IgnoreInlineComment: true, }, []byte(` key1=value ;comment key2=value2 #comment2`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("key1").String(), ShouldEqual, `value ;comment`) So(f.Section("").Key("key2").String(), ShouldEqual, `value2 #comment2`) Convey("Inverse case", func() { f, err := ini.Load([]byte(` key1=value ;comment key2=value2 #comment2`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("key1").String(), ShouldEqual, `value`) So(f.Section("").Key("key1").Comment, ShouldEqual, `;comment`) So(f.Section("").Key("key2").String(), ShouldEqual, `value2`) So(f.Section("").Key("key2").Comment, ShouldEqual, `#comment2`) }) }) Convey("Allow boolean type keys", func() { f, err := ini.LoadSources(ini.LoadOptions{ AllowBooleanKeys: true, }, []byte(` key1=hello #key2 key3`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").KeyStrings(), ShouldResemble, []string{"key1", "key3"}) So(f.Section("").Key("key3").MustBool(false), ShouldBeTrue) Convey("Write out", func() { var buf bytes.Buffer _, err := f.WriteTo(&buf) So(err, ShouldBeNil) So(buf.String(), ShouldEqual, `key1 = hello # key2 key3 `) }) Convey("Inverse case", func() { _, err := ini.Load([]byte(` key1=hello #key2 key3`)) So(err, ShouldNotBeNil) }) }) Convey("Allow shadow keys", func() { f, err := ini.ShadowLoad([]byte(` [remote "origin"] url = https://github.com/Antergone/test1.git url = https://github.com/Antergone/test2.git fetch = +refs/heads/*:refs/remotes/origin/*`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test1.git") So(f.Section(`remote "origin"`).Key("url").ValueWithShadows(), ShouldResemble, []string{ "https://github.com/Antergone/test1.git", "https://github.com/Antergone/test2.git", }) So(f.Section(`remote "origin"`).Key("fetch").String(), ShouldEqual, "+refs/heads/*:refs/remotes/origin/*") Convey("Write out", func() { var buf bytes.Buffer _, err := f.WriteTo(&buf) So(err, ShouldBeNil) So(buf.String(), ShouldEqual, `[remote "origin"] url = https://github.com/Antergone/test1.git url = https://github.com/Antergone/test2.git fetch = +refs/heads/*:refs/remotes/origin/* `) }) Convey("Inverse case", func() { f, err := ini.Load([]byte(` [remote "origin"] url = https://github.com/Antergone/test1.git url = https://github.com/Antergone/test2.git`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test2.git") }) }) Convey("Unescape double quotes inside value", func() { f, err := ini.LoadSources(ini.LoadOptions{ UnescapeValueDoubleQuotes: true, }, []byte(` create_repo="创建了仓库 %s"`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("create_repo").String(), ShouldEqual, `创建了仓库 %s`) Convey("Inverse case", func() { f, err := ini.Load([]byte(` create_repo="创建了仓库 %s"`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("create_repo").String(), ShouldEqual, `"创建了仓库 %s"`) }) }) Convey("Unescape comment symbols inside value", func() { f, err := ini.LoadSources(ini.LoadOptions{ IgnoreInlineComment: true, UnescapeValueCommentSymbols: true, }, []byte(` key = test value more text `)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("key").String(), ShouldEqual, `test value more text`) }) Convey("Allow unparseable sections", func() { f, err := ini.LoadSources(ini.LoadOptions{ Insensitive: true, UnparseableSections: []string{"core_lesson", "comments"}, }, []byte(` Lesson_Location = 87 Lesson_Status = C Score = 3 Time = 00:02:30 [CORE_LESSON] my lesson state data – 1111111111111111111000000000000000001110000 111111111111111111100000000000111000000000 – end my lesson state data [COMMENTS] <1> This slide has the fuel listed in the wrong units `)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("score").String(), ShouldEqual, "3") So(f.Section("").Body(), ShouldBeEmpty) So(f.Section("core_lesson").Body(), ShouldEqual, `my lesson state data – 1111111111111111111000000000000000001110000 111111111111111111100000000000111000000000 – end my lesson state data`) So(f.Section("comments").Body(), ShouldEqual, `<1> This slide has the fuel listed in the wrong units `) Convey("Write out", func() { var buf bytes.Buffer _, err := f.WriteTo(&buf) So(err, ShouldBeNil) So(buf.String(), ShouldEqual, `lesson_location = 87 lesson_status = C score = 3 time = 00:02:30 [core_lesson] my lesson state data – 1111111111111111111000000000000000001110000 111111111111111111100000000000111000000000 – end my lesson state data [comments] <1> This slide has the fuel listed in the wrong units `) }) Convey("Inverse case", func() { _, err := ini.Load([]byte(` [CORE_LESSON] my lesson state data – 1111111111111111111000000000000000001110000 111111111111111111100000000000111000000000 – end my lesson state data`)) So(err, ShouldNotBeNil) }) }) }) } ini-1.32.0/key.go000066400000000000000000000522711320421373500135020ustar00rootroot00000000000000// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini import ( "bytes" "errors" "fmt" "strconv" "strings" "time" ) // Key represents a key under a section. type Key struct { s *Section Comment string name string value string isAutoIncrement bool isBooleanType bool isShadow bool shadows []*Key nestedValues []string } // newKey simply return a key object with given values. func newKey(s *Section, name, val string) *Key { return &Key{ s: s, name: name, value: val, } } func (k *Key) addShadow(val string) error { if k.isShadow { return errors.New("cannot add shadow to another shadow key") } else if k.isAutoIncrement || k.isBooleanType { return errors.New("cannot add shadow to auto-increment or boolean key") } shadow := newKey(k.s, k.name, val) shadow.isShadow = true k.shadows = append(k.shadows, shadow) return nil } // AddShadow adds a new shadow key to itself. func (k *Key) AddShadow(val string) error { if !k.s.f.options.AllowShadows { return errors.New("shadow key is not allowed") } return k.addShadow(val) } func (k *Key) addNestedValue(val string) error { if k.isAutoIncrement || k.isBooleanType { return errors.New("cannot add nested value to auto-increment or boolean key") } k.nestedValues = append(k.nestedValues, val) return nil } func (k *Key) AddNestedValue(val string) error { if !k.s.f.options.AllowNestedValues { return errors.New("nested value is not allowed") } return k.addNestedValue(val) } // ValueMapper represents a mapping function for values, e.g. os.ExpandEnv type ValueMapper func(string) string // Name returns name of key. func (k *Key) Name() string { return k.name } // Value returns raw value of key for performance purpose. func (k *Key) Value() string { return k.value } // ValueWithShadows returns raw values of key and its shadows if any. func (k *Key) ValueWithShadows() []string { if len(k.shadows) == 0 { return []string{k.value} } vals := make([]string, len(k.shadows)+1) vals[0] = k.value for i := range k.shadows { vals[i+1] = k.shadows[i].value } return vals } // NestedValues returns nested values stored in the key. // It is possible returned value is nil if no nested values stored in the key. func (k *Key) NestedValues() []string { return k.nestedValues } // transformValue takes a raw value and transforms to its final string. func (k *Key) transformValue(val string) string { if k.s.f.ValueMapper != nil { val = k.s.f.ValueMapper(val) } // Fail-fast if no indicate char found for recursive value if !strings.Contains(val, "%") { return val } for i := 0; i < _DEPTH_VALUES; i++ { vr := varPattern.FindString(val) if len(vr) == 0 { break } // Take off leading '%(' and trailing ')s'. noption := strings.TrimLeft(vr, "%(") noption = strings.TrimRight(noption, ")s") // Search in the same section. nk, err := k.s.GetKey(noption) if err != nil || k == nk { // Search again in default section. nk, _ = k.s.f.Section("").GetKey(noption) } // Substitute by new value and take off leading '%(' and trailing ')s'. val = strings.Replace(val, vr, nk.value, -1) } return val } // String returns string representation of value. func (k *Key) String() string { return k.transformValue(k.value) } // Validate accepts a validate function which can // return modifed result as key value. func (k *Key) Validate(fn func(string) string) string { return fn(k.String()) } // parseBool returns the boolean value represented by the string. // // It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On, // 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off. // Any other value returns an error. func parseBool(str string) (value bool, err error) { switch str { case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On": return true, nil case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off": return false, nil } return false, fmt.Errorf("parsing \"%s\": invalid syntax", str) } // Bool returns bool type value. func (k *Key) Bool() (bool, error) { return parseBool(k.String()) } // Float64 returns float64 type value. func (k *Key) Float64() (float64, error) { return strconv.ParseFloat(k.String(), 64) } // Int returns int type value. func (k *Key) Int() (int, error) { return strconv.Atoi(k.String()) } // Int64 returns int64 type value. func (k *Key) Int64() (int64, error) { return strconv.ParseInt(k.String(), 10, 64) } // Uint returns uint type valued. func (k *Key) Uint() (uint, error) { u, e := strconv.ParseUint(k.String(), 10, 64) return uint(u), e } // Uint64 returns uint64 type value. func (k *Key) Uint64() (uint64, error) { return strconv.ParseUint(k.String(), 10, 64) } // Duration returns time.Duration type value. func (k *Key) Duration() (time.Duration, error) { return time.ParseDuration(k.String()) } // TimeFormat parses with given format and returns time.Time type value. func (k *Key) TimeFormat(format string) (time.Time, error) { return time.Parse(format, k.String()) } // Time parses with RFC3339 format and returns time.Time type value. func (k *Key) Time() (time.Time, error) { return k.TimeFormat(time.RFC3339) } // MustString returns default value if key value is empty. func (k *Key) MustString(defaultVal string) string { val := k.String() if len(val) == 0 { k.value = defaultVal return defaultVal } return val } // MustBool always returns value without error, // it returns false if error occurs. func (k *Key) MustBool(defaultVal ...bool) bool { val, err := k.Bool() if len(defaultVal) > 0 && err != nil { k.value = strconv.FormatBool(defaultVal[0]) return defaultVal[0] } return val } // MustFloat64 always returns value without error, // it returns 0.0 if error occurs. func (k *Key) MustFloat64(defaultVal ...float64) float64 { val, err := k.Float64() if len(defaultVal) > 0 && err != nil { k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64) return defaultVal[0] } return val } // MustInt always returns value without error, // it returns 0 if error occurs. func (k *Key) MustInt(defaultVal ...int) int { val, err := k.Int() if len(defaultVal) > 0 && err != nil { k.value = strconv.FormatInt(int64(defaultVal[0]), 10) return defaultVal[0] } return val } // MustInt64 always returns value without error, // it returns 0 if error occurs. func (k *Key) MustInt64(defaultVal ...int64) int64 { val, err := k.Int64() if len(defaultVal) > 0 && err != nil { k.value = strconv.FormatInt(defaultVal[0], 10) return defaultVal[0] } return val } // MustUint always returns value without error, // it returns 0 if error occurs. func (k *Key) MustUint(defaultVal ...uint) uint { val, err := k.Uint() if len(defaultVal) > 0 && err != nil { k.value = strconv.FormatUint(uint64(defaultVal[0]), 10) return defaultVal[0] } return val } // MustUint64 always returns value without error, // it returns 0 if error occurs. func (k *Key) MustUint64(defaultVal ...uint64) uint64 { val, err := k.Uint64() if len(defaultVal) > 0 && err != nil { k.value = strconv.FormatUint(defaultVal[0], 10) return defaultVal[0] } return val } // MustDuration always returns value without error, // it returns zero value if error occurs. func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration { val, err := k.Duration() if len(defaultVal) > 0 && err != nil { k.value = defaultVal[0].String() return defaultVal[0] } return val } // MustTimeFormat always parses with given format and returns value without error, // it returns zero value if error occurs. func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time { val, err := k.TimeFormat(format) if len(defaultVal) > 0 && err != nil { k.value = defaultVal[0].Format(format) return defaultVal[0] } return val } // MustTime always parses with RFC3339 format and returns value without error, // it returns zero value if error occurs. func (k *Key) MustTime(defaultVal ...time.Time) time.Time { return k.MustTimeFormat(time.RFC3339, defaultVal...) } // In always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) In(defaultVal string, candidates []string) string { val := k.String() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InFloat64 always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 { val := k.MustFloat64() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InInt always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InInt(defaultVal int, candidates []int) int { val := k.MustInt() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InInt64 always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 { val := k.MustInt64() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InUint always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InUint(defaultVal uint, candidates []uint) uint { val := k.MustUint() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InUint64 always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 { val := k.MustUint64() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InTimeFormat always parses with given format and returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time { val := k.MustTimeFormat(format) for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InTime always parses with RFC3339 format and returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time { return k.InTimeFormat(time.RFC3339, defaultVal, candidates) } // RangeFloat64 checks if value is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 { val := k.MustFloat64() if val < min || val > max { return defaultVal } return val } // RangeInt checks if value is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeInt(defaultVal, min, max int) int { val := k.MustInt() if val < min || val > max { return defaultVal } return val } // RangeInt64 checks if value is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeInt64(defaultVal, min, max int64) int64 { val := k.MustInt64() if val < min || val > max { return defaultVal } return val } // RangeTimeFormat checks if value with given format is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time { val := k.MustTimeFormat(format) if val.Unix() < min.Unix() || val.Unix() > max.Unix() { return defaultVal } return val } // RangeTime checks if value with RFC3339 format is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time { return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max) } // Strings returns list of string divided by given delimiter. func (k *Key) Strings(delim string) []string { str := k.String() if len(str) == 0 { return []string{} } runes := []rune(str) vals := make([]string, 0, 2) var buf bytes.Buffer escape := false idx := 0 for { if escape { escape = false if runes[idx] != '\\' && !strings.HasPrefix(string(runes[idx:]), delim) { buf.WriteRune('\\') } buf.WriteRune(runes[idx]) } else { if runes[idx] == '\\' { escape = true } else if strings.HasPrefix(string(runes[idx:]), delim) { idx += len(delim) - 1 vals = append(vals, strings.TrimSpace(buf.String())) buf.Reset() } else { buf.WriteRune(runes[idx]) } } idx += 1 if idx == len(runes) { break } } if buf.Len() > 0 { vals = append(vals, strings.TrimSpace(buf.String())) } return vals } // StringsWithShadows returns list of string divided by given delimiter. // Shadows will also be appended if any. func (k *Key) StringsWithShadows(delim string) []string { vals := k.ValueWithShadows() results := make([]string, 0, len(vals)*2) for i := range vals { if len(vals) == 0 { continue } results = append(results, strings.Split(vals[i], delim)...) } for i := range results { results[i] = k.transformValue(strings.TrimSpace(results[i])) } return results } // Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value. func (k *Key) Float64s(delim string) []float64 { vals, _ := k.parseFloat64s(k.Strings(delim), true, false) return vals } // Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value. func (k *Key) Ints(delim string) []int { vals, _ := k.parseInts(k.Strings(delim), true, false) return vals } // Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value. func (k *Key) Int64s(delim string) []int64 { vals, _ := k.parseInt64s(k.Strings(delim), true, false) return vals } // Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value. func (k *Key) Uints(delim string) []uint { vals, _ := k.parseUints(k.Strings(delim), true, false) return vals } // Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value. func (k *Key) Uint64s(delim string) []uint64 { vals, _ := k.parseUint64s(k.Strings(delim), true, false) return vals } // TimesFormat parses with given format and returns list of time.Time divided by given delimiter. // Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). func (k *Key) TimesFormat(format, delim string) []time.Time { vals, _ := k.parseTimesFormat(format, k.Strings(delim), true, false) return vals } // Times parses with RFC3339 format and returns list of time.Time divided by given delimiter. // Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). func (k *Key) Times(delim string) []time.Time { return k.TimesFormat(time.RFC3339, delim) } // ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then // it will not be included to result list. func (k *Key) ValidFloat64s(delim string) []float64 { vals, _ := k.parseFloat64s(k.Strings(delim), false, false) return vals } // ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will // not be included to result list. func (k *Key) ValidInts(delim string) []int { vals, _ := k.parseInts(k.Strings(delim), false, false) return vals } // ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer, // then it will not be included to result list. func (k *Key) ValidInt64s(delim string) []int64 { vals, _ := k.parseInt64s(k.Strings(delim), false, false) return vals } // ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer, // then it will not be included to result list. func (k *Key) ValidUints(delim string) []uint { vals, _ := k.parseUints(k.Strings(delim), false, false) return vals } // ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned // integer, then it will not be included to result list. func (k *Key) ValidUint64s(delim string) []uint64 { vals, _ := k.parseUint64s(k.Strings(delim), false, false) return vals } // ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter. func (k *Key) ValidTimesFormat(format, delim string) []time.Time { vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false) return vals } // ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter. func (k *Key) ValidTimes(delim string) []time.Time { return k.ValidTimesFormat(time.RFC3339, delim) } // StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input. func (k *Key) StrictFloat64s(delim string) ([]float64, error) { return k.parseFloat64s(k.Strings(delim), false, true) } // StrictInts returns list of int divided by given delimiter or error on first invalid input. func (k *Key) StrictInts(delim string) ([]int, error) { return k.parseInts(k.Strings(delim), false, true) } // StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input. func (k *Key) StrictInt64s(delim string) ([]int64, error) { return k.parseInt64s(k.Strings(delim), false, true) } // StrictUints returns list of uint divided by given delimiter or error on first invalid input. func (k *Key) StrictUints(delim string) ([]uint, error) { return k.parseUints(k.Strings(delim), false, true) } // StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input. func (k *Key) StrictUint64s(delim string) ([]uint64, error) { return k.parseUint64s(k.Strings(delim), false, true) } // StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter // or error on first invalid input. func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) { return k.parseTimesFormat(format, k.Strings(delim), false, true) } // StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter // or error on first invalid input. func (k *Key) StrictTimes(delim string) ([]time.Time, error) { return k.StrictTimesFormat(time.RFC3339, delim) } // parseFloat64s transforms strings to float64s. func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) { vals := make([]float64, 0, len(strs)) for _, str := range strs { val, err := strconv.ParseFloat(str, 64) if err != nil && returnOnInvalid { return nil, err } if err == nil || addInvalid { vals = append(vals, val) } } return vals, nil } // parseInts transforms strings to ints. func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) { vals := make([]int, 0, len(strs)) for _, str := range strs { val, err := strconv.Atoi(str) if err != nil && returnOnInvalid { return nil, err } if err == nil || addInvalid { vals = append(vals, val) } } return vals, nil } // parseInt64s transforms strings to int64s. func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) { vals := make([]int64, 0, len(strs)) for _, str := range strs { val, err := strconv.ParseInt(str, 10, 64) if err != nil && returnOnInvalid { return nil, err } if err == nil || addInvalid { vals = append(vals, val) } } return vals, nil } // parseUints transforms strings to uints. func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) { vals := make([]uint, 0, len(strs)) for _, str := range strs { val, err := strconv.ParseUint(str, 10, 0) if err != nil && returnOnInvalid { return nil, err } if err == nil || addInvalid { vals = append(vals, uint(val)) } } return vals, nil } // parseUint64s transforms strings to uint64s. func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) { vals := make([]uint64, 0, len(strs)) for _, str := range strs { val, err := strconv.ParseUint(str, 10, 64) if err != nil && returnOnInvalid { return nil, err } if err == nil || addInvalid { vals = append(vals, val) } } return vals, nil } // parseTimesFormat transforms strings to times in given format. func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) { vals := make([]time.Time, 0, len(strs)) for _, str := range strs { val, err := time.Parse(format, str) if err != nil && returnOnInvalid { return nil, err } if err == nil || addInvalid { vals = append(vals, val) } } return vals, nil } // SetValue changes key value. func (k *Key) SetValue(v string) { if k.s.f.BlockMode { k.s.f.lock.Lock() defer k.s.f.lock.Unlock() } k.value = v k.s.keysHash[k.name] = v } ini-1.32.0/key_test.go000066400000000000000000000405531320421373500145410ustar00rootroot00000000000000// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini_test import ( "bytes" "fmt" "strings" "testing" "time" . "github.com/smartystreets/goconvey/convey" "gopkg.in/ini.v1" ) func TestKey_AddShadow(t *testing.T) { Convey("Add shadow to a key", t, func() { f, err := ini.ShadowLoad([]byte(` [notes] -: note1`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.AddShadow("ini.v1"), ShouldBeNil) So(k.ValueWithShadows(), ShouldResemble, []string{"ini", "ini.v1"}) Convey("Add shadow to boolean key", func() { k, err := f.Section("").NewBooleanKey("published") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.AddShadow("beta"), ShouldNotBeNil) }) Convey("Add shadow to auto-increment key", func() { So(f.Section("notes").Key("#1").AddShadow("beta"), ShouldNotBeNil) }) }) Convey("Shadow is not allowed", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.AddShadow("ini.v1"), ShouldNotBeNil) }) } // Helpers for slice tests. func float64sEqual(values []float64, expected ...float64) { So(values, ShouldHaveLength, len(expected)) for i, v := range expected { So(values[i], ShouldEqual, v) } } func intsEqual(values []int, expected ...int) { So(values, ShouldHaveLength, len(expected)) for i, v := range expected { So(values[i], ShouldEqual, v) } } func int64sEqual(values []int64, expected ...int64) { So(values, ShouldHaveLength, len(expected)) for i, v := range expected { So(values[i], ShouldEqual, v) } } func uintsEqual(values []uint, expected ...uint) { So(values, ShouldHaveLength, len(expected)) for i, v := range expected { So(values[i], ShouldEqual, v) } } func uint64sEqual(values []uint64, expected ...uint64) { So(values, ShouldHaveLength, len(expected)) for i, v := range expected { So(values[i], ShouldEqual, v) } } func timesEqual(values []time.Time, expected ...time.Time) { So(values, ShouldHaveLength, len(expected)) for i, v := range expected { So(values[i].String(), ShouldEqual, v.String()) } } func TestKey_Helpers(t *testing.T) { Convey("Getting and setting values", t, func() { f, err := ini.Load(_FULL_CONF) So(err, ShouldBeNil) So(f, ShouldNotBeNil) Convey("Get string representation", func() { sec := f.Section("") So(sec, ShouldNotBeNil) So(sec.Key("NAME").Value(), ShouldEqual, "ini") So(sec.Key("NAME").String(), ShouldEqual, "ini") So(sec.Key("NAME").Validate(func(in string) string { return in }), ShouldEqual, "ini") So(sec.Key("NAME").Comment, ShouldEqual, "; Package name") So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "gopkg.in/ini.v1") Convey("With ValueMapper", func() { f.ValueMapper = func(in string) string { if in == "gopkg.in/%(NAME)s.%(VERSION)s" { return "github.com/go-ini/ini" } return in } So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "github.com/go-ini/ini") }) }) Convey("Get values in non-default section", func() { sec := f.Section("author") So(sec, ShouldNotBeNil) So(sec.Key("NAME").String(), ShouldEqual, "Unknwon") So(sec.Key("GITHUB").String(), ShouldEqual, "https://github.com/Unknwon") sec = f.Section("package") So(sec, ShouldNotBeNil) So(sec.Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") }) Convey("Get auto-increment key names", func() { keys := f.Section("features").Keys() for i, k := range keys { So(k.Name(), ShouldEqual, fmt.Sprintf("#%d", i+1)) } }) Convey("Get parent-keys that are available to the child section", func() { parentKeys := f.Section("package.sub").ParentKeys() for _, k := range parentKeys { So(k.Name(), ShouldEqual, "CLONE_URL") } }) Convey("Get overwrite value", func() { So(f.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io") }) Convey("Get sections", func() { sections := f.Sections() for i, name := range []string{ini.DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} { So(sections[i].Name(), ShouldEqual, name) } }) Convey("Get parent section value", func() { So(f.Section("package.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") So(f.Section("package.fake.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") }) Convey("Get multiple line value", func() { So(f.Section("author").Key("BIO").String(), ShouldEqual, "Gopher.\nCoding addict.\nGood man.\n") }) Convey("Get values with type", func() { sec := f.Section("types") v1, err := sec.Key("BOOL").Bool() So(err, ShouldBeNil) So(v1, ShouldBeTrue) v1, err = sec.Key("BOOL_FALSE").Bool() So(err, ShouldBeNil) So(v1, ShouldBeFalse) v2, err := sec.Key("FLOAT64").Float64() So(err, ShouldBeNil) So(v2, ShouldEqual, 1.25) v3, err := sec.Key("INT").Int() So(err, ShouldBeNil) So(v3, ShouldEqual, 10) v4, err := sec.Key("INT").Int64() So(err, ShouldBeNil) So(v4, ShouldEqual, 10) v5, err := sec.Key("UINT").Uint() So(err, ShouldBeNil) So(v5, ShouldEqual, 3) v6, err := sec.Key("UINT").Uint64() So(err, ShouldBeNil) So(v6, ShouldEqual, 3) t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") So(err, ShouldBeNil) v7, err := sec.Key("TIME").Time() So(err, ShouldBeNil) So(v7.String(), ShouldEqual, t.String()) Convey("Must get values with type", func() { So(sec.Key("STRING").MustString("404"), ShouldEqual, "str") So(sec.Key("BOOL").MustBool(), ShouldBeTrue) So(sec.Key("FLOAT64").MustFloat64(), ShouldEqual, 1.25) So(sec.Key("INT").MustInt(), ShouldEqual, 10) So(sec.Key("INT").MustInt64(), ShouldEqual, 10) So(sec.Key("UINT").MustUint(), ShouldEqual, 3) So(sec.Key("UINT").MustUint64(), ShouldEqual, 3) So(sec.Key("TIME").MustTime().String(), ShouldEqual, t.String()) dur, err := time.ParseDuration("2h45m") So(err, ShouldBeNil) So(sec.Key("DURATION").MustDuration().Seconds(), ShouldEqual, dur.Seconds()) Convey("Must get values with default value", func() { So(sec.Key("STRING_404").MustString("404"), ShouldEqual, "404") So(sec.Key("BOOL_404").MustBool(true), ShouldBeTrue) So(sec.Key("FLOAT64_404").MustFloat64(2.5), ShouldEqual, 2.5) So(sec.Key("INT_404").MustInt(15), ShouldEqual, 15) So(sec.Key("INT64_404").MustInt64(15), ShouldEqual, 15) So(sec.Key("UINT_404").MustUint(6), ShouldEqual, 6) So(sec.Key("UINT64_404").MustUint64(6), ShouldEqual, 6) t, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z") So(err, ShouldBeNil) So(sec.Key("TIME_404").MustTime(t).String(), ShouldEqual, t.String()) So(sec.Key("DURATION_404").MustDuration(dur).Seconds(), ShouldEqual, dur.Seconds()) Convey("Must should set default as key value", func() { So(sec.Key("STRING_404").String(), ShouldEqual, "404") So(sec.Key("BOOL_404").String(), ShouldEqual, "true") So(sec.Key("FLOAT64_404").String(), ShouldEqual, "2.5") So(sec.Key("INT_404").String(), ShouldEqual, "15") So(sec.Key("INT64_404").String(), ShouldEqual, "15") So(sec.Key("UINT_404").String(), ShouldEqual, "6") So(sec.Key("UINT64_404").String(), ShouldEqual, "6") So(sec.Key("TIME_404").String(), ShouldEqual, "2014-01-01T20:17:05Z") So(sec.Key("DURATION_404").String(), ShouldEqual, "2h45m0s") }) }) }) }) Convey("Get value with candidates", func() { sec := f.Section("types") So(sec.Key("STRING").In("", []string{"str", "arr", "types"}), ShouldEqual, "str") So(sec.Key("FLOAT64").InFloat64(0, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25) So(sec.Key("INT").InInt(0, []int{10, 20, 30}), ShouldEqual, 10) So(sec.Key("INT").InInt64(0, []int64{10, 20, 30}), ShouldEqual, 10) So(sec.Key("UINT").InUint(0, []uint{3, 6, 9}), ShouldEqual, 3) So(sec.Key("UINT").InUint64(0, []uint64{3, 6, 9}), ShouldEqual, 3) zt, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z") So(err, ShouldBeNil) t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") So(err, ShouldBeNil) So(sec.Key("TIME").InTime(zt, []time.Time{t, time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String()) Convey("Get value with candidates and default value", func() { So(sec.Key("STRING_404").In("str", []string{"str", "arr", "types"}), ShouldEqual, "str") So(sec.Key("FLOAT64_404").InFloat64(1.25, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25) So(sec.Key("INT_404").InInt(10, []int{10, 20, 30}), ShouldEqual, 10) So(sec.Key("INT64_404").InInt64(10, []int64{10, 20, 30}), ShouldEqual, 10) So(sec.Key("UINT_404").InUint(3, []uint{3, 6, 9}), ShouldEqual, 3) So(sec.Key("UINT_404").InUint64(3, []uint64{3, 6, 9}), ShouldEqual, 3) So(sec.Key("TIME_404").InTime(t, []time.Time{time.Now(), time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String()) }) }) Convey("Get values in range", func() { sec := f.Section("types") So(sec.Key("FLOAT64").RangeFloat64(0, 1, 2), ShouldEqual, 1.25) So(sec.Key("INT").RangeInt(0, 10, 20), ShouldEqual, 10) So(sec.Key("INT").RangeInt64(0, 10, 20), ShouldEqual, 10) minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z") So(err, ShouldBeNil) midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z") So(err, ShouldBeNil) maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z") So(err, ShouldBeNil) t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") So(err, ShouldBeNil) So(sec.Key("TIME").RangeTime(t, minT, maxT).String(), ShouldEqual, t.String()) Convey("Get value in range with default value", func() { So(sec.Key("FLOAT64").RangeFloat64(5, 0, 1), ShouldEqual, 5) So(sec.Key("INT").RangeInt(7, 0, 5), ShouldEqual, 7) So(sec.Key("INT").RangeInt64(7, 0, 5), ShouldEqual, 7) So(sec.Key("TIME").RangeTime(t, minT, midT).String(), ShouldEqual, t.String()) }) }) Convey("Get values into slice", func() { sec := f.Section("array") So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de") So(len(sec.Key("STRINGS_404").Strings(",")), ShouldEqual, 0) vals1 := sec.Key("FLOAT64S").Float64s(",") float64sEqual(vals1, 1.1, 2.2, 3.3) vals2 := sec.Key("INTS").Ints(",") intsEqual(vals2, 1, 2, 3) vals3 := sec.Key("INTS").Int64s(",") int64sEqual(vals3, 1, 2, 3) vals4 := sec.Key("UINTS").Uints(",") uintsEqual(vals4, 1, 2, 3) vals5 := sec.Key("UINTS").Uint64s(",") uint64sEqual(vals5, 1, 2, 3) t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") So(err, ShouldBeNil) vals6 := sec.Key("TIMES").Times(",") timesEqual(vals6, t, t, t) }) Convey("Test string slice escapes", func() { sec := f.Section("string escapes") So(sec.Key("key1").Strings(","), ShouldResemble, []string{"value1", "value2", "value3"}) So(sec.Key("key2").Strings(","), ShouldResemble, []string{"value1, value2"}) So(sec.Key("key3").Strings(","), ShouldResemble, []string{`val\ue1`, "value2"}) So(sec.Key("key4").Strings(","), ShouldResemble, []string{`value1\`, `value\\2`}) So(sec.Key("key5").Strings(",,"), ShouldResemble, []string{"value1,, value2"}) So(sec.Key("key6").Strings(" "), ShouldResemble, []string{"aaa", "bbb and space", "ccc"}) }) Convey("Get valid values into slice", func() { sec := f.Section("array") vals1 := sec.Key("FLOAT64S").ValidFloat64s(",") float64sEqual(vals1, 1.1, 2.2, 3.3) vals2 := sec.Key("INTS").ValidInts(",") intsEqual(vals2, 1, 2, 3) vals3 := sec.Key("INTS").ValidInt64s(",") int64sEqual(vals3, 1, 2, 3) vals4 := sec.Key("UINTS").ValidUints(",") uintsEqual(vals4, 1, 2, 3) vals5 := sec.Key("UINTS").ValidUint64s(",") uint64sEqual(vals5, 1, 2, 3) t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") So(err, ShouldBeNil) vals6 := sec.Key("TIMES").ValidTimes(",") timesEqual(vals6, t, t, t) }) Convey("Get values one type into slice of another type", func() { sec := f.Section("array") vals1 := sec.Key("STRINGS").ValidFloat64s(",") So(vals1, ShouldBeEmpty) vals2 := sec.Key("STRINGS").ValidInts(",") So(vals2, ShouldBeEmpty) vals3 := sec.Key("STRINGS").ValidInt64s(",") So(vals3, ShouldBeEmpty) vals4 := sec.Key("STRINGS").ValidUints(",") So(vals4, ShouldBeEmpty) vals5 := sec.Key("STRINGS").ValidUint64s(",") So(vals5, ShouldBeEmpty) vals6 := sec.Key("STRINGS").ValidTimes(",") So(vals6, ShouldBeEmpty) }) Convey("Get valid values into slice without errors", func() { sec := f.Section("array") vals1, err := sec.Key("FLOAT64S").StrictFloat64s(",") So(err, ShouldBeNil) float64sEqual(vals1, 1.1, 2.2, 3.3) vals2, err := sec.Key("INTS").StrictInts(",") So(err, ShouldBeNil) intsEqual(vals2, 1, 2, 3) vals3, err := sec.Key("INTS").StrictInt64s(",") So(err, ShouldBeNil) int64sEqual(vals3, 1, 2, 3) vals4, err := sec.Key("UINTS").StrictUints(",") So(err, ShouldBeNil) uintsEqual(vals4, 1, 2, 3) vals5, err := sec.Key("UINTS").StrictUint64s(",") So(err, ShouldBeNil) uint64sEqual(vals5, 1, 2, 3) t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") So(err, ShouldBeNil) vals6, err := sec.Key("TIMES").StrictTimes(",") So(err, ShouldBeNil) timesEqual(vals6, t, t, t) }) Convey("Get invalid values into slice", func() { sec := f.Section("array") vals1, err := sec.Key("STRINGS").StrictFloat64s(",") So(vals1, ShouldBeEmpty) So(err, ShouldNotBeNil) vals2, err := sec.Key("STRINGS").StrictInts(",") So(vals2, ShouldBeEmpty) So(err, ShouldNotBeNil) vals3, err := sec.Key("STRINGS").StrictInt64s(",") So(vals3, ShouldBeEmpty) So(err, ShouldNotBeNil) vals4, err := sec.Key("STRINGS").StrictUints(",") So(vals4, ShouldBeEmpty) So(err, ShouldNotBeNil) vals5, err := sec.Key("STRINGS").StrictUint64s(",") So(vals5, ShouldBeEmpty) So(err, ShouldNotBeNil) vals6, err := sec.Key("STRINGS").StrictTimes(",") So(vals6, ShouldBeEmpty) So(err, ShouldNotBeNil) }) }) } func TestKey_StringsWithShadows(t *testing.T) { Convey("Get strings of shadows of a key", t, func() { f, err := ini.ShadowLoad([]byte("")) So(err, ShouldBeNil) So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NUMS", "1,2") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("").NewKey("NUMS", "4,5,6") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.StringsWithShadows(","), ShouldResemble, []string{"1", "2", "4", "5", "6"}) }) } func TestKey_SetValue(t *testing.T) { Convey("Set value of key", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.Value(), ShouldEqual, "ini") k.SetValue("ini.v1") So(k.Value(), ShouldEqual, "ini.v1") }) } func TestKey_NestedValues(t *testing.T) { Convey("Read and write nested values", t, func() { f, err := ini.LoadSources(ini.LoadOptions{ AllowNestedValues: true, }, []byte(` aws_access_key_id = foo aws_secret_access_key = bar region = us-west-2 s3 = max_concurrent_requests=10 max_queue_size=1000`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("").Key("s3").NestedValues(), ShouldResemble, []string{"max_concurrent_requests=10", "max_queue_size=1000"}) var buf bytes.Buffer _, err = f.WriteTo(&buf) So(err, ShouldBeNil) So(buf.String(), ShouldEqual, `aws_access_key_id = foo aws_secret_access_key = bar region = us-west-2 s3 = max_concurrent_requests=10 max_queue_size=1000 `) }) } func TestRecursiveValues(t *testing.T) { Convey("Recursive values should not reflect on same key", t, func() { f, err := ini.Load([]byte(` NAME = ini [package] NAME = %(NAME)s`)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("package").Key("NAME").String(), ShouldEqual, "ini") }) } ini-1.32.0/parser.go000066400000000000000000000224461320421373500142070ustar00rootroot00000000000000// Copyright 2015 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini import ( "bufio" "bytes" "fmt" "io" "strconv" "strings" "unicode" ) type tokenType int const ( _TOKEN_INVALID tokenType = iota _TOKEN_COMMENT _TOKEN_SECTION _TOKEN_KEY ) type parser struct { buf *bufio.Reader isEOF bool count int comment *bytes.Buffer } func newParser(r io.Reader) *parser { return &parser{ buf: bufio.NewReader(r), count: 1, comment: &bytes.Buffer{}, } } // BOM handles header of UTF-8, UTF-16 LE and UTF-16 BE's BOM format. // http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding func (p *parser) BOM() error { mask, err := p.buf.Peek(2) if err != nil && err != io.EOF { return err } else if len(mask) < 2 { return nil } switch { case mask[0] == 254 && mask[1] == 255: fallthrough case mask[0] == 255 && mask[1] == 254: p.buf.Read(mask) case mask[0] == 239 && mask[1] == 187: mask, err := p.buf.Peek(3) if err != nil && err != io.EOF { return err } else if len(mask) < 3 { return nil } if mask[2] == 191 { p.buf.Read(mask) } } return nil } func (p *parser) readUntil(delim byte) ([]byte, error) { data, err := p.buf.ReadBytes(delim) if err != nil { if err == io.EOF { p.isEOF = true } else { return nil, err } } return data, nil } func cleanComment(in []byte) ([]byte, bool) { i := bytes.IndexAny(in, "#;") if i == -1 { return nil, false } return in[i:], true } func readKeyName(in []byte) (string, int, error) { line := string(in) // Check if key name surrounded by quotes. var keyQuote string if line[0] == '"' { if len(line) > 6 && string(line[0:3]) == `"""` { keyQuote = `"""` } else { keyQuote = `"` } } else if line[0] == '`' { keyQuote = "`" } // Get out key name endIdx := -1 if len(keyQuote) > 0 { startIdx := len(keyQuote) // FIXME: fail case -> """"""name"""=value pos := strings.Index(line[startIdx:], keyQuote) if pos == -1 { return "", -1, fmt.Errorf("missing closing key quote: %s", line) } pos += startIdx // Find key-value delimiter i := strings.IndexAny(line[pos+startIdx:], "=:") if i < 0 { return "", -1, ErrDelimiterNotFound{line} } endIdx = pos + i return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil } endIdx = strings.IndexAny(line, "=:") if endIdx < 0 { return "", -1, ErrDelimiterNotFound{line} } return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil } func (p *parser) readMultilines(line, val, valQuote string) (string, error) { for { data, err := p.readUntil('\n') if err != nil { return "", err } next := string(data) pos := strings.LastIndex(next, valQuote) if pos > -1 { val += next[:pos] comment, has := cleanComment([]byte(next[pos:])) if has { p.comment.Write(bytes.TrimSpace(comment)) } break } val += next if p.isEOF { return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next) } } return val, nil } func (p *parser) readContinuationLines(val string) (string, error) { for { data, err := p.readUntil('\n') if err != nil { return "", err } next := strings.TrimSpace(string(data)) if len(next) == 0 { break } val += next if val[len(val)-1] != '\\' { break } val = val[:len(val)-1] } return val, nil } // hasSurroundedQuote check if and only if the first and last characters // are quotes \" or \'. // It returns false if any other parts also contain same kind of quotes. func hasSurroundedQuote(in string, quote byte) bool { return len(in) >= 2 && in[0] == quote && in[len(in)-1] == quote && strings.IndexByte(in[1:], quote) == len(in)-2 } func (p *parser) readValue(in []byte, ignoreContinuation, ignoreInlineComment, unescapeValueDoubleQuotes, unescapeValueCommentSymbols bool) (string, error) { line := strings.TrimLeftFunc(string(in), unicode.IsSpace) if len(line) == 0 { return "", nil } var valQuote string if len(line) > 3 && string(line[0:3]) == `"""` { valQuote = `"""` } else if line[0] == '`' { valQuote = "`" } else if unescapeValueDoubleQuotes && line[0] == '"' { valQuote = `"` } if len(valQuote) > 0 { startIdx := len(valQuote) pos := strings.LastIndex(line[startIdx:], valQuote) // Check for multi-line value if pos == -1 { return p.readMultilines(line, line[startIdx:], valQuote) } if unescapeValueDoubleQuotes && valQuote == `"` { return strings.Replace(line[startIdx:pos+startIdx], `\"`, `"`, -1), nil } return line[startIdx : pos+startIdx], nil } // Won't be able to reach here if value only contains whitespace line = strings.TrimSpace(line) // Check continuation lines when desired if !ignoreContinuation && line[len(line)-1] == '\\' { return p.readContinuationLines(line[:len(line)-1]) } // Check if ignore inline comment if !ignoreInlineComment { i := strings.IndexAny(line, "#;") if i > -1 { p.comment.WriteString(line[i:]) line = strings.TrimSpace(line[:i]) } } // Trim single and double quotes if hasSurroundedQuote(line, '\'') || hasSurroundedQuote(line, '"') { line = line[1 : len(line)-1] } else if len(valQuote) == 0 && unescapeValueCommentSymbols { if strings.Contains(line, `\;`) { line = strings.Replace(line, `\;`, ";", -1) } if strings.Contains(line, `\#`) { line = strings.Replace(line, `\#`, "#", -1) } } return line, nil } // parse parses data through an io.Reader. func (f *File) parse(reader io.Reader) (err error) { p := newParser(reader) if err = p.BOM(); err != nil { return fmt.Errorf("BOM: %v", err) } // Ignore error because default section name is never empty string. name := DEFAULT_SECTION if f.options.Insensitive { name = strings.ToLower(DEFAULT_SECTION) } section, _ := f.NewSection(name) // This "last" is not strictly equivalent to "previous one" if current key is not the first nested key var isLastValueEmpty bool var lastRegularKey *Key var line []byte var inUnparseableSection bool for !p.isEOF { line, err = p.readUntil('\n') if err != nil { return err } if f.options.AllowNestedValues && isLastValueEmpty && len(line) > 0 { if line[0] == ' ' || line[0] == '\t' { lastRegularKey.addNestedValue(string(bytes.TrimSpace(line))) continue } } line = bytes.TrimLeftFunc(line, unicode.IsSpace) if len(line) == 0 { continue } // Comments if line[0] == '#' || line[0] == ';' { // Note: we do not care ending line break, // it is needed for adding second line, // so just clean it once at the end when set to value. p.comment.Write(line) continue } // Section if line[0] == '[' { // Read to the next ']' (TODO: support quoted strings) // TODO(unknwon): use LastIndexByte when stop supporting Go1.4 closeIdx := bytes.LastIndex(line, []byte("]")) if closeIdx == -1 { return fmt.Errorf("unclosed section: %s", line) } name := string(line[1:closeIdx]) section, err = f.NewSection(name) if err != nil { return err } comment, has := cleanComment(line[closeIdx+1:]) if has { p.comment.Write(comment) } section.Comment = strings.TrimSpace(p.comment.String()) // Reset aotu-counter and comments p.comment.Reset() p.count = 1 inUnparseableSection = false for i := range f.options.UnparseableSections { if f.options.UnparseableSections[i] == name || (f.options.Insensitive && strings.ToLower(f.options.UnparseableSections[i]) == strings.ToLower(name)) { inUnparseableSection = true continue } } continue } if inUnparseableSection { section.isRawSection = true section.rawBody += string(line) continue } kname, offset, err := readKeyName(line) if err != nil { // Treat as boolean key when desired, and whole line is key name. if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys { kname, err := p.readValue(line, f.options.IgnoreContinuation, f.options.IgnoreInlineComment, f.options.UnescapeValueDoubleQuotes, f.options.UnescapeValueCommentSymbols) if err != nil { return err } key, err := section.NewBooleanKey(kname) if err != nil { return err } key.Comment = strings.TrimSpace(p.comment.String()) p.comment.Reset() continue } return err } // Auto increment. isAutoIncr := false if kname == "-" { isAutoIncr = true kname = "#" + strconv.Itoa(p.count) p.count++ } value, err := p.readValue(line[offset:], f.options.IgnoreContinuation, f.options.IgnoreInlineComment, f.options.UnescapeValueDoubleQuotes, f.options.UnescapeValueCommentSymbols) if err != nil { return err } isLastValueEmpty = len(value) == 0 key, err := section.NewKey(kname, value) if err != nil { return err } key.isAutoIncrement = isAutoIncr key.Comment = strings.TrimSpace(p.comment.String()) p.comment.Reset() lastRegularKey = key } return nil } ini-1.32.0/parser_test.go000066400000000000000000000035441320421373500152440ustar00rootroot00000000000000// Copyright 2016 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini_test import ( "testing" . "github.com/smartystreets/goconvey/convey" "gopkg.in/ini.v1" ) func TestBOM(t *testing.T) { Convey("Test handling BOM", t, func() { Convey("UTF-8-BOM", func() { f, err := ini.Load("testdata/UTF-8-BOM.ini") So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io") }) Convey("UTF-16-LE-BOM", func() { f, err := ini.Load("testdata/UTF-16-LE-BOM.ini") So(err, ShouldBeNil) So(f, ShouldNotBeNil) }) Convey("UTF-16-BE-BOM", func() { }) }) } func TestBadLoad(t *testing.T) { Convey("Load with bad data", t, func() { Convey("Bad section name", func() { _, err := ini.Load([]byte("[]")) So(err, ShouldNotBeNil) _, err = ini.Load([]byte("[")) So(err, ShouldNotBeNil) }) Convey("Bad keys", func() { _, err := ini.Load([]byte(`"""name`)) So(err, ShouldNotBeNil) _, err = ini.Load([]byte(`"""name"""`)) So(err, ShouldNotBeNil) _, err = ini.Load([]byte(`""=1`)) So(err, ShouldNotBeNil) _, err = ini.Load([]byte(`=`)) So(err, ShouldNotBeNil) _, err = ini.Load([]byte(`name`)) So(err, ShouldNotBeNil) }) Convey("Bad values", func() { _, err := ini.Load([]byte(`name="""Unknwon`)) So(err, ShouldNotBeNil) }) }) } ini-1.32.0/section.go000066400000000000000000000134501320421373500143520ustar00rootroot00000000000000// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini import ( "errors" "fmt" "strings" ) // Section represents a config section. type Section struct { f *File Comment string name string keys map[string]*Key keyList []string keysHash map[string]string isRawSection bool rawBody string } func newSection(f *File, name string) *Section { return &Section{ f: f, name: name, keys: make(map[string]*Key), keyList: make([]string, 0, 10), keysHash: make(map[string]string), } } // Name returns name of Section. func (s *Section) Name() string { return s.name } // Body returns rawBody of Section if the section was marked as unparseable. // It still follows the other rules of the INI format surrounding leading/trailing whitespace. func (s *Section) Body() string { return strings.TrimSpace(s.rawBody) } // SetBody updates body content only if section is raw. func (s *Section) SetBody(body string) { if !s.isRawSection { return } s.rawBody = body } // NewKey creates a new key to given section. func (s *Section) NewKey(name, val string) (*Key, error) { if len(name) == 0 { return nil, errors.New("error creating new key: empty key name") } else if s.f.options.Insensitive { name = strings.ToLower(name) } if s.f.BlockMode { s.f.lock.Lock() defer s.f.lock.Unlock() } if inSlice(name, s.keyList) { if s.f.options.AllowShadows { if err := s.keys[name].addShadow(val); err != nil { return nil, err } } else { s.keys[name].value = val } return s.keys[name], nil } s.keyList = append(s.keyList, name) s.keys[name] = newKey(s, name, val) s.keysHash[name] = val return s.keys[name], nil } // NewBooleanKey creates a new boolean type key to given section. func (s *Section) NewBooleanKey(name string) (*Key, error) { key, err := s.NewKey(name, "true") if err != nil { return nil, err } key.isBooleanType = true return key, nil } // GetKey returns key in section by given name. func (s *Section) GetKey(name string) (*Key, error) { // FIXME: change to section level lock? if s.f.BlockMode { s.f.lock.RLock() } if s.f.options.Insensitive { name = strings.ToLower(name) } key := s.keys[name] if s.f.BlockMode { s.f.lock.RUnlock() } if key == nil { // Check if it is a child-section. sname := s.name for { if i := strings.LastIndex(sname, "."); i > -1 { sname = sname[:i] sec, err := s.f.GetSection(sname) if err != nil { continue } return sec.GetKey(name) } else { break } } return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name) } return key, nil } // HasKey returns true if section contains a key with given name. func (s *Section) HasKey(name string) bool { key, _ := s.GetKey(name) return key != nil } // Haskey is a backwards-compatible name for HasKey. // TODO: delete me in v2 func (s *Section) Haskey(name string) bool { return s.HasKey(name) } // HasValue returns true if section contains given raw value. func (s *Section) HasValue(value string) bool { if s.f.BlockMode { s.f.lock.RLock() defer s.f.lock.RUnlock() } for _, k := range s.keys { if value == k.value { return true } } return false } // Key assumes named Key exists in section and returns a zero-value when not. func (s *Section) Key(name string) *Key { key, err := s.GetKey(name) if err != nil { // It's OK here because the only possible error is empty key name, // but if it's empty, this piece of code won't be executed. key, _ = s.NewKey(name, "") return key } return key } // Keys returns list of keys of section. func (s *Section) Keys() []*Key { keys := make([]*Key, len(s.keyList)) for i := range s.keyList { keys[i] = s.Key(s.keyList[i]) } return keys } // ParentKeys returns list of keys of parent section. func (s *Section) ParentKeys() []*Key { var parentKeys []*Key sname := s.name for { if i := strings.LastIndex(sname, "."); i > -1 { sname = sname[:i] sec, err := s.f.GetSection(sname) if err != nil { continue } parentKeys = append(parentKeys, sec.Keys()...) } else { break } } return parentKeys } // KeyStrings returns list of key names of section. func (s *Section) KeyStrings() []string { list := make([]string, len(s.keyList)) copy(list, s.keyList) return list } // KeysHash returns keys hash consisting of names and values. func (s *Section) KeysHash() map[string]string { if s.f.BlockMode { s.f.lock.RLock() defer s.f.lock.RUnlock() } hash := map[string]string{} for key, value := range s.keysHash { hash[key] = value } return hash } // DeleteKey deletes a key from section. func (s *Section) DeleteKey(name string) { if s.f.BlockMode { s.f.lock.Lock() defer s.f.lock.Unlock() } for i, k := range s.keyList { if k == name { s.keyList = append(s.keyList[:i], s.keyList[i+1:]...) delete(s.keys, name) return } } } // ChildSections returns a list of child sections of current section. // For example, "[parent.child1]" and "[parent.child12]" are child sections // of section "[parent]". func (s *Section) ChildSections() []*Section { prefix := s.name + "." children := make([]*Section, 0, 3) for _, name := range s.f.sectionList { if strings.HasPrefix(name, prefix) { children = append(children, s.f.sections[name]) } } return children } ini-1.32.0/section_test.go000066400000000000000000000201401320421373500154030ustar00rootroot00000000000000// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini_test import ( "testing" . "github.com/smartystreets/goconvey/convey" "gopkg.in/ini.v1" ) func TestSection_SetBody(t *testing.T) { Convey("Set body of raw section", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000 111111111111111111100000000000111000000000`) So(err, ShouldBeNil) So(sec, ShouldNotBeNil) So(sec.Body(), ShouldEqual, `1111111111111111111000000000000000001110000 111111111111111111100000000000111000000000`) sec.SetBody("1111111111111111111000000000000000001110000") So(sec.Body(), ShouldEqual, `1111111111111111111000000000000000001110000`) Convey("Set for non-raw section", func() { sec, err := f.NewSection("author") So(err, ShouldBeNil) So(sec, ShouldNotBeNil) So(sec.Body(), ShouldBeEmpty) sec.SetBody("1111111111111111111000000000000000001110000") So(sec.Body(), ShouldBeEmpty) }) }) } func TestSection_NewKey(t *testing.T) { Convey("Create a new key", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.Name(), ShouldEqual, "NAME") So(k.Value(), ShouldEqual, "ini") Convey("With duplicated name", func() { k, err := f.Section("").NewKey("NAME", "ini.v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) // Overwrite previous existed key So(k.Value(), ShouldEqual, "ini.v1") }) Convey("With empty string", func() { _, err := f.Section("").NewKey("", "") So(err, ShouldNotBeNil) }) }) Convey("Create keys with same name and allow shadow", t, func() { f, err := ini.ShadowLoad([]byte("")) So(err, ShouldBeNil) So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("").NewKey("NAME", "ini.v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.ValueWithShadows(), ShouldResemble, []string{"ini", "ini.v1"}) }) } func TestSection_NewBooleanKey(t *testing.T) { Convey("Create a new boolean key", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewBooleanKey("start-ssh-server") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.Name(), ShouldEqual, "start-ssh-server") So(k.Value(), ShouldEqual, "true") Convey("With empty string", func() { _, err := f.Section("").NewBooleanKey("") So(err, ShouldNotBeNil) }) }) } func TestSection_GetKey(t *testing.T) { Convey("Get a key", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("").GetKey("NAME") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.Name(), ShouldEqual, "NAME") So(k.Value(), ShouldEqual, "ini") Convey("Key not exists", func() { _, err := f.Section("").GetKey("404") So(err, ShouldNotBeNil) }) Convey("Key exists in parent section", func() { k, err := f.Section("parent").NewKey("AGE", "18") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("parent.child.son").GetKey("AGE") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(k.Value(), ShouldEqual, "18") }) }) } func TestSection_HasKey(t *testing.T) { Convey("Check if a key exists", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(f.Section("").HasKey("NAME"), ShouldBeTrue) So(f.Section("").Haskey("NAME"), ShouldBeTrue) So(f.Section("").HasKey("404"), ShouldBeFalse) So(f.Section("").Haskey("404"), ShouldBeFalse) }) } func TestSection_HasValue(t *testing.T) { Convey("Check if contains a value in any key", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(f.Section("").HasValue("ini"), ShouldBeTrue) So(f.Section("").HasValue("404"), ShouldBeFalse) }) } func TestSection_Key(t *testing.T) { Convey("Get a key", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k = f.Section("").Key("NAME") So(k, ShouldNotBeNil) So(k.Name(), ShouldEqual, "NAME") So(k.Value(), ShouldEqual, "ini") Convey("Key not exists", func() { k := f.Section("").Key("404") So(k, ShouldNotBeNil) So(k.Name(), ShouldEqual, "404") }) Convey("Key exists in parent section", func() { k, err := f.Section("parent").NewKey("AGE", "18") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k = f.Section("parent.child.son").Key("AGE") So(k, ShouldNotBeNil) So(k.Value(), ShouldEqual, "18") }) }) } func TestSection_Keys(t *testing.T) { Convey("Get all keys in a section", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("").NewKey("VERSION", "v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("").NewKey("IMPORT_PATH", "gopkg.in/ini.v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) keys := f.Section("").Keys() names := []string{"NAME", "VERSION", "IMPORT_PATH"} So(len(keys), ShouldEqual, len(names)) for i, name := range names { So(keys[i].Name(), ShouldEqual, name) } }) } func TestSection_ParentKeys(t *testing.T) { Convey("Get all keys of parent sections", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("package").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("package").NewKey("VERSION", "v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("package").NewKey("IMPORT_PATH", "gopkg.in/ini.v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) keys := f.Section("package.sub.sub2").ParentKeys() names := []string{"NAME", "VERSION", "IMPORT_PATH"} So(len(keys), ShouldEqual, len(names)) for i, name := range names { So(keys[i].Name(), ShouldEqual, name) } }) } func TestSection_KeyStrings(t *testing.T) { Convey("Get all key names in a section", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("").NewKey("VERSION", "v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("").NewKey("IMPORT_PATH", "gopkg.in/ini.v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(f.Section("").KeyStrings(), ShouldResemble, []string{"NAME", "VERSION", "IMPORT_PATH"}) }) } func TestSection_KeyHash(t *testing.T) { Convey("Get clone of key hash", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("").NewKey("VERSION", "v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) k, err = f.Section("").NewKey("IMPORT_PATH", "gopkg.in/ini.v1") So(err, ShouldBeNil) So(k, ShouldNotBeNil) hash := f.Section("").KeysHash() relation := map[string]string{ "NAME": "ini", "VERSION": "v1", "IMPORT_PATH": "gopkg.in/ini.v1", } for k, v := range hash { So(v, ShouldEqual, relation[k]) } }) } func TestSection_DeleteKey(t *testing.T) { Convey("Delete a key", t, func() { f := ini.Empty() So(f, ShouldNotBeNil) k, err := f.Section("").NewKey("NAME", "ini") So(err, ShouldBeNil) So(k, ShouldNotBeNil) So(f.Section("").HasKey("NAME"), ShouldBeTrue) f.Section("").DeleteKey("NAME") So(f.Section("").HasKey("NAME"), ShouldBeFalse) }) } ini-1.32.0/struct.go000066400000000000000000000336211320421373500142340ustar00rootroot00000000000000// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini import ( "bytes" "errors" "fmt" "reflect" "strings" "time" "unicode" ) // NameMapper represents a ini tag name mapper. type NameMapper func(string) string // Built-in name getters. var ( // AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE. AllCapsUnderscore NameMapper = func(raw string) string { newstr := make([]rune, 0, len(raw)) for i, chr := range raw { if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { if i > 0 { newstr = append(newstr, '_') } } newstr = append(newstr, unicode.ToUpper(chr)) } return string(newstr) } // TitleUnderscore converts to format title_underscore. TitleUnderscore NameMapper = func(raw string) string { newstr := make([]rune, 0, len(raw)) for i, chr := range raw { if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { if i > 0 { newstr = append(newstr, '_') } chr -= ('A' - 'a') } newstr = append(newstr, chr) } return string(newstr) } ) func (s *Section) parseFieldName(raw, actual string) string { if len(actual) > 0 { return actual } if s.f.NameMapper != nil { return s.f.NameMapper(raw) } return raw } func parseDelim(actual string) string { if len(actual) > 0 { return actual } return "," } var reflectTime = reflect.TypeOf(time.Now()).Kind() // setSliceWithProperType sets proper values to slice based on its type. func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { var strs []string if allowShadow { strs = key.StringsWithShadows(delim) } else { strs = key.Strings(delim) } numVals := len(strs) if numVals == 0 { return nil } var vals interface{} var err error sliceOf := field.Type().Elem().Kind() switch sliceOf { case reflect.String: vals = strs case reflect.Int: vals, err = key.parseInts(strs, true, false) case reflect.Int64: vals, err = key.parseInt64s(strs, true, false) case reflect.Uint: vals, err = key.parseUints(strs, true, false) case reflect.Uint64: vals, err = key.parseUint64s(strs, true, false) case reflect.Float64: vals, err = key.parseFloat64s(strs, true, false) case reflectTime: vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false) default: return fmt.Errorf("unsupported type '[]%s'", sliceOf) } if err != nil && isStrict { return err } slice := reflect.MakeSlice(field.Type(), numVals, numVals) for i := 0; i < numVals; i++ { switch sliceOf { case reflect.String: slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i])) case reflect.Int: slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i])) case reflect.Int64: slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i])) case reflect.Uint: slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i])) case reflect.Uint64: slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i])) case reflect.Float64: slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i])) case reflectTime: slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i])) } } field.Set(slice) return nil } func wrapStrictError(err error, isStrict bool) error { if isStrict { return err } return nil } // setWithProperType sets proper value to field based on its type, // but it does not return error for failing parsing, // because we want to use default value that is already assigned to strcut. func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { switch t.Kind() { case reflect.String: if len(key.String()) == 0 { return nil } field.SetString(key.String()) case reflect.Bool: boolVal, err := key.Bool() if err != nil { return wrapStrictError(err, isStrict) } field.SetBool(boolVal) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: durationVal, err := key.Duration() // Skip zero value if err == nil && int64(durationVal) > 0 { field.Set(reflect.ValueOf(durationVal)) return nil } intVal, err := key.Int64() if err != nil { return wrapStrictError(err, isStrict) } field.SetInt(intVal) // byte is an alias for uint8, so supporting uint8 breaks support for byte case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: durationVal, err := key.Duration() // Skip zero value if err == nil && int(durationVal) > 0 { field.Set(reflect.ValueOf(durationVal)) return nil } uintVal, err := key.Uint64() if err != nil { return wrapStrictError(err, isStrict) } field.SetUint(uintVal) case reflect.Float32, reflect.Float64: floatVal, err := key.Float64() if err != nil { return wrapStrictError(err, isStrict) } field.SetFloat(floatVal) case reflectTime: timeVal, err := key.Time() if err != nil { return wrapStrictError(err, isStrict) } field.Set(reflect.ValueOf(timeVal)) case reflect.Slice: return setSliceWithProperType(key, field, delim, allowShadow, isStrict) default: return fmt.Errorf("unsupported type '%s'", t) } return nil } func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool) { opts := strings.SplitN(tag, ",", 3) rawName = opts[0] if len(opts) > 1 { omitEmpty = opts[1] == "omitempty" } if len(opts) > 2 { allowShadow = opts[2] == "allowshadow" } return rawName, omitEmpty, allowShadow } func (s *Section) mapTo(val reflect.Value, isStrict bool) error { if val.Kind() == reflect.Ptr { val = val.Elem() } typ := val.Type() for i := 0; i < typ.NumField(); i++ { field := val.Field(i) tpField := typ.Field(i) tag := tpField.Tag.Get("ini") if tag == "-" { continue } rawName, _, allowShadow := parseTagOptions(tag) fieldName := s.parseFieldName(tpField.Name, rawName) if len(fieldName) == 0 || !field.CanSet() { continue } isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous isStruct := tpField.Type.Kind() == reflect.Struct if isAnonymous { field.Set(reflect.New(tpField.Type.Elem())) } if isAnonymous || isStruct { if sec, err := s.f.GetSection(fieldName); err == nil { if err = sec.mapTo(field, isStrict); err != nil { return fmt.Errorf("error mapping field(%s): %v", fieldName, err) } continue } } if key, err := s.GetKey(fieldName); err == nil { delim := parseDelim(tpField.Tag.Get("delim")) if err = setWithProperType(tpField.Type, key, field, delim, allowShadow, isStrict); err != nil { return fmt.Errorf("error mapping field(%s): %v", fieldName, err) } } } return nil } // MapTo maps section to given struct. func (s *Section) MapTo(v interface{}) error { typ := reflect.TypeOf(v) val := reflect.ValueOf(v) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } else { return errors.New("cannot map to non-pointer struct") } return s.mapTo(val, false) } // MapTo maps section to given struct in strict mode, // which returns all possible error including value parsing error. func (s *Section) StrictMapTo(v interface{}) error { typ := reflect.TypeOf(v) val := reflect.ValueOf(v) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } else { return errors.New("cannot map to non-pointer struct") } return s.mapTo(val, true) } // MapTo maps file to given struct. func (f *File) MapTo(v interface{}) error { return f.Section("").MapTo(v) } // MapTo maps file to given struct in strict mode, // which returns all possible error including value parsing error. func (f *File) StrictMapTo(v interface{}) error { return f.Section("").StrictMapTo(v) } // MapTo maps data sources to given struct with name mapper. func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { cfg, err := Load(source, others...) if err != nil { return err } cfg.NameMapper = mapper return cfg.MapTo(v) } // StrictMapToWithMapper maps data sources to given struct with name mapper in strict mode, // which returns all possible error including value parsing error. func StrictMapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { cfg, err := Load(source, others...) if err != nil { return err } cfg.NameMapper = mapper return cfg.StrictMapTo(v) } // MapTo maps data sources to given struct. func MapTo(v, source interface{}, others ...interface{}) error { return MapToWithMapper(v, nil, source, others...) } // StrictMapTo maps data sources to given struct in strict mode, // which returns all possible error including value parsing error. func StrictMapTo(v, source interface{}, others ...interface{}) error { return StrictMapToWithMapper(v, nil, source, others...) } // reflectSliceWithProperType does the opposite thing as setSliceWithProperType. func reflectSliceWithProperType(key *Key, field reflect.Value, delim string) error { slice := field.Slice(0, field.Len()) if field.Len() == 0 { return nil } var buf bytes.Buffer sliceOf := field.Type().Elem().Kind() for i := 0; i < field.Len(); i++ { switch sliceOf { case reflect.String: buf.WriteString(slice.Index(i).String()) case reflect.Int, reflect.Int64: buf.WriteString(fmt.Sprint(slice.Index(i).Int())) case reflect.Uint, reflect.Uint64: buf.WriteString(fmt.Sprint(slice.Index(i).Uint())) case reflect.Float64: buf.WriteString(fmt.Sprint(slice.Index(i).Float())) case reflectTime: buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339)) default: return fmt.Errorf("unsupported type '[]%s'", sliceOf) } buf.WriteString(delim) } key.SetValue(buf.String()[:buf.Len()-1]) return nil } // reflectWithProperType does the opposite thing as setWithProperType. func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error { switch t.Kind() { case reflect.String: key.SetValue(field.String()) case reflect.Bool: key.SetValue(fmt.Sprint(field.Bool())) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: key.SetValue(fmt.Sprint(field.Int())) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: key.SetValue(fmt.Sprint(field.Uint())) case reflect.Float32, reflect.Float64: key.SetValue(fmt.Sprint(field.Float())) case reflectTime: key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339))) case reflect.Slice: return reflectSliceWithProperType(key, field, delim) default: return fmt.Errorf("unsupported type '%s'", t) } return nil } // CR: copied from encoding/json/encode.go with modifications of time.Time support. // TODO: add more test coverage. func isEmptyValue(v reflect.Value) bool { switch v.Kind() { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: return v.Len() == 0 case reflect.Bool: return !v.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Interface, reflect.Ptr: return v.IsNil() case reflectTime: t, ok := v.Interface().(time.Time) return ok && t.IsZero() } return false } func (s *Section) reflectFrom(val reflect.Value) error { if val.Kind() == reflect.Ptr { val = val.Elem() } typ := val.Type() for i := 0; i < typ.NumField(); i++ { field := val.Field(i) tpField := typ.Field(i) tag := tpField.Tag.Get("ini") if tag == "-" { continue } opts := strings.SplitN(tag, ",", 2) if len(opts) == 2 && opts[1] == "omitempty" && isEmptyValue(field) { continue } fieldName := s.parseFieldName(tpField.Name, opts[0]) if len(fieldName) == 0 || !field.CanSet() { continue } if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) || (tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") { // Note: The only error here is section doesn't exist. sec, err := s.f.GetSection(fieldName) if err != nil { // Note: fieldName can never be empty here, ignore error. sec, _ = s.f.NewSection(fieldName) } // Add comment from comment tag if len(sec.Comment) == 0 { sec.Comment = tpField.Tag.Get("comment") } if err = sec.reflectFrom(field); err != nil { return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) } continue } // Note: Same reason as secion. key, err := s.GetKey(fieldName) if err != nil { key, _ = s.NewKey(fieldName, "") } // Add comment from comment tag if len(key.Comment) == 0 { key.Comment = tpField.Tag.Get("comment") } if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) } } return nil } // ReflectFrom reflects secion from given struct. func (s *Section) ReflectFrom(v interface{}) error { typ := reflect.TypeOf(v) val := reflect.ValueOf(v) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } else { return errors.New("cannot reflect from non-pointer struct") } return s.reflectFrom(val) } // ReflectFrom reflects file from given struct. func (f *File) ReflectFrom(v interface{}) error { return f.Section("").ReflectFrom(v) } // ReflectFrom reflects data sources from given struct with name mapper. func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error { cfg.NameMapper = mapper return cfg.ReflectFrom(v) } // ReflectFrom reflects data sources from given struct. func ReflectFrom(cfg *File, v interface{}) error { return ReflectFromWithMapper(cfg, v, nil) } ini-1.32.0/struct_test.go000066400000000000000000000223211320421373500152660ustar00rootroot00000000000000// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package ini_test import ( "bytes" "fmt" "strings" "testing" "time" . "github.com/smartystreets/goconvey/convey" "gopkg.in/ini.v1" ) type testNested struct { Cities []string `delim:"|"` Visits []time.Time Years []int Numbers []int64 Ages []uint Populations []uint64 Coordinates []float64 Note string Unused int `ini:"-"` } type testEmbeded struct { GPA float64 } type testStruct struct { Name string `ini:"NAME"` Age int Male bool Money float64 Born time.Time Time time.Duration `ini:"Duration"` Others testNested *testEmbeded `ini:"grade"` Unused int `ini:"-"` Unsigned uint Omitted bool `ini:"omitthis,omitempty"` Shadows []string `ini:",,allowshadow"` ShadowInts []int `ini:"Shadows,,allowshadow"` } const _CONF_DATA_STRUCT = ` NAME = Unknwon Age = 21 Male = true Money = 1.25 Born = 1993-10-07T20:17:05Z Duration = 2h45m Unsigned = 3 omitthis = true Shadows = 1, 2 Shadows = 3, 4 [Others] Cities = HangZhou|Boston Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z Years = 1993,1994 Numbers = 10010,10086 Ages = 18,19 Populations = 12345678,98765432 Coordinates = 192.168,10.11 Note = Hello world! [grade] GPA = 2.8 [foo.bar] Here = there When = then ` type unsupport struct { Byte byte } type unsupport2 struct { Others struct { Cities byte } } type unsupport3 struct { Cities byte } type unsupport4 struct { *unsupport3 `ini:"Others"` } type defaultValue struct { Name string Age int Male bool Money float64 Born time.Time Cities []string } type fooBar struct { Here, When string } const _INVALID_DATA_CONF_STRUCT = ` Name = Age = age Male = 123 Money = money Born = nil Cities = ` func Test_MapToStruct(t *testing.T) { Convey("Map to struct", t, func() { Convey("Map file to struct", func() { ts := new(testStruct) So(ini.MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil) So(ts.Name, ShouldEqual, "Unknwon") So(ts.Age, ShouldEqual, 21) So(ts.Male, ShouldBeTrue) So(ts.Money, ShouldEqual, 1.25) So(ts.Unsigned, ShouldEqual, 3) t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z") So(err, ShouldBeNil) So(ts.Born.String(), ShouldEqual, t.String()) dur, err := time.ParseDuration("2h45m") So(err, ShouldBeNil) So(ts.Time.Seconds(), ShouldEqual, dur.Seconds()) So(strings.Join(ts.Others.Cities, ","), ShouldEqual, "HangZhou,Boston") So(ts.Others.Visits[0].String(), ShouldEqual, t.String()) So(fmt.Sprint(ts.Others.Years), ShouldEqual, "[1993 1994]") So(fmt.Sprint(ts.Others.Numbers), ShouldEqual, "[10010 10086]") So(fmt.Sprint(ts.Others.Ages), ShouldEqual, "[18 19]") So(fmt.Sprint(ts.Others.Populations), ShouldEqual, "[12345678 98765432]") So(fmt.Sprint(ts.Others.Coordinates), ShouldEqual, "[192.168 10.11]") So(ts.Others.Note, ShouldEqual, "Hello world!") So(ts.testEmbeded.GPA, ShouldEqual, 2.8) }) Convey("Map section to struct", func() { foobar := new(fooBar) f, err := ini.Load([]byte(_CONF_DATA_STRUCT)) So(err, ShouldBeNil) So(f.Section("foo.bar").MapTo(foobar), ShouldBeNil) So(foobar.Here, ShouldEqual, "there") So(foobar.When, ShouldEqual, "then") }) Convey("Map to non-pointer struct", func() { f, err := ini.Load([]byte(_CONF_DATA_STRUCT)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) So(f.MapTo(testStruct{}), ShouldNotBeNil) }) Convey("Map to unsupported type", func() { f, err := ini.Load([]byte(_CONF_DATA_STRUCT)) So(err, ShouldBeNil) So(f, ShouldNotBeNil) f.NameMapper = func(raw string) string { if raw == "Byte" { return "NAME" } return raw } So(f.MapTo(&unsupport{}), ShouldNotBeNil) So(f.MapTo(&unsupport2{}), ShouldNotBeNil) So(f.MapTo(&unsupport4{}), ShouldNotBeNil) }) Convey("Map to omitempty field", func() { ts := new(testStruct) So(ini.MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil) So(ts.Omitted, ShouldEqual, true) }) Convey("Map with shadows", func() { f, err := ini.LoadSources(ini.LoadOptions{AllowShadows: true}, []byte(_CONF_DATA_STRUCT)) So(err, ShouldBeNil) ts := new(testStruct) So(f.MapTo(ts), ShouldBeNil) So(strings.Join(ts.Shadows, " "), ShouldEqual, "1 2 3 4") So(fmt.Sprintf("%v", ts.ShadowInts), ShouldEqual, "[1 2 3 4]") }) Convey("Map from invalid data source", func() { So(ini.MapTo(&testStruct{}, "hi"), ShouldNotBeNil) }) Convey("Map to wrong types and gain default values", func() { f, err := ini.Load([]byte(_INVALID_DATA_CONF_STRUCT)) So(err, ShouldBeNil) t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z") So(err, ShouldBeNil) dv := &defaultValue{"Joe", 10, true, 1.25, t, []string{"HangZhou", "Boston"}} So(f.MapTo(dv), ShouldBeNil) So(dv.Name, ShouldEqual, "Joe") So(dv.Age, ShouldEqual, 10) So(dv.Male, ShouldBeTrue) So(dv.Money, ShouldEqual, 1.25) So(dv.Born.String(), ShouldEqual, t.String()) So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston") }) }) Convey("Map to struct in strict mode", t, func() { f, err := ini.Load([]byte(` name=bruce age=a30`)) So(err, ShouldBeNil) type Strict struct { Name string `ini:"name"` Age int `ini:"age"` } s := new(Strict) So(f.Section("").StrictMapTo(s), ShouldNotBeNil) }) Convey("Map slice in strict mode", t, func() { f, err := ini.Load([]byte(` names=alice, bruce`)) So(err, ShouldBeNil) type Strict struct { Names []string `ini:"names"` } s := new(Strict) So(f.Section("").StrictMapTo(s), ShouldBeNil) So(fmt.Sprint(s.Names), ShouldEqual, "[alice bruce]") }) } func Test_ReflectFromStruct(t *testing.T) { Convey("Reflect from struct", t, func() { type Embeded struct { Dates []time.Time `delim:"|" comment:"Time data"` Places []string Years []int Numbers []int64 Ages []uint Populations []uint64 Coordinates []float64 None []int } type Author struct { Name string `ini:"NAME"` Male bool Age int `comment:"Author's age"` Height uint GPA float64 Date time.Time NeverMind string `ini:"-"` *Embeded `ini:"infos" comment:"Embeded section"` } t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z") So(err, ShouldBeNil) a := &Author{"Unknwon", true, 21, 100, 2.8, t, "", &Embeded{ []time.Time{t, t}, []string{"HangZhou", "Boston"}, []int{1993, 1994}, []int64{10010, 10086}, []uint{18, 19}, []uint64{12345678, 98765432}, []float64{192.168, 10.11}, []int{}, }} cfg := ini.Empty() So(ini.ReflectFrom(cfg, a), ShouldBeNil) var buf bytes.Buffer _, err = cfg.WriteTo(&buf) So(err, ShouldBeNil) So(buf.String(), ShouldEqual, `NAME = Unknwon Male = true ; Author's age Age = 21 Height = 100 GPA = 2.8 Date = 1993-10-07T20:17:05Z ; Embeded section [infos] ; Time data Dates = 1993-10-07T20:17:05Z|1993-10-07T20:17:05Z Places = HangZhou,Boston Years = 1993,1994 Numbers = 10010,10086 Ages = 18,19 Populations = 12345678,98765432 Coordinates = 192.168,10.11 None = `) Convey("Reflect from non-point struct", func() { So(ini.ReflectFrom(cfg, Author{}), ShouldNotBeNil) }) Convey("Reflect from struct with omitempty", func() { cfg := ini.Empty() type SpecialStruct struct { FirstName string `ini:"first_name"` LastName string `ini:"last_name"` JustOmitMe string `ini:"omitempty"` LastLogin time.Time `ini:"last_login,omitempty"` LastLogin2 time.Time `ini:",omitempty"` NotEmpty int `ini:"omitempty"` } So(ini.ReflectFrom(cfg, &SpecialStruct{FirstName: "John", LastName: "Doe", NotEmpty: 9}), ShouldBeNil) var buf bytes.Buffer _, err = cfg.WriteTo(&buf) So(buf.String(), ShouldEqual, `first_name = John last_name = Doe omitempty = 9 `) }) }) } type testMapper struct { PackageName string } func Test_NameGetter(t *testing.T) { Convey("Test name mappers", t, func() { So(ini.MapToWithMapper(&testMapper{}, ini.TitleUnderscore, []byte("packag_name=ini")), ShouldBeNil) cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) So(err, ShouldBeNil) So(cfg, ShouldNotBeNil) cfg.NameMapper = ini.AllCapsUnderscore tg := new(testMapper) So(cfg.MapTo(tg), ShouldBeNil) So(tg.PackageName, ShouldEqual, "ini") }) } type testDurationStruct struct { Duration time.Duration `ini:"Duration"` } func Test_Duration(t *testing.T) { Convey("Duration less than 16m50s", t, func() { ds := new(testDurationStruct) So(ini.MapTo(ds, []byte("Duration=16m49s")), ShouldBeNil) dur, err := time.ParseDuration("16m49s") So(err, ShouldBeNil) So(ds.Duration.Seconds(), ShouldEqual, dur.Seconds()) }) } ini-1.32.0/testdata/000077500000000000000000000000001320421373500141655ustar00rootroot00000000000000ini-1.32.0/testdata/UTF-16-BE-BOM.ini000066400000000000000000000000701320421373500164440ustar00rootroot00000000000000[author] E-MAIL = u@gogs.ioini-1.32.0/testdata/UTF-16-LE-BOM.ini000066400000000000000000000000701320421373500164560ustar00rootroot00000000000000[author] E-MAIL = u@gogs.ioini-1.32.0/testdata/UTF-8-BOM.ini000066400000000000000000000000361320421373500161030ustar00rootroot00000000000000[author] E-MAIL = u@gogs.ioini-1.32.0/testdata/full.ini000066400000000000000000000034601320421373500156330ustar00rootroot00000000000000; Package name NAME = ini ; Package version VERSION = v1 ; Package import path IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s # Information about package author # Bio can be written in multiple lines. [author] NAME = Unknwon E-MAIL = u@gogs.io GITHUB = https://github.com/%(NAME)s BIO = """Gopher. Coding addict. Good man. """ # Succeeding comment [package] CLONE_URL = https://%(IMPORT_PATH)s [package.sub] UNUSED_KEY = should be deleted [features] -: Support read/write comments of keys and sections -: Support auto-increment of key names -: Support load multiple files to overwrite key values [types] STRING = str BOOL = true BOOL_FALSE = false FLOAT64 = 1.25 INT = 10 TIME = 2015-01-01T20:17:05Z DURATION = 2h45m UINT = 3 [array] STRINGS = en, zh, de FLOAT64S = 1.1, 2.2, 3.3 INTS = 1, 2, 3 UINTS = 1, 2, 3 TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z [note] empty_lines = next line is empty\ ; Comment before the section [comments] ; This is a comment for the section too ; Comment before key key = "value" key2 = "value2" ; This is a comment for key2 key3 = "one", "two", "three" [string escapes] key1 = value1, value2, value3 key2 = value1\, value2 key3 = val\ue1, value2 key4 = value1\\, value\\\\2 key5 = value1\,, value2 key6 = aaa bbb\ and\ space ccc [advance] value with quotes = "some value" value quote2 again = 'some value' includes comment sign = `my#password` includes comment sign2 = `my;password` true = 2+3=5 "1+1=2" = true """6+1=7""" = true """`5+5`""" = 10 `"6+6"` = 12 `7-2=4` = false ADDRESS = `404 road, NotFound, State, 50000` two_lines = how about \ continuation lines? lots_of_lines = 1 \ 2 \ 3 \ 4 \ ini-1.32.0/testdata/minimal.ini000066400000000000000000000000331320421373500163100ustar00rootroot00000000000000[author] E-MAIL = u@gogs.io