| @@ -1,4 +1,4 @@ | |||||
| INI [](https://travis-ci.org/go-ini/ini) | |||||
| INI [](https://travis-ci.org/go-ini/ini) [](https://sourcegraph.com/github.com/go-ini/ini?badge) | |||||
| === | === | ||||
|  |  | ||||
| @@ -9,7 +9,7 @@ Package ini provides INI file read and write functionality in Go. | |||||
| ## Feature | ## Feature | ||||
| - Load multiple data sources(`[]byte` or file) with overwrites. | |||||
| - Load multiple data sources(`[]byte`, file and `io.ReadCloser`) with overwrites. | |||||
| - Read with recursion values. | - Read with recursion values. | ||||
| - Read with parent-child sections. | - Read with parent-child sections. | ||||
| - Read with auto-increment key names. | - Read with auto-increment key names. | ||||
| @@ -44,10 +44,10 @@ Please add `-u` flag to update in the future. | |||||
| ### Loading from data sources | ### 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 data sources as you want**. Passing other types will simply return an error. | |||||
| A **Data Source** is either raw data in type `[]byte`, a file name with type `string` or `io.ReadCloser`. You can load **as many data sources as you want**. Passing other types will simply return an error. | |||||
| ```go | ```go | ||||
| cfg, err := ini.Load([]byte("raw data"), "filename") | |||||
| cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) | |||||
| ``` | ``` | ||||
| Or start with an empty object: | Or start with an empty object: | ||||
| @@ -83,8 +83,8 @@ sec1, err := cfg.GetSection("Section") | |||||
| sec2, err := cfg.GetSection("SecTIOn") | sec2, err := cfg.GetSection("SecTIOn") | ||||
| // key1 and key2 are the exactly same key object | // key1 and key2 are the exactly same key object | ||||
| key1, err := cfg.GetKey("Key") | |||||
| key2, err := cfg.GetKey("KeY") | |||||
| key1, err := sec1.GetKey("Key") | |||||
| key2, err := sec2.GetKey("KeY") | |||||
| ``` | ``` | ||||
| #### MySQL-like boolean key | #### MySQL-like boolean key | ||||
| @@ -106,6 +106,28 @@ cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) | |||||
| The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read. | The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read. | ||||
| To generate such keys in your program, you could use `NewBooleanKey`: | |||||
| ```go | |||||
| key, err := sec.NewBooleanKey("skip-host-cache") | |||||
| ``` | |||||
| #### Comment | |||||
| Take care that following format will be treated as comment: | |||||
| 1. Line begins with `#` or `;` | |||||
| 2. Words after `#` or `;` | |||||
| 3. Words after section name (i.e words after `[some section name]`) | |||||
| If you want to save a value with `#` or `;`, please quote them with ``` ` ``` or ``` """ ```. | |||||
| Alternatively, you can use following `LoadOptions` to completely ignore inline comments: | |||||
| ```go | |||||
| cfg, err := LoadSources(LoadOptions{IgnoreInlineComment: true}, "app.ini")) | |||||
| ``` | |||||
| ### Working with sections | ### Working with sections | ||||
| To get a section, you would need to: | To get a section, you would need to: | ||||
| @@ -123,7 +145,7 @@ section, err := cfg.GetSection("") | |||||
| When you're pretty sure the section exists, following code could make your life easier: | When you're pretty sure the section exists, following code could make your life easier: | ||||
| ```go | ```go | ||||
| section := cfg.Section("") | |||||
| section := cfg.Section("section name") | |||||
| ``` | ``` | ||||
| What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you. | What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you. | ||||
| @@ -400,6 +422,12 @@ cfg.WriteTo(writer) | |||||
| cfg.WriteToIndent(writer, "\t") | cfg.WriteToIndent(writer, "\t") | ||||
| ``` | ``` | ||||
| By default, spaces are used to align "=" sign between key and values, to disable that: | |||||
| ```go | |||||
| ini.PrettyFormat = false | |||||
| ``` | |||||
| ## Advanced Usage | ## Advanced Usage | ||||
| ### Recursive Values | ### Recursive Values | ||||
| @@ -447,6 +475,21 @@ cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 | |||||
| cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] | cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] | ||||
| ``` | ``` | ||||
| ### Unparseable Sections | |||||
| Sometimes, you have sections that do not contain key-value pairs but raw content, to handle such case, you can use `LoadOptions.UnparsableSections`: | |||||
| ```go | |||||
| cfg, err := LoadSources(LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] | |||||
| <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)) | |||||
| body := cfg.Section("COMMENTS").Body() | |||||
| /* --- start --- | |||||
| <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1> | |||||
| ------ end --- */ | |||||
| ``` | |||||
| ### Auto-increment Key Names | ### 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. | 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. | ||||
| @@ -2,7 +2,7 @@ | |||||
| ## 功能特性 | ## 功能特性 | ||||
| - 支持覆盖加载多个数据源(`[]byte` 或文件) | |||||
| - 支持覆盖加载多个数据源(`[]byte`、文件和 `io.ReadCloser`) | |||||
| - 支持递归读取键值 | - 支持递归读取键值 | ||||
| - 支持读取父子分区 | - 支持读取父子分区 | ||||
| - 支持读取自增键名 | - 支持读取自增键名 | ||||
| @@ -37,10 +37,10 @@ | |||||
| ### 从数据源加载 | ### 从数据源加载 | ||||
| 一个 **数据源** 可以是 `[]byte` 类型的原始数据,或 `string` 类型的文件路径。您可以加载 **任意多个** 数据源。如果您传递其它类型的数据源,则会直接返回错误。 | |||||
| 一个 **数据源** 可以是 `[]byte` 类型的原始数据,`string` 类型的文件路径或 `io.ReadCloser`。您可以加载 **任意多个** 数据源。如果您传递其它类型的数据源,则会直接返回错误。 | |||||
| ```go | ```go | ||||
| cfg, err := ini.Load([]byte("raw data"), "filename") | |||||
| cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) | |||||
| ``` | ``` | ||||
| 或者从一个空白的文件开始: | 或者从一个空白的文件开始: | ||||
| @@ -76,8 +76,8 @@ sec1, err := cfg.GetSection("Section") | |||||
| sec2, err := cfg.GetSection("SecTIOn") | sec2, err := cfg.GetSection("SecTIOn") | ||||
| // key1 和 key2 指向同一个键对象 | // key1 和 key2 指向同一个键对象 | ||||
| key1, err := cfg.GetKey("Key") | |||||
| key2, err := cfg.GetKey("KeY") | |||||
| key1, err := sec1.GetKey("Key") | |||||
| key2, err := sec2.GetKey("KeY") | |||||
| ``` | ``` | ||||
| #### 类似 MySQL 配置中的布尔值键 | #### 类似 MySQL 配置中的布尔值键 | ||||
| @@ -99,6 +99,28 @@ cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) | |||||
| 这些键的值永远为 `true`,且在保存到文件时也只会输出键名。 | 这些键的值永远为 `true`,且在保存到文件时也只会输出键名。 | ||||
| 如果您想要通过程序来生成此类键,则可以使用 `NewBooleanKey`: | |||||
| ```go | |||||
| key, err := sec.NewBooleanKey("skip-host-cache") | |||||
| ``` | |||||
| #### 关于注释 | |||||
| 下述几种情况的内容将被视为注释: | |||||
| 1. 所有以 `#` 或 `;` 开头的行 | |||||
| 2. 所有在 `#` 或 `;` 之后的内容 | |||||
| 3. 分区标签后的文字 (即 `[分区名]` 之后的内容) | |||||
| 如果你希望使用包含 `#` 或 `;` 的值,请使用 ``` ` ``` 或 ``` """ ``` 进行包覆。 | |||||
| 除此之外,您还可以通过 `LoadOptions` 完全忽略行内注释: | |||||
| ```go | |||||
| cfg, err := LoadSources(LoadOptions{IgnoreInlineComment: true}, "app.ini")) | |||||
| ``` | |||||
| ### 操作分区(Section) | ### 操作分区(Section) | ||||
| 获取指定分区: | 获取指定分区: | ||||
| @@ -116,7 +138,7 @@ section, err := cfg.GetSection("") | |||||
| 当您非常确定某个分区是存在的,可以使用以下简便方法: | 当您非常确定某个分区是存在的,可以使用以下简便方法: | ||||
| ```go | ```go | ||||
| section := cfg.Section("") | |||||
| section := cfg.Section("section name") | |||||
| ``` | ``` | ||||
| 如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。 | 如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。 | ||||
| @@ -393,9 +415,15 @@ cfg.WriteTo(writer) | |||||
| cfg.WriteToIndent(writer, "\t") | cfg.WriteToIndent(writer, "\t") | ||||
| ``` | ``` | ||||
| ### 高级用法 | |||||
| 默认情况下,空格将被用于对齐键值之间的等号以美化输出结果,以下代码可以禁用该功能: | |||||
| #### 递归读取键值 | |||||
| ```go | |||||
| ini.PrettyFormat = false | |||||
| ``` | |||||
| ## 高级用法 | |||||
| ### 递归读取键值 | |||||
| 在获取所有键值的过程中,特殊语法 `%(<name>)s` 会被应用,其中 `<name>` 可以是相同分区或者默认分区下的键名。字符串 `%(<name>)s` 会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代。您可以最多使用 99 层的递归嵌套。 | 在获取所有键值的过程中,特殊语法 `%(<name>)s` 会被应用,其中 `<name>` 可以是相同分区或者默认分区下的键名。字符串 `%(<name>)s` 会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代。您可以最多使用 99 层的递归嵌套。 | ||||
| @@ -415,7 +443,7 @@ cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon | |||||
| cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini | cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini | ||||
| ``` | ``` | ||||
| #### 读取父子分区 | |||||
| ### 读取父子分区 | |||||
| 您可以在分区名称中使用 `.` 来表示两个或多个分区之间的父子关系。如果某个键在子分区中不存在,则会去它的父分区中再次寻找,直到没有父分区为止。 | 您可以在分区名称中使用 `.` 来表示两个或多个分区之间的父子关系。如果某个键在子分区中不存在,则会去它的父分区中再次寻找,直到没有父分区为止。 | ||||
| @@ -440,7 +468,22 @@ cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 | |||||
| cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] | cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] | ||||
| ``` | ``` | ||||
| #### 读取自增键名 | |||||
| ### 无法解析的分区 | |||||
| 如果遇到一些比较特殊的分区,它们不包含常见的键值对,而是没有固定格式的纯文本,则可以使用 `LoadOptions.UnparsableSections` 进行处理: | |||||
| ```go | |||||
| cfg, err := LoadSources(LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] | |||||
| <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)) | |||||
| body := cfg.Section("COMMENTS").Body() | |||||
| /* --- start --- | |||||
| <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1> | |||||
| ------ end --- */ | |||||
| ``` | |||||
| ### 读取自增键名 | |||||
| 如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。 | 如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。 | ||||
| @@ -20,13 +20,12 @@ import ( | |||||
| "errors" | "errors" | ||||
| "fmt" | "fmt" | ||||
| "io" | "io" | ||||
| "io/ioutil" | |||||
| "os" | "os" | ||||
| "regexp" | "regexp" | ||||
| "runtime" | "runtime" | ||||
| "strconv" | |||||
| "strings" | "strings" | ||||
| "sync" | "sync" | ||||
| "time" | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| @@ -36,7 +35,7 @@ const ( | |||||
| // Maximum allowed depth when recursively substituing variable names. | // Maximum allowed depth when recursively substituing variable names. | ||||
| _DEPTH_VALUES = 99 | _DEPTH_VALUES = 99 | ||||
| _VERSION = "1.21.1" | |||||
| _VERSION = "1.28.1" | |||||
| ) | ) | ||||
| // Version returns current package version literal. | // Version returns current package version literal. | ||||
| @@ -59,6 +58,9 @@ var ( | |||||
| // Explicitly write DEFAULT section header | // Explicitly write DEFAULT section header | ||||
| DefaultHeader = false | DefaultHeader = false | ||||
| // Indicate whether to put a line between sections | |||||
| PrettySection = true | |||||
| ) | ) | ||||
| func init() { | func init() { | ||||
| @@ -108,7 +110,16 @@ type sourceData struct { | |||||
| } | } | ||||
| func (s *sourceData) ReadCloser() (io.ReadCloser, error) { | func (s *sourceData) ReadCloser() (io.ReadCloser, error) { | ||||
| return &bytesReadCloser{bytes.NewReader(s.data)}, nil | |||||
| return ioutil.NopCloser(bytes.NewReader(s.data)), nil | |||||
| } | |||||
| // sourceReadCloser represents an input stream with Close method. | |||||
| type sourceReadCloser struct { | |||||
| reader io.ReadCloser | |||||
| } | |||||
| func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) { | |||||
| return s.reader, nil | |||||
| } | } | ||||
| // File represents a combination of a or more INI file(s) in memory. | // File represents a combination of a or more INI file(s) in memory. | ||||
| @@ -149,6 +160,8 @@ func parseDataSource(source interface{}) (dataSource, error) { | |||||
| return sourceFile{s}, nil | return sourceFile{s}, nil | ||||
| case []byte: | case []byte: | ||||
| return &sourceData{s}, nil | return &sourceData{s}, nil | ||||
| case io.ReadCloser: | |||||
| return &sourceReadCloser{s}, nil | |||||
| default: | default: | ||||
| return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s) | return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s) | ||||
| } | } | ||||
| @@ -161,9 +174,16 @@ type LoadOptions struct { | |||||
| Insensitive bool | Insensitive bool | ||||
| // IgnoreContinuation indicates whether to ignore continuation lines while parsing. | // IgnoreContinuation indicates whether to ignore continuation lines while parsing. | ||||
| IgnoreContinuation bool | IgnoreContinuation bool | ||||
| // IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value. | |||||
| IgnoreInlineComment bool | |||||
| // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing. | // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing. | ||||
| // This type of keys are mostly used in my.cnf. | // This type of keys are mostly used in my.cnf. | ||||
| AllowBooleanKeys bool | AllowBooleanKeys bool | ||||
| // AllowShadows indicates whether to keep track of keys with same name under same section. | |||||
| AllowShadows bool | |||||
| // Some INI formats allow group blocks that store a block of raw content that doesn't otherwise | |||||
| // conform to key/value pairs. Specify the names of those blocks here. | |||||
| UnparseableSections []string | |||||
| } | } | ||||
| func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) { | func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) { | ||||
| @@ -204,6 +224,12 @@ func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) { | |||||
| return LoadSources(LoadOptions{Insensitive: true}, source, others...) | return LoadSources(LoadOptions{Insensitive: true}, source, others...) | ||||
| } | } | ||||
| // InsensitiveLoad has exactly same functionality as Load function | |||||
| // except it allows have shadow keys. | |||||
| func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { | |||||
| return LoadSources(LoadOptions{AllowShadows: true}, source, others...) | |||||
| } | |||||
| // Empty returns an empty file object. | // Empty returns an empty file object. | ||||
| func Empty() *File { | func Empty() *File { | ||||
| // Ignore error here, we sure our data is good. | // Ignore error here, we sure our data is good. | ||||
| @@ -233,6 +259,18 @@ func (f *File) NewSection(name string) (*Section, error) { | |||||
| return f.sections[name], nil | return f.sections[name], nil | ||||
| } | } | ||||
| // NewRawSection creates a new section with an unparseable body. | |||||
| func (f *File) NewRawSection(name, body string) (*Section, error) { | |||||
| section, err := f.NewSection(name) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| section.isRawSection = true | |||||
| section.rawBody = body | |||||
| return section, nil | |||||
| } | |||||
| // NewSections creates a list of sections. | // NewSections creates a list of sections. | ||||
| func (f *File) NewSections(names ...string) (err error) { | func (f *File) NewSections(names ...string) (err error) { | ||||
| for _, name := range names { | for _, name := range names { | ||||
| @@ -284,6 +322,11 @@ func (f *File) Sections() []*Section { | |||||
| return sections | return sections | ||||
| } | } | ||||
| // ChildSections returns a list of child sections of given section name. | |||||
| func (f *File) ChildSections(name string) []*Section { | |||||
| return f.Section(name).ChildSections() | |||||
| } | |||||
| // SectionStrings returns list of section names. | // SectionStrings returns list of section names. | ||||
| func (f *File) SectionStrings() []string { | func (f *File) SectionStrings() []string { | ||||
| list := make([]string, len(f.sectionList)) | list := make([]string, len(f.sectionList)) | ||||
| @@ -353,10 +396,7 @@ func (f *File) Append(source interface{}, others ...interface{}) error { | |||||
| return f.Reload() | return f.Reload() | ||||
| } | } | ||||
| // WriteToIndent writes content into io.Writer with given indention. | |||||
| // If PrettyFormat has been set to be true, | |||||
| // it will align "=" sign with spaces under each section. | |||||
| func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { | |||||
| func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) { | |||||
| equalSign := "=" | equalSign := "=" | ||||
| if PrettyFormat { | if PrettyFormat { | ||||
| equalSign = " = " | equalSign = " = " | ||||
| @@ -370,14 +410,14 @@ func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { | |||||
| if sec.Comment[0] != '#' && sec.Comment[0] != ';' { | if sec.Comment[0] != '#' && sec.Comment[0] != ';' { | ||||
| sec.Comment = "; " + sec.Comment | sec.Comment = "; " + sec.Comment | ||||
| } | } | ||||
| if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil { | |||||
| return 0, err | |||||
| if _, err := buf.WriteString(sec.Comment + LineBreak); err != nil { | |||||
| return nil, err | |||||
| } | } | ||||
| } | } | ||||
| if i > 0 || DefaultHeader { | if i > 0 || DefaultHeader { | ||||
| if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil { | |||||
| return 0, err | |||||
| if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil { | |||||
| return nil, err | |||||
| } | } | ||||
| } else { | } else { | ||||
| // Write nothing if default section is empty | // Write nothing if default section is empty | ||||
| @@ -386,6 +426,13 @@ func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { | |||||
| } | } | ||||
| } | } | ||||
| if sec.isRawSection { | |||||
| if _, err := buf.WriteString(sec.rawBody); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| continue | |||||
| } | |||||
| // Count and generate alignment length and buffer spaces using the | // Count and generate alignment length and buffer spaces using the | ||||
| // longest key. Keys may be modifed if they contain certain characters so | // longest key. Keys may be modifed if they contain certain characters so | ||||
| // we need to take that into account in our calculation. | // we need to take that into account in our calculation. | ||||
| @@ -407,6 +454,7 @@ func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { | |||||
| } | } | ||||
| alignSpaces := bytes.Repeat([]byte(" "), alignLength) | alignSpaces := bytes.Repeat([]byte(" "), alignLength) | ||||
| KEY_LIST: | |||||
| for _, kname := range sec.keyList { | for _, kname := range sec.keyList { | ||||
| key := sec.Key(kname) | key := sec.Key(kname) | ||||
| if len(key.Comment) > 0 { | if len(key.Comment) > 0 { | ||||
| @@ -416,8 +464,8 @@ func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { | |||||
| if key.Comment[0] != '#' && key.Comment[0] != ';' { | if key.Comment[0] != '#' && key.Comment[0] != ';' { | ||||
| key.Comment = "; " + key.Comment | key.Comment = "; " + key.Comment | ||||
| } | } | ||||
| if _, err = buf.WriteString(key.Comment + LineBreak); err != nil { | |||||
| return 0, err | |||||
| if _, err := buf.WriteString(key.Comment + LineBreak); err != nil { | |||||
| return nil, err | |||||
| } | } | ||||
| } | } | ||||
| @@ -433,37 +481,55 @@ func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { | |||||
| case strings.Contains(kname, "`"): | case strings.Contains(kname, "`"): | ||||
| kname = `"""` + kname + `"""` | kname = `"""` + kname + `"""` | ||||
| } | } | ||||
| if _, err = buf.WriteString(kname); err != nil { | |||||
| return 0, err | |||||
| } | |||||
| if key.isBooleanType { | |||||
| continue | |||||
| } | |||||
| for _, val := range key.ValueWithShadows() { | |||||
| if _, err := buf.WriteString(kname); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| // Write out alignment spaces before "=" sign | |||||
| if PrettyFormat { | |||||
| buf.Write(alignSpaces[:alignLength-len(kname)]) | |||||
| } | |||||
| if key.isBooleanType { | |||||
| if kname != sec.keyList[len(sec.keyList)-1] { | |||||
| buf.WriteString(LineBreak) | |||||
| } | |||||
| continue KEY_LIST | |||||
| } | |||||
| 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(equalSign + val + LineBreak); err != nil { | |||||
| return 0, err | |||||
| // Write out alignment spaces before "=" sign | |||||
| if PrettyFormat { | |||||
| buf.Write(alignSpaces[:alignLength-len(kname)]) | |||||
| } | |||||
| // In case key value contains "\n", "`", "\"", "#" or ";" | |||||
| if strings.ContainsAny(val, "\n`") { | |||||
| val = `"""` + val + `"""` | |||||
| } else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") { | |||||
| val = "`" + val + "`" | |||||
| } | |||||
| if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| // Put a line between sections | |||||
| if _, err = buf.WriteString(LineBreak); err != nil { | |||||
| return 0, err | |||||
| if PrettySection { | |||||
| // Put a line between sections | |||||
| if _, err := buf.WriteString(LineBreak); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| return buf, nil | |||||
| } | |||||
| // WriteToIndent writes content into io.Writer with given indention. | |||||
| // If PrettyFormat has been set to be true, | |||||
| // it will align "=" sign with spaces under each section. | |||||
| func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) { | |||||
| buf, err := f.writeToBuffer(indent) | |||||
| if err != nil { | |||||
| return 0, err | |||||
| } | |||||
| return buf.WriteTo(w) | return buf.WriteTo(w) | ||||
| } | } | ||||
| @@ -476,23 +542,12 @@ func (f *File) WriteTo(w io.Writer) (int64, error) { | |||||
| func (f *File) SaveToIndent(filename, indent string) error { | func (f *File) SaveToIndent(filename, indent string) error { | ||||
| // Note: Because we are truncating with os.Create, | // Note: Because we are truncating with os.Create, | ||||
| // so it's safer to save to a temporary file location and rename afte done. | // 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) | |||||
| buf, err := f.writeToBuffer(indent); | |||||
| if err != nil { | if err != nil { | ||||
| return err | 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) | |||||
| return ioutil.WriteFile(filename, buf.Bytes(), 0666) | |||||
| } | } | ||||
| // SaveTo writes content to file system. | // SaveTo writes content to file system. | ||||
| @@ -15,6 +15,7 @@ | |||||
| package ini | package ini | ||||
| import ( | import ( | ||||
| "errors" | |||||
| "fmt" | "fmt" | ||||
| "strconv" | "strconv" | ||||
| "strings" | "strings" | ||||
| @@ -29,9 +30,42 @@ type Key struct { | |||||
| isAutoIncrement bool | isAutoIncrement bool | ||||
| isBooleanType bool | isBooleanType bool | ||||
| isShadow bool | |||||
| shadows []*Key | |||||
| Comment string | Comment string | ||||
| } | } | ||||
| // newKey simply return a key object with given values. | |||||
| func newKey(s *Section, name, val string) *Key { | |||||
| return &Key{ | |||||
| s: s, | |||||
| name: name, | |||||
| value: val, | |||||
| } | |||||
| } | |||||
| func (k *Key) addShadow(val string) error { | |||||
| if k.isShadow { | |||||
| return errors.New("cannot add shadow to another shadow key") | |||||
| } else if k.isAutoIncrement || k.isBooleanType { | |||||
| return errors.New("cannot add shadow to auto-increment or boolean key") | |||||
| } | |||||
| shadow := newKey(k.s, k.name, val) | |||||
| shadow.isShadow = true | |||||
| k.shadows = append(k.shadows, shadow) | |||||
| return nil | |||||
| } | |||||
| // AddShadow adds a new shadow key to itself. | |||||
| func (k *Key) AddShadow(val string) error { | |||||
| if !k.s.f.options.AllowShadows { | |||||
| return errors.New("shadow key is not allowed") | |||||
| } | |||||
| return k.addShadow(val) | |||||
| } | |||||
| // ValueMapper represents a mapping function for values, e.g. os.ExpandEnv | // ValueMapper represents a mapping function for values, e.g. os.ExpandEnv | ||||
| type ValueMapper func(string) string | type ValueMapper func(string) string | ||||
| @@ -45,16 +79,29 @@ func (k *Key) Value() string { | |||||
| return k.value | return k.value | ||||
| } | } | ||||
| // String returns string representation of value. | |||||
| func (k *Key) String() string { | |||||
| val := k.value | |||||
| // ValueWithShadows returns raw values of key and its shadows if any. | |||||
| func (k *Key) ValueWithShadows() []string { | |||||
| if len(k.shadows) == 0 { | |||||
| return []string{k.value} | |||||
| } | |||||
| vals := make([]string, len(k.shadows)+1) | |||||
| vals[0] = k.value | |||||
| for i := range k.shadows { | |||||
| vals[i+1] = k.shadows[i].value | |||||
| } | |||||
| return vals | |||||
| } | |||||
| // transformValue takes a raw value and transforms to its final string. | |||||
| func (k *Key) transformValue(val string) string { | |||||
| if k.s.f.ValueMapper != nil { | if k.s.f.ValueMapper != nil { | ||||
| val = k.s.f.ValueMapper(val) | val = k.s.f.ValueMapper(val) | ||||
| } | } | ||||
| if strings.Index(val, "%") == -1 { | |||||
| // Fail-fast if no indicate char found for recursive value | |||||
| if !strings.Contains(val, "%") { | |||||
| return val | return val | ||||
| } | } | ||||
| for i := 0; i < _DEPTH_VALUES; i++ { | for i := 0; i < _DEPTH_VALUES; i++ { | ||||
| vr := varPattern.FindString(val) | vr := varPattern.FindString(val) | ||||
| if len(vr) == 0 { | if len(vr) == 0 { | ||||
| @@ -78,6 +125,11 @@ func (k *Key) String() string { | |||||
| return val | return val | ||||
| } | } | ||||
| // String returns string representation of value. | |||||
| func (k *Key) String() string { | |||||
| return k.transformValue(k.value) | |||||
| } | |||||
| // Validate accepts a validate function which can | // Validate accepts a validate function which can | ||||
| // return modifed result as key value. | // return modifed result as key value. | ||||
| func (k *Key) Validate(fn func(string) string) string { | func (k *Key) Validate(fn func(string) string) string { | ||||
| @@ -394,45 +446,65 @@ func (k *Key) Strings(delim string) []string { | |||||
| vals := strings.Split(str, delim) | vals := strings.Split(str, delim) | ||||
| for i := range vals { | for i := range vals { | ||||
| // vals[i] = k.transformValue(strings.TrimSpace(vals[i])) | |||||
| vals[i] = strings.TrimSpace(vals[i]) | vals[i] = strings.TrimSpace(vals[i]) | ||||
| } | } | ||||
| return vals | return vals | ||||
| } | } | ||||
| // StringsWithShadows returns list of string divided by given delimiter. | |||||
| // Shadows will also be appended if any. | |||||
| func (k *Key) StringsWithShadows(delim string) []string { | |||||
| vals := k.ValueWithShadows() | |||||
| results := make([]string, 0, len(vals)*2) | |||||
| for i := range vals { | |||||
| if len(vals) == 0 { | |||||
| continue | |||||
| } | |||||
| results = append(results, strings.Split(vals[i], delim)...) | |||||
| } | |||||
| for i := range results { | |||||
| results[i] = k.transformValue(strings.TrimSpace(results[i])) | |||||
| } | |||||
| return results | |||||
| } | |||||
| // Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value. | // Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value. | ||||
| func (k *Key) Float64s(delim string) []float64 { | func (k *Key) Float64s(delim string) []float64 { | ||||
| vals, _ := k.getFloat64s(delim, true, false) | |||||
| vals, _ := k.parseFloat64s(k.Strings(delim), true, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value. | // Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value. | ||||
| func (k *Key) Ints(delim string) []int { | func (k *Key) Ints(delim string) []int { | ||||
| vals, _ := k.getInts(delim, true, false) | |||||
| vals, _ := k.parseInts(k.Strings(delim), true, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value. | // Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value. | ||||
| func (k *Key) Int64s(delim string) []int64 { | func (k *Key) Int64s(delim string) []int64 { | ||||
| vals, _ := k.getInt64s(delim, true, false) | |||||
| vals, _ := k.parseInt64s(k.Strings(delim), true, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value. | // Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value. | ||||
| func (k *Key) Uints(delim string) []uint { | func (k *Key) Uints(delim string) []uint { | ||||
| vals, _ := k.getUints(delim, true, false) | |||||
| vals, _ := k.parseUints(k.Strings(delim), true, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value. | // Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value. | ||||
| func (k *Key) Uint64s(delim string) []uint64 { | func (k *Key) Uint64s(delim string) []uint64 { | ||||
| vals, _ := k.getUint64s(delim, true, false) | |||||
| vals, _ := k.parseUint64s(k.Strings(delim), true, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // TimesFormat parses with given format and returns list of time.Time divided by given delimiter. | // TimesFormat parses with given format and returns list of time.Time divided by given delimiter. | ||||
| // Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). | // Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). | ||||
| func (k *Key) TimesFormat(format, delim string) []time.Time { | func (k *Key) TimesFormat(format, delim string) []time.Time { | ||||
| vals, _ := k.getTimesFormat(format, delim, true, false) | |||||
| vals, _ := k.parseTimesFormat(format, k.Strings(delim), true, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| @@ -445,41 +517,41 @@ func (k *Key) Times(delim string) []time.Time { | |||||
| // ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then | // ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then | ||||
| // it will not be included to result list. | // it will not be included to result list. | ||||
| func (k *Key) ValidFloat64s(delim string) []float64 { | func (k *Key) ValidFloat64s(delim string) []float64 { | ||||
| vals, _ := k.getFloat64s(delim, false, false) | |||||
| vals, _ := k.parseFloat64s(k.Strings(delim), false, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will | // ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will | ||||
| // not be included to result list. | // not be included to result list. | ||||
| func (k *Key) ValidInts(delim string) []int { | func (k *Key) ValidInts(delim string) []int { | ||||
| vals, _ := k.getInts(delim, false, false) | |||||
| vals, _ := k.parseInts(k.Strings(delim), false, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer, | // ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer, | ||||
| // then it will not be included to result list. | // then it will not be included to result list. | ||||
| func (k *Key) ValidInt64s(delim string) []int64 { | func (k *Key) ValidInt64s(delim string) []int64 { | ||||
| vals, _ := k.getInt64s(delim, false, false) | |||||
| vals, _ := k.parseInt64s(k.Strings(delim), false, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer, | // ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer, | ||||
| // then it will not be included to result list. | // then it will not be included to result list. | ||||
| func (k *Key) ValidUints(delim string) []uint { | func (k *Key) ValidUints(delim string) []uint { | ||||
| vals, _ := k.getUints(delim, false, false) | |||||
| vals, _ := k.parseUints(k.Strings(delim), false, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned | // ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned | ||||
| // integer, then it will not be included to result list. | // integer, then it will not be included to result list. | ||||
| func (k *Key) ValidUint64s(delim string) []uint64 { | func (k *Key) ValidUint64s(delim string) []uint64 { | ||||
| vals, _ := k.getUint64s(delim, false, false) | |||||
| vals, _ := k.parseUint64s(k.Strings(delim), false, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| // ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter. | // ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter. | ||||
| func (k *Key) ValidTimesFormat(format, delim string) []time.Time { | func (k *Key) ValidTimesFormat(format, delim string) []time.Time { | ||||
| vals, _ := k.getTimesFormat(format, delim, false, false) | |||||
| vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false) | |||||
| return vals | return vals | ||||
| } | } | ||||
| @@ -490,33 +562,33 @@ func (k *Key) ValidTimes(delim string) []time.Time { | |||||
| // StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input. | // StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input. | ||||
| func (k *Key) StrictFloat64s(delim string) ([]float64, error) { | func (k *Key) StrictFloat64s(delim string) ([]float64, error) { | ||||
| return k.getFloat64s(delim, false, true) | |||||
| return k.parseFloat64s(k.Strings(delim), false, true) | |||||
| } | } | ||||
| // StrictInts returns list of int divided by given delimiter or error on first invalid input. | // StrictInts returns list of int divided by given delimiter or error on first invalid input. | ||||
| func (k *Key) StrictInts(delim string) ([]int, error) { | func (k *Key) StrictInts(delim string) ([]int, error) { | ||||
| return k.getInts(delim, false, true) | |||||
| return k.parseInts(k.Strings(delim), false, true) | |||||
| } | } | ||||
| // StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input. | // StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input. | ||||
| func (k *Key) StrictInt64s(delim string) ([]int64, error) { | func (k *Key) StrictInt64s(delim string) ([]int64, error) { | ||||
| return k.getInt64s(delim, false, true) | |||||
| return k.parseInt64s(k.Strings(delim), false, true) | |||||
| } | } | ||||
| // StrictUints returns list of uint divided by given delimiter or error on first invalid input. | // StrictUints returns list of uint divided by given delimiter or error on first invalid input. | ||||
| func (k *Key) StrictUints(delim string) ([]uint, error) { | func (k *Key) StrictUints(delim string) ([]uint, error) { | ||||
| return k.getUints(delim, false, true) | |||||
| return k.parseUints(k.Strings(delim), false, true) | |||||
| } | } | ||||
| // StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input. | // StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input. | ||||
| func (k *Key) StrictUint64s(delim string) ([]uint64, error) { | func (k *Key) StrictUint64s(delim string) ([]uint64, error) { | ||||
| return k.getUint64s(delim, false, true) | |||||
| return k.parseUint64s(k.Strings(delim), false, true) | |||||
| } | } | ||||
| // StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter | // StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter | ||||
| // or error on first invalid input. | // or error on first invalid input. | ||||
| func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) { | func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) { | ||||
| return k.getTimesFormat(format, delim, false, true) | |||||
| return k.parseTimesFormat(format, k.Strings(delim), false, true) | |||||
| } | } | ||||
| // StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter | // StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter | ||||
| @@ -525,9 +597,8 @@ func (k *Key) StrictTimes(delim string) ([]time.Time, error) { | |||||
| return k.StrictTimesFormat(time.RFC3339, delim) | return k.StrictTimesFormat(time.RFC3339, delim) | ||||
| } | } | ||||
| // getFloat64s returns list of float64 divided by given delimiter. | |||||
| func (k *Key) getFloat64s(delim string, addInvalid, returnOnInvalid bool) ([]float64, error) { | |||||
| strs := k.Strings(delim) | |||||
| // parseFloat64s transforms strings to float64s. | |||||
| func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) { | |||||
| vals := make([]float64, 0, len(strs)) | vals := make([]float64, 0, len(strs)) | ||||
| for _, str := range strs { | for _, str := range strs { | ||||
| val, err := strconv.ParseFloat(str, 64) | val, err := strconv.ParseFloat(str, 64) | ||||
| @@ -541,9 +612,8 @@ func (k *Key) getFloat64s(delim string, addInvalid, returnOnInvalid bool) ([]flo | |||||
| return vals, nil | return vals, nil | ||||
| } | } | ||||
| // getInts returns list of int divided by given delimiter. | |||||
| func (k *Key) getInts(delim string, addInvalid, returnOnInvalid bool) ([]int, error) { | |||||
| strs := k.Strings(delim) | |||||
| // parseInts transforms strings to ints. | |||||
| func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) { | |||||
| vals := make([]int, 0, len(strs)) | vals := make([]int, 0, len(strs)) | ||||
| for _, str := range strs { | for _, str := range strs { | ||||
| val, err := strconv.Atoi(str) | val, err := strconv.Atoi(str) | ||||
| @@ -557,9 +627,8 @@ func (k *Key) getInts(delim string, addInvalid, returnOnInvalid bool) ([]int, er | |||||
| return vals, nil | return vals, nil | ||||
| } | } | ||||
| // getInt64s returns list of int64 divided by given delimiter. | |||||
| func (k *Key) getInt64s(delim string, addInvalid, returnOnInvalid bool) ([]int64, error) { | |||||
| strs := k.Strings(delim) | |||||
| // parseInt64s transforms strings to int64s. | |||||
| func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) { | |||||
| vals := make([]int64, 0, len(strs)) | vals := make([]int64, 0, len(strs)) | ||||
| for _, str := range strs { | for _, str := range strs { | ||||
| val, err := strconv.ParseInt(str, 10, 64) | val, err := strconv.ParseInt(str, 10, 64) | ||||
| @@ -573,9 +642,8 @@ func (k *Key) getInt64s(delim string, addInvalid, returnOnInvalid bool) ([]int64 | |||||
| return vals, nil | return vals, nil | ||||
| } | } | ||||
| // getUints returns list of uint divided by given delimiter. | |||||
| func (k *Key) getUints(delim string, addInvalid, returnOnInvalid bool) ([]uint, error) { | |||||
| strs := k.Strings(delim) | |||||
| // parseUints transforms strings to uints. | |||||
| func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) { | |||||
| vals := make([]uint, 0, len(strs)) | vals := make([]uint, 0, len(strs)) | ||||
| for _, str := range strs { | for _, str := range strs { | ||||
| val, err := strconv.ParseUint(str, 10, 0) | val, err := strconv.ParseUint(str, 10, 0) | ||||
| @@ -589,9 +657,8 @@ func (k *Key) getUints(delim string, addInvalid, returnOnInvalid bool) ([]uint, | |||||
| return vals, nil | return vals, nil | ||||
| } | } | ||||
| // getUint64s returns list of uint64 divided by given delimiter. | |||||
| func (k *Key) getUint64s(delim string, addInvalid, returnOnInvalid bool) ([]uint64, error) { | |||||
| strs := k.Strings(delim) | |||||
| // parseUint64s transforms strings to uint64s. | |||||
| func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) { | |||||
| vals := make([]uint64, 0, len(strs)) | vals := make([]uint64, 0, len(strs)) | ||||
| for _, str := range strs { | for _, str := range strs { | ||||
| val, err := strconv.ParseUint(str, 10, 64) | val, err := strconv.ParseUint(str, 10, 64) | ||||
| @@ -605,9 +672,8 @@ func (k *Key) getUint64s(delim string, addInvalid, returnOnInvalid bool) ([]uint | |||||
| return vals, nil | return vals, nil | ||||
| } | } | ||||
| // getTimesFormat parses with given format and returns list of time.Time divided by given delimiter. | |||||
| func (k *Key) getTimesFormat(format, delim string, addInvalid, returnOnInvalid bool) ([]time.Time, error) { | |||||
| strs := k.Strings(delim) | |||||
| // parseTimesFormat transforms strings to times in given format. | |||||
| func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) { | |||||
| vals := make([]time.Time, 0, len(strs)) | vals := make([]time.Time, 0, len(strs)) | ||||
| for _, str := range strs { | for _, str := range strs { | ||||
| val, err := time.Parse(format, str) | val, err := time.Parse(format, str) | ||||
| @@ -48,16 +48,31 @@ func newParser(r io.Reader) *parser { | |||||
| } | } | ||||
| } | } | ||||
| // BOM handles header of BOM-UTF8 format. | |||||
| // BOM handles header of UTF-8, UTF-16 LE and UTF-16 BE's BOM format. | |||||
| // http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding | // http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding | ||||
| func (p *parser) BOM() error { | func (p *parser) BOM() error { | ||||
| mask, err := p.buf.Peek(3) | |||||
| mask, err := p.buf.Peek(2) | |||||
| if err != nil && err != io.EOF { | if err != nil && err != io.EOF { | ||||
| return err | return err | ||||
| } else if len(mask) < 3 { | |||||
| } else if len(mask) < 2 { | |||||
| return nil | return nil | ||||
| } else if mask[0] == 239 && mask[1] == 187 && mask[2] == 191 { | |||||
| } | |||||
| switch { | |||||
| case mask[0] == 254 && mask[1] == 255: | |||||
| fallthrough | |||||
| case mask[0] == 255 && mask[1] == 254: | |||||
| p.buf.Read(mask) | p.buf.Read(mask) | ||||
| case mask[0] == 239 && mask[1] == 187: | |||||
| mask, err := p.buf.Peek(3) | |||||
| if err != nil && err != io.EOF { | |||||
| return err | |||||
| } else if len(mask) < 3 { | |||||
| return nil | |||||
| } | |||||
| if mask[2] == 191 { | |||||
| p.buf.Read(mask) | |||||
| } | |||||
| } | } | ||||
| return nil | return nil | ||||
| } | } | ||||
| @@ -174,11 +189,11 @@ func (p *parser) readContinuationLines(val string) (string, error) { | |||||
| // are quotes \" or \'. | // are quotes \" or \'. | ||||
| // It returns false if any other parts also contain same kind of quotes. | // It returns false if any other parts also contain same kind of quotes. | ||||
| func hasSurroundedQuote(in string, quote byte) bool { | func hasSurroundedQuote(in string, quote byte) bool { | ||||
| return len(in) > 2 && in[0] == quote && in[len(in)-1] == quote && | |||||
| return len(in) >= 2 && in[0] == quote && in[len(in)-1] == quote && | |||||
| strings.IndexByte(in[1:], quote) == len(in)-2 | strings.IndexByte(in[1:], quote) == len(in)-2 | ||||
| } | } | ||||
| func (p *parser) readValue(in []byte, ignoreContinuation bool) (string, error) { | |||||
| func (p *parser) readValue(in []byte, ignoreContinuation, ignoreInlineComment bool) (string, error) { | |||||
| line := strings.TrimLeftFunc(string(in), unicode.IsSpace) | line := strings.TrimLeftFunc(string(in), unicode.IsSpace) | ||||
| if len(line) == 0 { | if len(line) == 0 { | ||||
| return "", nil | return "", nil | ||||
| @@ -202,18 +217,21 @@ func (p *parser) readValue(in []byte, ignoreContinuation bool) (string, error) { | |||||
| return line[startIdx : pos+startIdx], nil | return line[startIdx : pos+startIdx], nil | ||||
| } | } | ||||
| // Won't be able to reach here if value only contains whitespace. | |||||
| // Won't be able to reach here if value only contains whitespace | |||||
| line = strings.TrimSpace(line) | line = strings.TrimSpace(line) | ||||
| // Check continuation lines when desired. | |||||
| // Check continuation lines when desired | |||||
| if !ignoreContinuation && line[len(line)-1] == '\\' { | if !ignoreContinuation && line[len(line)-1] == '\\' { | ||||
| return p.readContinuationLines(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]) | |||||
| // Check if ignore inline comment | |||||
| if !ignoreInlineComment { | |||||
| i := strings.IndexAny(line, "#;") | |||||
| if i > -1 { | |||||
| p.comment.WriteString(line[i:]) | |||||
| line = strings.TrimSpace(line[:i]) | |||||
| } | |||||
| } | } | ||||
| // Trim single quotes | // Trim single quotes | ||||
| @@ -235,6 +253,7 @@ func (f *File) parse(reader io.Reader) (err error) { | |||||
| section, _ := f.NewSection(DEFAULT_SECTION) | section, _ := f.NewSection(DEFAULT_SECTION) | ||||
| var line []byte | var line []byte | ||||
| var inUnparseableSection bool | |||||
| for !p.isEOF { | for !p.isEOF { | ||||
| line, err = p.readUntil('\n') | line, err = p.readUntil('\n') | ||||
| if err != nil { | if err != nil { | ||||
| @@ -280,6 +299,21 @@ func (f *File) parse(reader io.Reader) (err error) { | |||||
| // Reset aotu-counter and comments | // Reset aotu-counter and comments | ||||
| p.comment.Reset() | p.comment.Reset() | ||||
| p.count = 1 | p.count = 1 | ||||
| inUnparseableSection = false | |||||
| for i := range f.options.UnparseableSections { | |||||
| if f.options.UnparseableSections[i] == name || | |||||
| (f.options.Insensitive && strings.ToLower(f.options.UnparseableSections[i]) == strings.ToLower(name)) { | |||||
| inUnparseableSection = true | |||||
| continue | |||||
| } | |||||
| } | |||||
| continue | |||||
| } | |||||
| if inUnparseableSection { | |||||
| section.isRawSection = true | |||||
| section.rawBody += string(line) | |||||
| continue | continue | ||||
| } | } | ||||
| @@ -287,11 +321,14 @@ func (f *File) parse(reader io.Reader) (err error) { | |||||
| if err != nil { | if err != nil { | ||||
| // Treat as boolean key when desired, and whole line is key name. | // Treat as boolean key when desired, and whole line is key name. | ||||
| if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys { | if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys { | ||||
| key, err := section.NewKey(string(line), "true") | |||||
| kname, err := p.readValue(line, f.options.IgnoreContinuation, f.options.IgnoreInlineComment) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| key, err := section.NewBooleanKey(kname) | |||||
| if err != nil { | if err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| key.isBooleanType = true | |||||
| key.Comment = strings.TrimSpace(p.comment.String()) | key.Comment = strings.TrimSpace(p.comment.String()) | ||||
| p.comment.Reset() | p.comment.Reset() | ||||
| continue | continue | ||||
| @@ -307,17 +344,16 @@ func (f *File) parse(reader io.Reader) (err error) { | |||||
| p.count++ | p.count++ | ||||
| } | } | ||||
| key, err := section.NewKey(kname, "") | |||||
| value, err := p.readValue(line[offset:], f.options.IgnoreContinuation, f.options.IgnoreInlineComment) | |||||
| if err != nil { | if err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| key.isAutoIncrement = isAutoIncr | |||||
| value, err := p.readValue(line[offset:], f.options.IgnoreContinuation) | |||||
| key, err := section.NewKey(kname, value) | |||||
| if err != nil { | if err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| key.SetValue(value) | |||||
| key.isAutoIncrement = isAutoIncr | |||||
| key.Comment = strings.TrimSpace(p.comment.String()) | key.Comment = strings.TrimSpace(p.comment.String()) | ||||
| p.comment.Reset() | p.comment.Reset() | ||||
| } | } | ||||
| @@ -28,10 +28,19 @@ type Section struct { | |||||
| keys map[string]*Key | keys map[string]*Key | ||||
| keyList []string | keyList []string | ||||
| keysHash map[string]string | keysHash map[string]string | ||||
| isRawSection bool | |||||
| rawBody string | |||||
| } | } | ||||
| func newSection(f *File, name string) *Section { | func newSection(f *File, name string) *Section { | ||||
| return &Section{f, "", name, make(map[string]*Key), make([]string, 0, 10), make(map[string]string)} | |||||
| return &Section{ | |||||
| f: f, | |||||
| name: name, | |||||
| keys: make(map[string]*Key), | |||||
| keyList: make([]string, 0, 10), | |||||
| keysHash: make(map[string]string), | |||||
| } | |||||
| } | } | ||||
| // Name returns name of Section. | // Name returns name of Section. | ||||
| @@ -39,6 +48,12 @@ func (s *Section) Name() string { | |||||
| return s.name | return s.name | ||||
| } | } | ||||
| // Body returns rawBody of Section if the section was marked as unparseable. | |||||
| // It still follows the other rules of the INI format surrounding leading/trailing whitespace. | |||||
| func (s *Section) Body() string { | |||||
| return strings.TrimSpace(s.rawBody) | |||||
| } | |||||
| // NewKey creates a new key to given section. | // NewKey creates a new key to given section. | ||||
| func (s *Section) NewKey(name, val string) (*Key, error) { | func (s *Section) NewKey(name, val string) (*Key, error) { | ||||
| if len(name) == 0 { | if len(name) == 0 { | ||||
| @@ -53,20 +68,33 @@ func (s *Section) NewKey(name, val string) (*Key, error) { | |||||
| } | } | ||||
| if inSlice(name, s.keyList) { | if inSlice(name, s.keyList) { | ||||
| s.keys[name].value = val | |||||
| if s.f.options.AllowShadows { | |||||
| if err := s.keys[name].addShadow(val); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| } else { | |||||
| s.keys[name].value = val | |||||
| } | |||||
| return s.keys[name], nil | return s.keys[name], nil | ||||
| } | } | ||||
| s.keyList = append(s.keyList, name) | s.keyList = append(s.keyList, name) | ||||
| s.keys[name] = &Key{ | |||||
| s: s, | |||||
| name: name, | |||||
| value: val, | |||||
| } | |||||
| s.keys[name] = newKey(s, name, val) | |||||
| s.keysHash[name] = val | s.keysHash[name] = val | ||||
| return s.keys[name], nil | return s.keys[name], nil | ||||
| } | } | ||||
| // NewBooleanKey creates a new boolean type key to given section. | |||||
| func (s *Section) NewBooleanKey(name string) (*Key, error) { | |||||
| key, err := s.NewKey(name, "true") | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| key.isBooleanType = true | |||||
| return key, nil | |||||
| } | |||||
| // GetKey returns key in section by given name. | // GetKey returns key in section by given name. | ||||
| func (s *Section) GetKey(name string) (*Key, error) { | func (s *Section) GetKey(name string) (*Key, error) { | ||||
| // FIXME: change to section level lock? | // FIXME: change to section level lock? | ||||
| @@ -204,3 +232,17 @@ func (s *Section) DeleteKey(name string) { | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| // ChildSections returns a list of child sections of current section. | |||||
| // For example, "[parent.child1]" and "[parent.child12]" are child sections | |||||
| // of section "[parent]". | |||||
| func (s *Section) ChildSections() []*Section { | |||||
| prefix := s.name + "." | |||||
| children := make([]*Section, 0, 3) | |||||
| for _, name := range s.f.sectionList { | |||||
| if strings.HasPrefix(name, prefix) { | |||||
| children = append(children, s.f.sections[name]) | |||||
| } | |||||
| } | |||||
| return children | |||||
| } | |||||
| @@ -78,34 +78,44 @@ func parseDelim(actual string) string { | |||||
| var reflectTime = reflect.TypeOf(time.Now()).Kind() | var reflectTime = reflect.TypeOf(time.Now()).Kind() | ||||
| // setSliceWithProperType sets proper values to slice based on its type. | // setSliceWithProperType sets proper values to slice based on its type. | ||||
| func setSliceWithProperType(key *Key, field reflect.Value, delim string) error { | |||||
| strs := key.Strings(delim) | |||||
| func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { | |||||
| var strs []string | |||||
| if allowShadow { | |||||
| strs = key.StringsWithShadows(delim) | |||||
| } else { | |||||
| strs = key.Strings(delim) | |||||
| } | |||||
| numVals := len(strs) | numVals := len(strs) | ||||
| if numVals == 0 { | if numVals == 0 { | ||||
| return nil | return nil | ||||
| } | } | ||||
| var vals interface{} | var vals interface{} | ||||
| var err error | |||||
| sliceOf := field.Type().Elem().Kind() | sliceOf := field.Type().Elem().Kind() | ||||
| switch sliceOf { | switch sliceOf { | ||||
| case reflect.String: | case reflect.String: | ||||
| vals = strs | vals = strs | ||||
| case reflect.Int: | case reflect.Int: | ||||
| vals = key.Ints(delim) | |||||
| vals, err = key.parseInts(strs, true, false) | |||||
| case reflect.Int64: | case reflect.Int64: | ||||
| vals = key.Int64s(delim) | |||||
| vals, err = key.parseInt64s(strs, true, false) | |||||
| case reflect.Uint: | case reflect.Uint: | ||||
| vals = key.Uints(delim) | |||||
| vals, err = key.parseUints(strs, true, false) | |||||
| case reflect.Uint64: | case reflect.Uint64: | ||||
| vals = key.Uint64s(delim) | |||||
| vals, err = key.parseUint64s(strs, true, false) | |||||
| case reflect.Float64: | case reflect.Float64: | ||||
| vals = key.Float64s(delim) | |||||
| vals, err = key.parseFloat64s(strs, true, false) | |||||
| case reflectTime: | case reflectTime: | ||||
| vals = key.Times(delim) | |||||
| vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false) | |||||
| default: | default: | ||||
| return fmt.Errorf("unsupported type '[]%s'", sliceOf) | return fmt.Errorf("unsupported type '[]%s'", sliceOf) | ||||
| } | } | ||||
| if isStrict { | |||||
| return err | |||||
| } | |||||
| slice := reflect.MakeSlice(field.Type(), numVals, numVals) | slice := reflect.MakeSlice(field.Type(), numVals, numVals) | ||||
| for i := 0; i < numVals; i++ { | for i := 0; i < numVals; i++ { | ||||
| @@ -130,10 +140,17 @@ func setSliceWithProperType(key *Key, field reflect.Value, delim string) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func wrapStrictError(err error, isStrict bool) error { | |||||
| if isStrict { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // setWithProperType sets proper value to field based on its type, | // setWithProperType sets proper value to field based on its type, | ||||
| // but it does not return error for failing parsing, | // but it does not return error for failing parsing, | ||||
| // because we want to use default value that is already assigned to strcut. | // 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 { | |||||
| func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { | |||||
| switch t.Kind() { | switch t.Kind() { | ||||
| case reflect.String: | case reflect.String: | ||||
| if len(key.String()) == 0 { | if len(key.String()) == 0 { | ||||
| @@ -143,7 +160,7 @@ func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim stri | |||||
| case reflect.Bool: | case reflect.Bool: | ||||
| boolVal, err := key.Bool() | boolVal, err := key.Bool() | ||||
| if err != nil { | if err != nil { | ||||
| return nil | |||||
| return wrapStrictError(err, isStrict) | |||||
| } | } | ||||
| field.SetBool(boolVal) | field.SetBool(boolVal) | ||||
| case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | ||||
| @@ -155,8 +172,8 @@ func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim stri | |||||
| } | } | ||||
| intVal, err := key.Int64() | intVal, err := key.Int64() | ||||
| if err != nil || intVal == 0 { | |||||
| return nil | |||||
| if err != nil { | |||||
| return wrapStrictError(err, isStrict) | |||||
| } | } | ||||
| field.SetInt(intVal) | field.SetInt(intVal) | ||||
| // byte is an alias for uint8, so supporting uint8 breaks support for byte | // byte is an alias for uint8, so supporting uint8 breaks support for byte | ||||
| @@ -170,31 +187,43 @@ func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim stri | |||||
| uintVal, err := key.Uint64() | uintVal, err := key.Uint64() | ||||
| if err != nil { | if err != nil { | ||||
| return nil | |||||
| return wrapStrictError(err, isStrict) | |||||
| } | } | ||||
| field.SetUint(uintVal) | field.SetUint(uintVal) | ||||
| case reflect.Float64: | |||||
| case reflect.Float32, reflect.Float64: | |||||
| floatVal, err := key.Float64() | floatVal, err := key.Float64() | ||||
| if err != nil { | if err != nil { | ||||
| return nil | |||||
| return wrapStrictError(err, isStrict) | |||||
| } | } | ||||
| field.SetFloat(floatVal) | field.SetFloat(floatVal) | ||||
| case reflectTime: | case reflectTime: | ||||
| timeVal, err := key.Time() | timeVal, err := key.Time() | ||||
| if err != nil { | if err != nil { | ||||
| return nil | |||||
| return wrapStrictError(err, isStrict) | |||||
| } | } | ||||
| field.Set(reflect.ValueOf(timeVal)) | field.Set(reflect.ValueOf(timeVal)) | ||||
| case reflect.Slice: | case reflect.Slice: | ||||
| return setSliceWithProperType(key, field, delim) | |||||
| return setSliceWithProperType(key, field, delim, allowShadow, isStrict) | |||||
| default: | default: | ||||
| return fmt.Errorf("unsupported type '%s'", t) | return fmt.Errorf("unsupported type '%s'", t) | ||||
| } | } | ||||
| return nil | return nil | ||||
| } | } | ||||
| func (s *Section) mapTo(val reflect.Value) error { | |||||
| func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool) { | |||||
| opts := strings.SplitN(tag, ",", 3) | |||||
| rawName = opts[0] | |||||
| if len(opts) > 1 { | |||||
| omitEmpty = opts[1] == "omitempty" | |||||
| } | |||||
| if len(opts) > 2 { | |||||
| allowShadow = opts[2] == "allowshadow" | |||||
| } | |||||
| return rawName, omitEmpty, allowShadow | |||||
| } | |||||
| func (s *Section) mapTo(val reflect.Value, isStrict bool) error { | |||||
| if val.Kind() == reflect.Ptr { | if val.Kind() == reflect.Ptr { | ||||
| val = val.Elem() | val = val.Elem() | ||||
| } | } | ||||
| @@ -209,8 +238,8 @@ func (s *Section) mapTo(val reflect.Value) error { | |||||
| continue | continue | ||||
| } | } | ||||
| opts := strings.SplitN(tag, ",", 2) // strip off possible omitempty | |||||
| fieldName := s.parseFieldName(tpField.Name, opts[0]) | |||||
| rawName, _, allowShadow := parseTagOptions(tag) | |||||
| fieldName := s.parseFieldName(tpField.Name, rawName) | |||||
| if len(fieldName) == 0 || !field.CanSet() { | if len(fieldName) == 0 || !field.CanSet() { | ||||
| continue | continue | ||||
| } | } | ||||
| @@ -223,7 +252,7 @@ func (s *Section) mapTo(val reflect.Value) error { | |||||
| if isAnonymous || isStruct { | if isAnonymous || isStruct { | ||||
| if sec, err := s.f.GetSection(fieldName); err == nil { | if sec, err := s.f.GetSection(fieldName); err == nil { | ||||
| if err = sec.mapTo(field); err != nil { | |||||
| if err = sec.mapTo(field, isStrict); err != nil { | |||||
| return fmt.Errorf("error mapping field(%s): %v", fieldName, err) | return fmt.Errorf("error mapping field(%s): %v", fieldName, err) | ||||
| } | } | ||||
| continue | continue | ||||
| @@ -231,7 +260,8 @@ func (s *Section) mapTo(val reflect.Value) error { | |||||
| } | } | ||||
| if key, err := s.GetKey(fieldName); err == nil { | if key, err := s.GetKey(fieldName); err == nil { | ||||
| if err = setWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { | |||||
| delim := parseDelim(tpField.Tag.Get("delim")) | |||||
| if err = setWithProperType(tpField.Type, key, field, delim, allowShadow, isStrict); err != nil { | |||||
| return fmt.Errorf("error mapping field(%s): %v", fieldName, err) | return fmt.Errorf("error mapping field(%s): %v", fieldName, err) | ||||
| } | } | ||||
| } | } | ||||
| @@ -250,7 +280,22 @@ func (s *Section) MapTo(v interface{}) error { | |||||
| return errors.New("cannot map to non-pointer struct") | return errors.New("cannot map to non-pointer struct") | ||||
| } | } | ||||
| return s.mapTo(val) | |||||
| return s.mapTo(val, false) | |||||
| } | |||||
| // MapTo maps section to given struct in strict mode, | |||||
| // which returns all possible error including value parsing error. | |||||
| func (s *Section) StrictMapTo(v interface{}) error { | |||||
| typ := reflect.TypeOf(v) | |||||
| val := reflect.ValueOf(v) | |||||
| if typ.Kind() == reflect.Ptr { | |||||
| typ = typ.Elem() | |||||
| val = val.Elem() | |||||
| } else { | |||||
| return errors.New("cannot map to non-pointer struct") | |||||
| } | |||||
| return s.mapTo(val, true) | |||||
| } | } | ||||
| // MapTo maps file to given struct. | // MapTo maps file to given struct. | ||||
| @@ -258,6 +303,12 @@ func (f *File) MapTo(v interface{}) error { | |||||
| return f.Section("").MapTo(v) | return f.Section("").MapTo(v) | ||||
| } | } | ||||
| // MapTo maps file to given struct in strict mode, | |||||
| // which returns all possible error including value parsing error. | |||||
| func (f *File) StrictMapTo(v interface{}) error { | |||||
| return f.Section("").StrictMapTo(v) | |||||
| } | |||||
| // MapTo maps data sources to given struct with name mapper. | // MapTo maps data sources to given struct with name mapper. | ||||
| func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { | func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { | ||||
| cfg, err := Load(source, others...) | cfg, err := Load(source, others...) | ||||
| @@ -268,11 +319,28 @@ func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, other | |||||
| return cfg.MapTo(v) | return cfg.MapTo(v) | ||||
| } | } | ||||
| // StrictMapToWithMapper maps data sources to given struct with name mapper in strict mode, | |||||
| // which returns all possible error including value parsing error. | |||||
| func StrictMapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { | |||||
| cfg, err := Load(source, others...) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| cfg.NameMapper = mapper | |||||
| return cfg.StrictMapTo(v) | |||||
| } | |||||
| // MapTo maps data sources to given struct. | // MapTo maps data sources to given struct. | ||||
| func MapTo(v, source interface{}, others ...interface{}) error { | func MapTo(v, source interface{}, others ...interface{}) error { | ||||
| return MapToWithMapper(v, nil, source, others...) | return MapToWithMapper(v, nil, source, others...) | ||||
| } | } | ||||
| // StrictMapTo maps data sources to given struct in strict mode, | |||||
| // which returns all possible error including value parsing error. | |||||
| func StrictMapTo(v, source interface{}, others ...interface{}) error { | |||||
| return StrictMapToWithMapper(v, nil, source, others...) | |||||
| } | |||||
| // reflectSliceWithProperType does the opposite thing as setSliceWithProperType. | // reflectSliceWithProperType does the opposite thing as setSliceWithProperType. | ||||
| func reflectSliceWithProperType(key *Key, field reflect.Value, delim string) error { | func reflectSliceWithProperType(key *Key, field reflect.Value, delim string) error { | ||||
| slice := field.Slice(0, field.Len()) | slice := field.Slice(0, field.Len()) | ||||
| @@ -340,10 +408,11 @@ func isEmptyValue(v reflect.Value) bool { | |||||
| return v.Uint() == 0 | return v.Uint() == 0 | ||||
| case reflect.Float32, reflect.Float64: | case reflect.Float32, reflect.Float64: | ||||
| return v.Float() == 0 | return v.Float() == 0 | ||||
| case reflectTime: | |||||
| return v.Interface().(time.Time).IsZero() | |||||
| case reflect.Interface, reflect.Ptr: | case reflect.Interface, reflect.Ptr: | ||||
| return v.IsNil() | return v.IsNil() | ||||
| case reflectTime: | |||||
| t, ok := v.Interface().(time.Time) | |||||
| return ok && t.IsZero() | |||||
| } | } | ||||
| return false | return false | ||||
| } | } | ||||
| @@ -1466,10 +1466,11 @@ | |||||
| "revisionTime": "2016-04-11T21:29:32Z" | "revisionTime": "2016-04-11T21:29:32Z" | ||||
| }, | }, | ||||
| { | { | ||||
| "checksumSHA1": "YRD335tkMvgHzkfbfveMUpsE3Bw=", | |||||
| "checksumSHA1": "MMb7aeIRnJq17iQvuGvevymOIYQ=", | |||||
| "origin": "github.com/go-gitea/ini", | |||||
| "path": "gopkg.in/ini.v1", | "path": "gopkg.in/ini.v1", | ||||
| "revision": "6e4869b434bd001f6983749881c7ead3545887d8", | |||||
| "revisionTime": "2016-08-27T06:11:18Z" | |||||
| "revision": "88679ba677ac064c7880c9bde81ef5b9fd132e82", | |||||
| "revisionTime": "2017-08-04T04:10:12Z" | |||||
| }, | }, | ||||
| { | { | ||||
| "checksumSHA1": "7jPSjzw3mckHVQ2SjY4NvtIJR4g=", | "checksumSHA1": "7jPSjzw3mckHVQ2SjY4NvtIJR4g=", | ||||