pax_global_header00006660000000000000000000000064126341734110014514gustar00rootroot0000000000000052 comment=77178f22699a4ecafce485fb8d86b7afeb7e3e28 ini-1.8.5/000077500000000000000000000000001263417341100123065ustar00rootroot00000000000000ini-1.8.5/.gitignore000066400000000000000000000001321263417341100142720ustar00rootroot00000000000000testdata/conf_out.ini ini.sublime-project ini.sublime-workspace testdata/conf_reflect.ini ini-1.8.5/LICENSE000066400000000000000000000240411263417341100133140ustar00rootroot00000000000000Apache 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 [yyyy] [name of copyright owner] 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.8.5/README.md000066400000000000000000000324051263417341100135710ustar00rootroot00000000000000ini [![Build Status](https://drone.io/github.com/go-ini/ini/status.png)](https://drone.io/github.com/go-ini/ini/latest) [![](http://gocover.io/_badge/github.com/go-ini/ini)](http://gocover.io/github.com/go-ini/ini) === ![](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` or file) 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 go get gopkg.in/ini.v1 ## Getting Started ### Loading from data sources A **Data Source** is either raw data in type `[]byte` or a file name with type `string` and you can load **as many as** data sources you want. Passing other types will simply return an error. ```go cfg, err := ini.Load([]byte("raw data"), "filename") ``` Or start with an empty object: ```go cfg := ini.Empty() ``` When you cannot decide how many data sources to load at the beginning, you still able to **Append()** them later. ```go err := cfg.Append("other file", []byte("other raw data")) ``` ### 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("") ``` 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.GetSection("").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 ``` Note that single quotes around values will be stripped: ```ini foo = "some value" // foo: some value bar = 'some value' // bar: some value ``` 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 ``` To auto-split value into slice: ```go 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(",") ``` ### 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") ``` ## 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 ``` ### 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:"|"` Places []string None []int } type Author struct { Name string `ini:"NAME"` Male bool Age int GPA float64 NeverMind string `ini:"-"` *Embeded } 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 Age = 21 GPA = 2.8 [Embeded] Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 Places = HangZhou,Boston None = ``` #### 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. #### 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.8.5/README_ZH.md000066400000000000000000000325321263417341100141730ustar00rootroot00000000000000本包提供了 Go 语言中读写 INI 文件的功能。 ## 功能特性 - 支持覆盖加载多个数据源(`[]byte` 或文件) - 支持递归读取键值 - 支持读取父子分区 - 支持读取自增键名 - 支持读取多行的键值 - 支持大量辅助方法 - 支持在读取时直接转换为 Go 语言类型 - 支持读取和 **写入** 分区和键的注释 - 轻松操作分区、键值和注释 - 在保存文件时分区和键值会保持原有的顺序 ## 下载安装 go get gopkg.in/ini.v1 ## 开始使用 ### 从数据源加载 一个 **数据源** 可以是 `[]byte` 类型的原始数据,或 `string` 类型的文件路径。您可以加载 **任意多个** 数据源。如果您传递其它类型的数据源,则会直接返回错误。 ```go cfg, err := ini.Load([]byte("raw data"), "filename") ``` 或者从一个空白的文件开始: ```go cfg := ini.Empty() ``` 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。 ```go err := cfg.Append("other file", []byte("other raw data")) ``` ### 操作分区(Section) 获取指定分区: ```go section, err := cfg.GetSection("section name") ``` 如果您想要获取默认分区,则可以用空字符串代替分区名: ```go section, err := cfg.GetSection("") ``` 当您非常确定某个分区是存在的,可以使用以下简便方法: ```go section := cfg.Section("") ``` 如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。 创建一个分区: ```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.GetSection("").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 ``` 需要注意的是,值两侧的单引号会被自动剔除: ```ini foo = "some value" // foo: some value bar = 'some value' // bar: some value ``` 这就是全部了?哈哈,当然不是。 #### 操作键值的辅助方法 获取键值时设定候选值: ```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 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 // ... err = cfg.SaveTo("my.ini") err = cfg.SaveToIndent("my.ini", "\t") ``` 另一个比较高级的做法是写入到任何实现 `io.Writer` 接口的对象中: ```go // ... cfg.WriteTo(writer) cfg.WriteToIndent(writer, "\t") ``` ### 高级用法 #### 递归读取键值 在获取所有键值的过程中,特殊语法 `%()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 ``` #### 读取自增键名 如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 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:"|"` Places []string None []int } type Author struct { Name string `ini:"NAME"` Male bool Age int GPA float64 NeverMind string `ini:"-"` *Embeded } 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 Age = 21 GPA = 2.8 [Embeded] Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 Places = HangZhou,Boston None = ``` #### 名称映射器(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` 时也可应用相同的规则。 #### 映射/反射的其它说明 任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联: ```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.8.5/ini.go000066400000000000000000000607371263417341100134310ustar00rootroot00000000000000// 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" "errors" "fmt" "io" "os" "regexp" "runtime" "strconv" "strings" "sync" "time" ) const ( DEFAULT_SECTION = "DEFAULT" // Maximum allowed depth when recursively substituing variable names. _DEPTH_VALUES = 99 _VERSION = "1.8.5" ) func Version() string { return _VERSION } var ( LineBreak = "\n" // Variable regexp pattern: %(variable)s varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`) // Write spaces around "=" to look better. PrettyFormat = 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 a interface that returns file content. type dataSource interface { ReadCloser() (io.ReadCloser, error) } type sourceFile struct { name string } func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { return os.Open(s.name) } type bytesReadCloser struct { reader io.Reader } func (rc *bytesReadCloser) Read(p []byte) (n int, err error) { return rc.reader.Read(p) } func (rc *bytesReadCloser) Close() error { return nil } type sourceData struct { data []byte } func (s *sourceData) ReadCloser() (io.ReadCloser, error) { return &bytesReadCloser{bytes.NewReader(s.data)}, nil } // ____ __. // | |/ _|____ ___.__. // | <_/ __ < | | // | | \ ___/\___ | // |____|__ \___ > ____| // \/ \/\/ // Key represents a key under a section. type Key struct { s *Section Comment string name string value string isAutoIncr bool } // 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 } // String returns string representation of value. func (k *Key) String() string { val := k.value if strings.Index(val, "%") == -1 { 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 { // 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 } // 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 devide by given delimiter. func (k *Key) Strings(delim string) []string { str := k.String() if len(str) == 0 { return []string{} } vals := strings.Split(str, delim) for i := range vals { vals[i] = strings.TrimSpace(vals[i]) } return vals } // Float64s returns list of float64 devide by given delimiter. func (k *Key) Float64s(delim string) []float64 { strs := k.Strings(delim) vals := make([]float64, len(strs)) for i := range strs { vals[i], _ = strconv.ParseFloat(strs[i], 64) } return vals } // Ints returns list of int devide by given delimiter. func (k *Key) Ints(delim string) []int { strs := k.Strings(delim) vals := make([]int, len(strs)) for i := range strs { vals[i], _ = strconv.Atoi(strs[i]) } return vals } // Int64s returns list of int64 devide by given delimiter. func (k *Key) Int64s(delim string) []int64 { strs := k.Strings(delim) vals := make([]int64, len(strs)) for i := range strs { vals[i], _ = strconv.ParseInt(strs[i], 10, 64) } return vals } // Uints returns list of uint devide by given delimiter. func (k *Key) Uints(delim string) []uint { strs := k.Strings(delim) vals := make([]uint, len(strs)) for i := range strs { u, _ := strconv.ParseUint(strs[i], 10, 64) vals[i] = uint(u) } return vals } // Uint64s returns list of uint64 devide by given delimiter. func (k *Key) Uint64s(delim string) []uint64 { strs := k.Strings(delim) vals := make([]uint64, len(strs)) for i := range strs { vals[i], _ = strconv.ParseUint(strs[i], 10, 64) } return vals } // TimesFormat parses with given format and returns list of time.Time devide by given delimiter. func (k *Key) TimesFormat(format, delim string) []time.Time { strs := k.Strings(delim) vals := make([]time.Time, len(strs)) for i := range strs { vals[i], _ = time.Parse(format, strs[i]) } return vals } // Times parses with RFC3339 format and returns list of time.Time devide by given delimiter. func (k *Key) Times(delim string) []time.Time { return k.TimesFormat(time.RFC3339, delim) } // 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 } // _________ __ .__ // / _____/ ____ _____/ |_|__| ____ ____ // \_____ \_/ __ \_/ ___\ __\ |/ _ \ / \ // / \ ___/\ \___| | | ( <_> ) | \ // /_______ /\___ >\___ >__| |__|\____/|___| / // \/ \/ \/ \/ // Section represents a config section. type Section struct { f *File Comment string name string keys map[string]*Key keyList []string keysHash map[string]string } func newSection(f *File, name string) *Section { return &Section{f, "", name, make(map[string]*Key), make([]string, 0, 10), make(map[string]string)} } // Name returns name of Section. func (s *Section) Name() string { return s.name } // 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") } if s.f.BlockMode { s.f.lock.Lock() defer s.f.lock.Unlock() } if inSlice(name, s.keyList) { s.keys[name].value = val return s.keys[name], nil } s.keyList = append(s.keyList, name) s.keys[name] = &Key{s, "", name, val, false} s.keysHash[name] = val return s.keys[name], 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() } 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. 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 } // 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 } } } // ___________.__.__ // \_ _____/|__| | ____ // | __) | | | _/ __ \ // | \ | | |_\ ___/ // \___ / |__|____/\___ > // \/ \/ // File represents a combination of a or more INI file(s) in memory. type File struct { // Should make things safe, but sometimes doesn't matter. BlockMode bool // Make sure data is safe in multiple goroutines. lock sync.RWMutex // Allow combination of multiple data sources. dataSources []dataSource // Actual data is stored here. sections map[string]*Section // To keep data in order. sectionList []string NameMapper } // newFile initializes File object with given data sources. func newFile(dataSources []dataSource) *File { return &File{ BlockMode: true, dataSources: dataSources, sections: make(map[string]*Section), sectionList: make([]string, 0, 10), } } func parseDataSource(source interface{}) (dataSource, error) { switch s := source.(type) { case string: return sourceFile{s}, nil case []byte: return &sourceData{s}, nil default: return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s) } } // Load loads and parses from INI data sources. // Arguments can be mixed of file name with string type, or raw data in []byte. func Load(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) if err = f.Reload(); err != nil { return nil, err } return f, nil } // 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") } 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 } // 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.BlockMode { f.lock.RLock() defer f.lock.RUnlock() } sec := f.sections[name] if sec == nil { return nil, fmt.Errorf("error when getting section: section '%s' not exists", 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 } // 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 { 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() } // WriteToIndent writes file content into io.Writer with given value indention. func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err 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 } if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil { return 0, err } } if i > 0 { if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil { return 0, err } } else { // Write nothing if default section is empty. if len(sec.keyList) == 0 { continue } } 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 } if _, err = buf.WriteString(key.Comment + LineBreak); err != nil { return 0, err } } if len(indent) > 0 && sname != DEFAULT_SECTION { buf.WriteString(indent) } switch { case key.isAutoIncr: kname = "-" case strings.ContainsAny(kname, "\"=:"): kname = "`" + kname + "`" case strings.Contains(kname, "`"): kname = `"""` + kname + `"""` } val := key.value // In case key value contains "\n", "`", "\"", "#" or ";". if strings.ContainsAny(val, "\n`") { val = `"""` + val + `"""` } else if strings.ContainsAny(val, "#;") { val = "`" + val + "`" } if _, err = buf.WriteString(kname + equalSign + val + LineBreak); err != nil { return 0, err } } // Put a line between sections. if _, err = buf.WriteString(LineBreak); 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. tmpPath := filename + "." + strconv.Itoa(time.Now().Nanosecond()) + ".tmp" defer os.Remove(tmpPath) fw, err := os.Create(tmpPath) if err != nil { return err } if _, err = f.WriteToIndent(fw, indent); err != nil { fw.Close() return err } fw.Close() // Remove old file and rename the new one. os.Remove(filename) return os.Rename(tmpPath, filename) } // SaveTo writes content to file system. func (f *File) SaveTo(filename string) error { return f.SaveToIndent(filename, "") } ini-1.8.5/ini_test.go000066400000000000000000000405101263417341100144530ustar00rootroot00000000000000// 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" "fmt" "strings" "testing" "time" . "github.com/smartystreets/goconvey/convey" ) func Test_Version(t *testing.T) { Convey("Get version", t, func() { So(Version(), ShouldEqual, _VERSION) }) } 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 [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" [advance] value with quotes = "some value" value quote2 again = 'some value' 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 Test_Load(t *testing.T) { Convey("Load from data sources", t, func() { Convey("Load with empty data", func() { So(Empty(), ShouldNotBeNil) }) Convey("Load with multiple data sources", func() { cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini") So(err, ShouldBeNil) So(cfg, ShouldNotBeNil) }) }) Convey("Bad load process", t, func() { Convey("Load from invalid data sources", func() { _, err := Load(_CONF_DATA) So(err, ShouldNotBeNil) f, err := Load("testdata/404.ini") So(err, ShouldNotBeNil) So(f, ShouldBeNil) _, err = Load(1) So(err, ShouldNotBeNil) _, err = Load([]byte(""), 1) So(err, ShouldNotBeNil) }) Convey("Load with bad section name", func() { _, err := Load([]byte("[]")) So(err, ShouldNotBeNil) _, err = Load([]byte("[")) So(err, ShouldNotBeNil) }) Convey("Load with bad keys", func() { _, err := Load([]byte(`"""name`)) So(err, ShouldNotBeNil) _, err = Load([]byte(`"""name"""`)) So(err, ShouldNotBeNil) _, err = Load([]byte(`""=1`)) So(err, ShouldNotBeNil) _, err = Load([]byte(`=`)) So(err, ShouldNotBeNil) _, err = Load([]byte(`name`)) So(err, ShouldNotBeNil) }) Convey("Load with bad values", func() { _, err := Load([]byte(`name="""Unknwon`)) So(err, ShouldNotBeNil) }) }) } func Test_Values(t *testing.T) { Convey("Test getting and setting values", t, func() { cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini") So(err, ShouldBeNil) So(cfg, ShouldNotBeNil) Convey("Get values in default section", func() { sec := cfg.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("Get values in non-default section", func() { sec := cfg.Section("author") So(sec, ShouldNotBeNil) So(sec.Key("NAME").String(), ShouldEqual, "Unknwon") So(sec.Key("GITHUB").String(), ShouldEqual, "https://github.com/Unknwon") sec = cfg.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 := cfg.Section("features").Keys() for i, k := range keys { So(k.Name(), ShouldEqual, fmt.Sprintf("#%d", i+1)) } }) Convey("Get overwrite value", func() { So(cfg.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io") }) Convey("Get sections", func() { sections := cfg.Sections() for i, name := range []string{DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "advance"} { So(sections[i].Name(), ShouldEqual, name) } }) Convey("Get parent section value", func() { So(cfg.Section("package.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") So(cfg.Section("package.fake.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") }) Convey("Get multiple line value", func() { So(cfg.Section("author").Key("BIO").String(), ShouldEqual, "Gopher.\nCoding addict.\nGood man.\n") }) Convey("Get values with type", func() { sec := cfg.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("INT_404").MustInt64(15), ShouldEqual, 15) So(sec.Key("UINT_404").MustUint(6), ShouldEqual, 6) So(sec.Key("UINT_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("Get value with candidates", func() { sec := cfg.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 := cfg.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 := cfg.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(",") for i, v := range []float64{1.1, 2.2, 3.3} { So(vals1[i], ShouldEqual, v) } vals2 := sec.Key("INTS").Ints(",") for i, v := range []int{1, 2, 3} { So(vals2[i], ShouldEqual, v) } vals3 := sec.Key("INTS").Int64s(",") for i, v := range []int64{1, 2, 3} { So(vals3[i], ShouldEqual, v) } vals4 := sec.Key("UINTS").Uints(",") for i, v := range []uint{1, 2, 3} { So(vals4[i], ShouldEqual, v) } vals5 := sec.Key("UINTS").Uint64s(",") for i, v := range []uint64{1, 2, 3} { So(vals5[i], ShouldEqual, v) } t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") So(err, ShouldBeNil) vals6 := sec.Key("TIMES").Times(",") for i, v := range []time.Time{t, t, t} { So(vals6[i].String(), ShouldEqual, v.String()) } }) Convey("Get key hash", func() { cfg.Section("").KeysHash() }) Convey("Set key value", func() { k := cfg.Section("author").Key("NAME") k.SetValue("无闻") So(k.String(), ShouldEqual, "无闻") }) Convey("Get key strings", func() { So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,BOOL_FALSE,FLOAT64,INT,TIME,DURATION,UINT") }) Convey("Delete a key", func() { cfg.Section("package.sub").DeleteKey("UNUSED_KEY") _, err := cfg.Section("package.sub").GetKey("UNUSED_KEY") So(err, ShouldNotBeNil) }) Convey("Has Key (backwards compatible)", func() { sec := cfg.Section("package.sub") haskey1 := sec.Haskey("UNUSED_KEY") haskey2 := sec.Haskey("CLONE_URL") haskey3 := sec.Haskey("CLONE_URL_NO") So(haskey1, ShouldBeTrue) So(haskey2, ShouldBeTrue) So(haskey3, ShouldBeFalse) }) Convey("Has Key", func() { sec := cfg.Section("package.sub") haskey1 := sec.HasKey("UNUSED_KEY") haskey2 := sec.HasKey("CLONE_URL") haskey3 := sec.HasKey("CLONE_URL_NO") So(haskey1, ShouldBeTrue) So(haskey2, ShouldBeTrue) So(haskey3, ShouldBeFalse) }) Convey("Has Value", func() { sec := cfg.Section("author") hasvalue1 := sec.HasValue("Unknwon") hasvalue2 := sec.HasValue("doc") So(hasvalue1, ShouldBeTrue) So(hasvalue2, ShouldBeFalse) }) Convey("Get section strings", func() { So(strings.Join(cfg.SectionStrings(), ","), ShouldEqual, "DEFAULT,author,package,package.sub,features,types,array,note,comments,advance") }) Convey("Delete a section", func() { cfg.DeleteSection("") So(cfg.SectionStrings()[0], ShouldNotEqual, DEFAULT_SECTION) }) Convey("Create new sections", func() { cfg.NewSections("test", "test2") _, err := cfg.GetSection("test") So(err, ShouldBeNil) _, err = cfg.GetSection("test2") So(err, ShouldBeNil) }) }) Convey("Test getting and setting bad values", t, func() { cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini") So(err, ShouldBeNil) So(cfg, ShouldNotBeNil) Convey("Create new key with empty name", func() { k, err := cfg.Section("").NewKey("", "") So(err, ShouldNotBeNil) So(k, ShouldBeNil) }) Convey("Create new section with empty name", func() { s, err := cfg.NewSection("") So(err, ShouldNotBeNil) So(s, ShouldBeNil) }) Convey("Create new sections with empty name", func() { So(cfg.NewSections(""), ShouldNotBeNil) }) Convey("Get section that not exists", func() { s, err := cfg.GetSection("404") So(err, ShouldNotBeNil) So(s, ShouldBeNil) s = cfg.Section("404") So(s, ShouldNotBeNil) }) }) Convey("Test key hash clone", t, func() { cfg, err := Load([]byte(strings.Replace("network=tcp,addr=127.0.0.1:6379,db=4,pool_size=100,idle_timeout=180", ",", "\n", -1))) So(err, ShouldBeNil) for _, v := range cfg.Section("").KeysHash() { So(len(v), ShouldBeGreaterThan, 0) } }) Convey("Key has empty value", t, func() { _conf := `key1= key2= ; comment` cfg, err := Load([]byte(_conf)) So(err, ShouldBeNil) So(cfg.Section("").Key("key1").Value(), ShouldBeEmpty) }) } func Test_File_Append(t *testing.T) { Convey("Append data sources", t, func() { cfg, err := Load([]byte("")) So(err, ShouldBeNil) So(cfg, ShouldNotBeNil) So(cfg.Append([]byte(""), []byte("")), ShouldBeNil) Convey("Append bad data sources", func() { So(cfg.Append(1), ShouldNotBeNil) So(cfg.Append([]byte(""), 1), ShouldNotBeNil) }) }) } func Test_File_WriteTo(t *testing.T) { Convey("Write to somewhere", t, func() { var buf bytes.Buffer cfg := Empty() cfg.WriteTo(&buf) }) } func Test_File_SaveTo(t *testing.T) { Convey("Save file", t, func() { cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini") So(err, ShouldBeNil) So(cfg, ShouldNotBeNil) cfg.Section("").Key("NAME").Comment = "Package name" cfg.Section("author").Comment = `Information about package author # Bio can be written in multiple lines.` cfg.Section("advanced").Key("val w/ pound").SetValue("my#password") So(cfg.SaveTo("testdata/conf_out.ini"), ShouldBeNil) cfg.Section("author").Key("NAME").Comment = "This is author name" So(cfg.SaveToIndent("testdata/conf_out.ini", "\t"), ShouldBeNil) }) } func Benchmark_Key_Value(b *testing.B) { c, _ := Load([]byte(_CONF_DATA)) for i := 0; i < b.N; i++ { c.Section("").Key("NAME").Value() } } func Benchmark_Key_String(b *testing.B) { c, _ := Load([]byte(_CONF_DATA)) for i := 0; i < b.N; i++ { c.Section("").Key("NAME").String() } } func Benchmark_Key_Value_NonBlock(b *testing.B) { c, _ := Load([]byte(_CONF_DATA)) c.BlockMode = false for i := 0; i < b.N; i++ { c.Section("").Key("NAME").Value() } } func Benchmark_Key_String_NonBlock(b *testing.B) { c, _ := Load([]byte(_CONF_DATA)) c.BlockMode = false for i := 0; i < b.N; i++ { c.Section("").Key("NAME").String() } } func Benchmark_Key_SetValue(b *testing.B) { c, _ := Load([]byte(_CONF_DATA)) for i := 0; i < b.N; i++ { c.Section("").Key("NAME").SetValue("10") } } ini-1.8.5/parser.go000066400000000000000000000153511263417341100141360ustar00rootroot00000000000000// 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 BOM-UTF8 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(3) if err != nil && err != io.EOF { return err } else if len(mask) < 3 { return nil } else if mask[0] == 239 && mask[1] == 187 && 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, fmt.Errorf("key-value delimiter not found: %s", line) } endIdx = pos + i return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil } endIdx = strings.IndexAny(line, "=:") if endIdx < 0 { return "", -1, fmt.Errorf("key-value delimiter not found: %s", 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) (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 = "`" } 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) } 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 if line[len(line)-1] == '\\' { return p.readContinuationLines(line[:len(line)-1]) } i := strings.IndexAny(line, "#;") if i > -1 { p.comment.WriteString(line[i:]) line = strings.TrimSpace(line[:i]) } // Trim single quotes if hasSurroundedQuote(line, '\'') || hasSurroundedQuote(line, '"') { line = line[1 : len(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. section, _ := f.NewSection(DEFAULT_SECTION) var line []byte for !p.isEOF { line, err = p.readUntil('\n') if err != nil { return err } 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) closeIdx := bytes.IndexByte(line, ']') if closeIdx == -1 { return fmt.Errorf("unclosed section: %s", line) } section, err = f.NewSection(string(line[1:closeIdx])) 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 continue } kname, offset, err := readKeyName(line) if err != nil { return err } // Auto increment. isAutoIncr := false if kname == "-" { isAutoIncr = true kname = "#" + strconv.Itoa(p.count) p.count++ } key, err := section.NewKey(kname, "") if err != nil { return err } key.isAutoIncr = isAutoIncr value, err := p.readValue(line[offset:]) if err != nil { return err } key.SetValue(value) key.Comment = strings.TrimSpace(p.comment.String()) p.comment.Reset() } return nil } ini-1.8.5/struct.go000066400000000000000000000213151263417341100141630ustar00rootroot00000000000000// 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" "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() // 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) 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 nil } field.SetBool(boolVal) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: durationVal, err := key.Duration() // Skip zero value if err == nil && int(durationVal) > 0 { field.Set(reflect.ValueOf(durationVal)) return nil } intVal, err := key.Int64() if err != nil || intVal == 0 { return nil } 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() if err == nil { field.Set(reflect.ValueOf(durationVal)) return nil } uintVal, err := key.Uint64() if err != nil { return nil } field.SetUint(uintVal) case reflect.Float64: floatVal, err := key.Float64() if err != nil { return nil } field.SetFloat(floatVal) case reflectTime: timeVal, err := key.Time() if err != nil { return nil } field.Set(reflect.ValueOf(timeVal)) case reflect.Slice: vals := key.Strings(delim) numVals := len(vals) if numVals == 0 { return nil } sliceOf := field.Type().Elem().Kind() var times []time.Time if sliceOf == reflectTime { times = key.Times(delim) } slice := reflect.MakeSlice(field.Type(), numVals, numVals) for i := 0; i < numVals; i++ { switch sliceOf { case reflectTime: slice.Index(i).Set(reflect.ValueOf(times[i])) default: slice.Index(i).Set(reflect.ValueOf(vals[i])) } } field.Set(slice) default: return fmt.Errorf("unsupported type '%s'", t) } return nil } func (s *Section) mapTo(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 } fieldName := s.parseFieldName(tpField.Name, tag) 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); err != nil { return fmt.Errorf("error mapping field(%s): %v", fieldName, err) } continue } } if key, err := s.GetKey(fieldName); err == nil { if err = setWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); 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) } // MapTo maps file to given struct. func (f *File) MapTo(v interface{}) error { return f.Section("").MapTo(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) } // MapTo maps data sources to given struct. func MapTo(v, source interface{}, others ...interface{}) error { return MapToWithMapper(v, nil, source, others...) } // reflectWithProperType does the opposite thing with 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, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float64, reflectTime: key.SetValue(fmt.Sprint(field)) case reflect.Slice: vals := field.Slice(0, field.Len()) if field.Len() == 0 { return nil } var buf bytes.Buffer isTime := fmt.Sprint(field.Type()) == "[]time.Time" for i := 0; i < field.Len(); i++ { if isTime { buf.WriteString(vals.Index(i).Interface().(time.Time).Format(time.RFC3339)) } else { buf.WriteString(fmt.Sprint(vals.Index(i))) } buf.WriteString(delim) } key.SetValue(buf.String()[:buf.Len()-1]) default: return fmt.Errorf("unsupported type '%s'", t) } return nil } 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 } fieldName := s.parseFieldName(tpField.Name, tag) if len(fieldName) == 0 || !field.CanSet() { continue } if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) || (tpField.Type.Kind() == reflect.Struct) { // 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) } 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, "") } 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.8.5/struct_test.go000066400000000000000000000125561263417341100152310ustar00rootroot00000000000000// 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 ( "strings" "testing" "time" . "github.com/smartystreets/goconvey/convey" ) type testNested struct { Cities []string `delim:"|"` Visits []time.Time 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 } const _CONF_DATA_STRUCT = ` NAME = Unknwon Age = 21 Male = true Money = 1.25 Born = 1993-10-07T20:17:05Z Duration = 2h45m Unsigned = 3 [Others] Cities = HangZhou|Boston Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z 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_Struct(t *testing.T) { Convey("Map to struct", t, func() { Convey("Map file to struct", func() { ts := new(testStruct) So(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(ts.Others.Note, ShouldEqual, "Hello world!") So(ts.testEmbeded.GPA, ShouldEqual, 2.8) }) Convey("Map section to struct", func() { foobar := new(fooBar) f, err := 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() { cfg, err := Load([]byte(_CONF_DATA_STRUCT)) So(err, ShouldBeNil) So(cfg, ShouldNotBeNil) So(cfg.MapTo(testStruct{}), ShouldNotBeNil) }) Convey("Map to unsupported type", func() { cfg, err := Load([]byte(_CONF_DATA_STRUCT)) So(err, ShouldBeNil) So(cfg, ShouldNotBeNil) cfg.NameMapper = func(raw string) string { if raw == "Byte" { return "NAME" } return raw } So(cfg.MapTo(&unsupport{}), ShouldNotBeNil) So(cfg.MapTo(&unsupport2{}), ShouldNotBeNil) So(cfg.MapTo(&unsupport4{}), ShouldNotBeNil) }) Convey("Map from invalid data source", func() { So(MapTo(&testStruct{}, "hi"), ShouldNotBeNil) }) Convey("Map to wrong types and gain default values", func() { cfg, err := 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(cfg.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("Reflect from struct", t, func() { type Embeded struct { Dates []time.Time `delim:"|"` Places []string None []int } type Author struct { Name string `ini:"NAME"` Male bool Age int GPA float64 NeverMind string `ini:"-"` *Embeded `ini:"infos"` } a := &Author{"Unknwon", true, 21, 2.8, "", &Embeded{ []time.Time{time.Now(), time.Now()}, []string{"HangZhou", "Boston"}, []int{}, }} cfg := Empty() So(ReflectFrom(cfg, a), ShouldBeNil) cfg.SaveTo("testdata/conf_reflect.ini") Convey("Reflect from non-point struct", func() { So(ReflectFrom(cfg, Author{}), ShouldNotBeNil) }) }) } type testMapper struct { PackageName string } func Test_NameGetter(t *testing.T) { Convey("Test name mappers", t, func() { So(MapToWithMapper(&testMapper{}, TitleUnderscore, []byte("packag_name=ini")), ShouldBeNil) cfg, err := Load([]byte("PACKAGE_NAME=ini")) So(err, ShouldBeNil) So(cfg, ShouldNotBeNil) cfg.NameMapper = AllCapsUnderscore tg := new(testMapper) So(cfg.MapTo(tg), ShouldBeNil) So(tg.PackageName, ShouldEqual, "ini") }) } ini-1.8.5/testdata/000077500000000000000000000000001263417341100141175ustar00rootroot00000000000000ini-1.8.5/testdata/conf.ini000066400000000000000000000000361263417341100155440ustar00rootroot00000000000000[author] E-MAIL = u@gogs.io