|
@@ -16,7 +16,6 @@
|
|
|
package ini
|
|
package ini
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
- "bufio"
|
|
|
|
|
"bytes"
|
|
"bytes"
|
|
|
"errors"
|
|
"errors"
|
|
|
"fmt"
|
|
"fmt"
|
|
@@ -31,25 +30,35 @@ import (
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
const (
|
|
|
|
|
+ // Name for default section. You can use this constant or the string literal.
|
|
|
|
|
+ // In most of cases, an empty string is all you need to access the section.
|
|
|
DEFAULT_SECTION = "DEFAULT"
|
|
DEFAULT_SECTION = "DEFAULT"
|
|
|
|
|
+
|
|
|
// Maximum allowed depth when recursively substituing variable names.
|
|
// Maximum allowed depth when recursively substituing variable names.
|
|
|
_DEPTH_VALUES = 99
|
|
_DEPTH_VALUES = 99
|
|
|
-
|
|
|
|
|
- _VERSION = "1.2.6"
|
|
|
|
|
|
|
+ _VERSION = "1.21.1"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+// Version returns current package version literal.
|
|
|
func Version() string {
|
|
func Version() string {
|
|
|
return _VERSION
|
|
return _VERSION
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
var (
|
|
|
|
|
+ // Delimiter to determine or compose a new line.
|
|
|
|
|
+ // This variable will be changed to "\r\n" automatically on Windows
|
|
|
|
|
+ // at package init time.
|
|
|
LineBreak = "\n"
|
|
LineBreak = "\n"
|
|
|
|
|
|
|
|
// Variable regexp pattern: %(variable)s
|
|
// Variable regexp pattern: %(variable)s
|
|
|
varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`)
|
|
varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`)
|
|
|
|
|
|
|
|
- // Write spaces around "=" to look better.
|
|
|
|
|
|
|
+ // Indicate whether to align "=" sign with spaces to produce pretty output
|
|
|
|
|
+ // or reduce all possible spaces for compact format.
|
|
|
PrettyFormat = true
|
|
PrettyFormat = true
|
|
|
|
|
+
|
|
|
|
|
+ // Explicitly write DEFAULT section header
|
|
|
|
|
+ DefaultHeader = false
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
func init() {
|
|
@@ -67,501 +76,41 @@ func inSlice(str string, s []string) bool {
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// dataSource is a interface that returns file content.
|
|
|
|
|
|
|
+// dataSource is an interface that returns object which can be read and closed.
|
|
|
type dataSource interface {
|
|
type dataSource interface {
|
|
|
- Reader() (io.Reader, error)
|
|
|
|
|
|
|
+ ReadCloser() (io.ReadCloser, error)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// sourceFile represents an object that contains content on the local file system.
|
|
|
type sourceFile struct {
|
|
type sourceFile struct {
|
|
|
name string
|
|
name string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (s sourceFile) Reader() (io.Reader, error) {
|
|
|
|
|
|
|
+func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) {
|
|
|
return os.Open(s.name)
|
|
return os.Open(s.name)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type sourceData struct {
|
|
|
|
|
- data []byte
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func (s *sourceData) Reader() (io.Reader, error) {
|
|
|
|
|
- return 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
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// parseBool returns the boolean value represented by the string.
|
|
|
|
|
-//
|
|
|
|
|
-// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On,
|
|
|
|
|
-// 0, f, F, FALSE, false, False, NO, no, No, 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", "ON", "on", "On":
|
|
|
|
|
- return true, nil
|
|
|
|
|
- case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "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)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 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
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 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
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 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
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 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) {
|
|
|
|
|
- k.value = 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()
|
|
|
|
|
- defer s.f.lock.RUnlock()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- key := s.keys[name]
|
|
|
|
|
- if key == nil {
|
|
|
|
|
- // Check if it is a child-section.
|
|
|
|
|
- if i := strings.LastIndex(s.name, "."); i > -1 {
|
|
|
|
|
- return s.f.Section(s.name[:i]).GetKey(name)
|
|
|
|
|
- }
|
|
|
|
|
- return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name)
|
|
|
|
|
- }
|
|
|
|
|
- return key, nil
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 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
|
|
|
|
|
|
|
+type bytesReadCloser struct {
|
|
|
|
|
+ reader io.Reader
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 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
|
|
|
|
|
|
|
+func (rc *bytesReadCloser) Read(p []byte) (n int, err error) {
|
|
|
|
|
+ return rc.reader.Read(p)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 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
|
|
|
|
|
|
|
+func (rc *bytesReadCloser) Close() error {
|
|
|
|
|
+ return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 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
|
|
|
|
|
|
|
+// sourceData represents an object that contains content in memory.
|
|
|
|
|
+type sourceData struct {
|
|
|
|
|
+ data []byte
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 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
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+func (s *sourceData) ReadCloser() (io.ReadCloser, error) {
|
|
|
|
|
+ return &bytesReadCloser{bytes.NewReader(s.data)}, 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.
|
|
|
type File struct {
|
|
type File struct {
|
|
|
// Should make things safe, but sometimes doesn't matter.
|
|
// Should make things safe, but sometimes doesn't matter.
|
|
@@ -577,16 +126,20 @@ type File struct {
|
|
|
// To keep data in order.
|
|
// To keep data in order.
|
|
|
sectionList []string
|
|
sectionList []string
|
|
|
|
|
|
|
|
|
|
+ options LoadOptions
|
|
|
|
|
+
|
|
|
NameMapper
|
|
NameMapper
|
|
|
|
|
+ ValueMapper
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// newFile initializes File object with given data sources.
|
|
// newFile initializes File object with given data sources.
|
|
|
-func newFile(dataSources []dataSource) *File {
|
|
|
|
|
|
|
+func newFile(dataSources []dataSource, opts LoadOptions) *File {
|
|
|
return &File{
|
|
return &File{
|
|
|
BlockMode: true,
|
|
BlockMode: true,
|
|
|
dataSources: dataSources,
|
|
dataSources: dataSources,
|
|
|
sections: make(map[string]*Section),
|
|
sections: make(map[string]*Section),
|
|
|
sectionList: make([]string, 0, 10),
|
|
sectionList: make([]string, 0, 10),
|
|
|
|
|
+ options: opts,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -601,9 +154,19 @@ func parseDataSource(source interface{}) (dataSource, error) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 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) {
|
|
|
|
|
|
|
+type LoadOptions struct {
|
|
|
|
|
+ // Loose indicates whether the parser should ignore nonexistent files or return error.
|
|
|
|
|
+ Loose bool
|
|
|
|
|
+ // Insensitive indicates whether the parser forces all section and key names to lowercase.
|
|
|
|
|
+ Insensitive bool
|
|
|
|
|
+ // IgnoreContinuation indicates whether to ignore continuation lines while parsing.
|
|
|
|
|
+ IgnoreContinuation bool
|
|
|
|
|
+ // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing.
|
|
|
|
|
+ // This type of keys are mostly used in my.cnf.
|
|
|
|
|
+ AllowBooleanKeys bool
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) {
|
|
|
sources := make([]dataSource, len(others)+1)
|
|
sources := make([]dataSource, len(others)+1)
|
|
|
sources[0], err = parseDataSource(source)
|
|
sources[0], err = parseDataSource(source)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -615,8 +178,30 @@ func Load(source interface{}, others ...interface{}) (_ *File, err error) {
|
|
|
return nil, err
|
|
return nil, err
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- f := newFile(sources)
|
|
|
|
|
- return f, f.Reload()
|
|
|
|
|
|
|
+ f := newFile(sources, opts)
|
|
|
|
|
+ if err = f.Reload(); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return f, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Load loads and parses from INI data sources.
|
|
|
|
|
+// Arguments can be mixed of file name with string type, or raw data in []byte.
|
|
|
|
|
+// It will return error if list contains nonexistent files.
|
|
|
|
|
+func Load(source interface{}, others ...interface{}) (*File, error) {
|
|
|
|
|
+ return LoadSources(LoadOptions{}, source, others...)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// LooseLoad has exactly same functionality as Load function
|
|
|
|
|
+// except it ignores nonexistent files instead of returning error.
|
|
|
|
|
+func LooseLoad(source interface{}, others ...interface{}) (*File, error) {
|
|
|
|
|
+ return LoadSources(LoadOptions{Loose: true}, source, others...)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// InsensitiveLoad has exactly same functionality as Load function
|
|
|
|
|
+// except it forces all section and key names to be lowercased.
|
|
|
|
|
+func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) {
|
|
|
|
|
+ return LoadSources(LoadOptions{Insensitive: true}, source, others...)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Empty returns an empty file object.
|
|
// Empty returns an empty file object.
|
|
@@ -630,6 +215,8 @@ func Empty() *File {
|
|
|
func (f *File) NewSection(name string) (*Section, error) {
|
|
func (f *File) NewSection(name string) (*Section, error) {
|
|
|
if len(name) == 0 {
|
|
if len(name) == 0 {
|
|
|
return nil, errors.New("error creating new section: empty section name")
|
|
return nil, errors.New("error creating new section: empty section name")
|
|
|
|
|
+ } else if f.options.Insensitive && name != DEFAULT_SECTION {
|
|
|
|
|
+ name = strings.ToLower(name)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if f.BlockMode {
|
|
if f.BlockMode {
|
|
@@ -660,6 +247,8 @@ func (f *File) NewSections(names ...string) (err error) {
|
|
|
func (f *File) GetSection(name string) (*Section, error) {
|
|
func (f *File) GetSection(name string) (*Section, error) {
|
|
|
if len(name) == 0 {
|
|
if len(name) == 0 {
|
|
|
name = DEFAULT_SECTION
|
|
name = DEFAULT_SECTION
|
|
|
|
|
+ } else if f.options.Insensitive {
|
|
|
|
|
+ name = strings.ToLower(name)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if f.BlockMode {
|
|
if f.BlockMode {
|
|
@@ -669,7 +258,7 @@ func (f *File) GetSection(name string) (*Section, error) {
|
|
|
|
|
|
|
|
sec := f.sections[name]
|
|
sec := f.sections[name]
|
|
|
if sec == nil {
|
|
if sec == nil {
|
|
|
- return nil, fmt.Errorf("error when getting section: section '%s' not exists", name)
|
|
|
|
|
|
|
+ return nil, fmt.Errorf("section '%s' does not exist", name)
|
|
|
}
|
|
}
|
|
|
return sec, nil
|
|
return sec, nil
|
|
|
}
|
|
}
|
|
@@ -678,7 +267,7 @@ func (f *File) GetSection(name string) (*Section, error) {
|
|
|
func (f *File) Section(name string) *Section {
|
|
func (f *File) Section(name string) *Section {
|
|
|
sec, err := f.GetSection(name)
|
|
sec, err := f.GetSection(name)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- // It's OK here because the only possible error is empty section name,
|
|
|
|
|
|
|
+ // 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.
|
|
// but if it's empty, this piece of code won't be executed.
|
|
|
sec, _ = f.NewSection(name)
|
|
sec, _ = f.NewSection(name)
|
|
|
return sec
|
|
return sec
|
|
@@ -722,200 +311,25 @@ func (f *File) DeleteSection(name string) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func cutComment(str string) string {
|
|
|
|
|
- i := strings.Index(str, "#")
|
|
|
|
|
- if i == -1 {
|
|
|
|
|
- return str
|
|
|
|
|
- }
|
|
|
|
|
- return str[:i]
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// parse parses data through an io.Reader.
|
|
|
|
|
-func (f *File) parse(reader io.Reader) error {
|
|
|
|
|
- buf := bufio.NewReader(reader)
|
|
|
|
|
-
|
|
|
|
|
- // Handle BOM-UTF8.
|
|
|
|
|
- // http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding
|
|
|
|
|
- mask, err := buf.Peek(3)
|
|
|
|
|
- if err == nil && len(mask) >= 3 && mask[0] == 239 && mask[1] == 187 && mask[2] == 191 {
|
|
|
|
|
- buf.Read(mask)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- count := 1
|
|
|
|
|
- comments := ""
|
|
|
|
|
- isEnd := false
|
|
|
|
|
-
|
|
|
|
|
- section, err := f.NewSection(DEFAULT_SECTION)
|
|
|
|
|
|
|
+func (f *File) reload(s dataSource) error {
|
|
|
|
|
+ r, err := s.ReadCloser()
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
|
|
+ defer r.Close()
|
|
|
|
|
|
|
|
- for {
|
|
|
|
|
- line, err := buf.ReadString('\n')
|
|
|
|
|
- line = strings.TrimSpace(line)
|
|
|
|
|
- length := len(line)
|
|
|
|
|
-
|
|
|
|
|
- // Check error and ignore io.EOF just for a moment.
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- if err != io.EOF {
|
|
|
|
|
- return fmt.Errorf("error reading next line: %v", err)
|
|
|
|
|
- }
|
|
|
|
|
- // The last line of file could be an empty line.
|
|
|
|
|
- if length == 0 {
|
|
|
|
|
- break
|
|
|
|
|
- }
|
|
|
|
|
- isEnd = true
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Skip empty lines.
|
|
|
|
|
- if length == 0 {
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- switch {
|
|
|
|
|
- case line[0] == '#' || line[0] == ';': // Comments.
|
|
|
|
|
- if len(comments) == 0 {
|
|
|
|
|
- comments = line
|
|
|
|
|
- } else {
|
|
|
|
|
- comments += LineBreak + line
|
|
|
|
|
- }
|
|
|
|
|
- continue
|
|
|
|
|
- case line[0] == '[' && line[length-1] == ']': // New sction.
|
|
|
|
|
- name := strings.TrimSpace(line[1 : length-1])
|
|
|
|
|
- section, err = f.NewSection(name)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return err
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if len(comments) > 0 {
|
|
|
|
|
- section.Comment = comments
|
|
|
|
|
- comments = ""
|
|
|
|
|
- }
|
|
|
|
|
- // Reset counter.
|
|
|
|
|
- count = 1
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Other possibilities.
|
|
|
|
|
- var (
|
|
|
|
|
- i int
|
|
|
|
|
- keyQuote string
|
|
|
|
|
- kname string
|
|
|
|
|
- valQuote string
|
|
|
|
|
- val string
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- // Key name surrounded by quotes.
|
|
|
|
|
- if line[0] == '"' {
|
|
|
|
|
- if length > 6 && line[0:3] == `"""` {
|
|
|
|
|
- keyQuote = `"""`
|
|
|
|
|
- } else {
|
|
|
|
|
- keyQuote = `"`
|
|
|
|
|
- }
|
|
|
|
|
- } else if line[0] == '`' {
|
|
|
|
|
- keyQuote = "`"
|
|
|
|
|
- }
|
|
|
|
|
- if len(keyQuote) > 0 {
|
|
|
|
|
- qLen := len(keyQuote)
|
|
|
|
|
- pos := strings.Index(line[qLen:], keyQuote)
|
|
|
|
|
- if pos == -1 {
|
|
|
|
|
- return fmt.Errorf("error parsing line: missing closing key quote: %s", line)
|
|
|
|
|
- }
|
|
|
|
|
- pos = pos + qLen
|
|
|
|
|
- i = strings.IndexAny(line[pos:], "=:")
|
|
|
|
|
- if i < 0 {
|
|
|
|
|
- return fmt.Errorf("error parsing line: key-value delimiter not found: %s", line)
|
|
|
|
|
- } else if i == pos {
|
|
|
|
|
- return fmt.Errorf("error parsing line: key is empty: %s", line)
|
|
|
|
|
- }
|
|
|
|
|
- i = i + pos
|
|
|
|
|
- kname = line[qLen:pos] // Just keep spaces inside quotes.
|
|
|
|
|
- } else {
|
|
|
|
|
- i = strings.IndexAny(line, "=:")
|
|
|
|
|
- if i < 0 {
|
|
|
|
|
- return fmt.Errorf("error parsing line: key-value delimiter not found: %s", line)
|
|
|
|
|
- } else if i == 0 {
|
|
|
|
|
- return fmt.Errorf("error parsing line: key is empty: %s", line)
|
|
|
|
|
- }
|
|
|
|
|
- kname = strings.TrimSpace(line[0:i])
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- isAutoIncr := false
|
|
|
|
|
- // Auto increment.
|
|
|
|
|
- if kname == "-" {
|
|
|
|
|
- isAutoIncr = true
|
|
|
|
|
- kname = "#" + fmt.Sprint(count)
|
|
|
|
|
- count++
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- lineRight := strings.TrimSpace(line[i+1:])
|
|
|
|
|
- lineRightLength := len(lineRight)
|
|
|
|
|
- firstChar := ""
|
|
|
|
|
- if lineRightLength >= 2 {
|
|
|
|
|
- firstChar = lineRight[0:1]
|
|
|
|
|
- }
|
|
|
|
|
- if firstChar == "`" {
|
|
|
|
|
- valQuote = "`"
|
|
|
|
|
- } else if lineRightLength >= 6 && lineRight[0:3] == `"""` {
|
|
|
|
|
- valQuote = `"""`
|
|
|
|
|
- }
|
|
|
|
|
- if len(valQuote) > 0 {
|
|
|
|
|
- qLen := len(valQuote)
|
|
|
|
|
- pos := strings.LastIndex(lineRight[qLen:], valQuote)
|
|
|
|
|
- // For multiple lines value.
|
|
|
|
|
- if pos == -1 {
|
|
|
|
|
- isEnd := false
|
|
|
|
|
- val = lineRight[qLen:] + "\n"
|
|
|
|
|
- for {
|
|
|
|
|
- next, err := buf.ReadString('\n')
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- if err != io.EOF {
|
|
|
|
|
- return err
|
|
|
|
|
- }
|
|
|
|
|
- isEnd = true
|
|
|
|
|
- }
|
|
|
|
|
- pos = strings.LastIndex(next, valQuote)
|
|
|
|
|
- if pos > -1 {
|
|
|
|
|
- val += next[:pos]
|
|
|
|
|
- break
|
|
|
|
|
- }
|
|
|
|
|
- val += next
|
|
|
|
|
- if isEnd {
|
|
|
|
|
- return fmt.Errorf("error parsing line: missing closing key quote from '%s' to '%s'", line, next)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- val = lineRight[qLen : pos+qLen]
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- val = strings.TrimSpace(cutComment(lineRight[0:]))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- k, err := section.NewKey(kname, val)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return err
|
|
|
|
|
- }
|
|
|
|
|
- k.isAutoIncr = isAutoIncr
|
|
|
|
|
- if len(comments) > 0 {
|
|
|
|
|
- k.Comment = comments
|
|
|
|
|
- comments = ""
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if isEnd {
|
|
|
|
|
- break
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return nil
|
|
|
|
|
|
|
+ return f.parse(r)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Reload reloads and parses all data sources.
|
|
// Reload reloads and parses all data sources.
|
|
|
-func (f *File) Reload() error {
|
|
|
|
|
|
|
+func (f *File) Reload() (err error) {
|
|
|
for _, s := range f.dataSources {
|
|
for _, s := range f.dataSources {
|
|
|
- r, err := s.Reader()
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return err
|
|
|
|
|
- }
|
|
|
|
|
- if err = f.parse(r); err != nil {
|
|
|
|
|
|
|
+ if err = f.reload(s); err != nil {
|
|
|
|
|
+ // In loose mode, we create an empty default section for nonexistent files.
|
|
|
|
|
+ if os.IsNotExist(err) && f.options.Loose {
|
|
|
|
|
+ f.parse(bytes.NewBuffer(nil))
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -939,8 +353,10 @@ func (f *File) Append(source interface{}, others ...interface{}) error {
|
|
|
return f.Reload()
|
|
return f.Reload()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// SaveTo writes content to filesystem.
|
|
|
|
|
-func (f *File) SaveTo(filename string) (err error) {
|
|
|
|
|
|
|
+// 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) {
|
|
|
equalSign := "="
|
|
equalSign := "="
|
|
|
if PrettyFormat {
|
|
if PrettyFormat {
|
|
|
equalSign = " = "
|
|
equalSign = " = "
|
|
@@ -955,63 +371,131 @@ func (f *File) SaveTo(filename string) (err error) {
|
|
|
sec.Comment = "; " + sec.Comment
|
|
sec.Comment = "; " + sec.Comment
|
|
|
}
|
|
}
|
|
|
if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil {
|
|
if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil {
|
|
|
- return err
|
|
|
|
|
|
|
+ return 0, err
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if i > 0 {
|
|
|
|
|
|
|
+ if i > 0 || DefaultHeader {
|
|
|
if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
|
|
if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
|
|
|
- return err
|
|
|
|
|
|
|
+ return 0, err
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // Write nothing if default section is empty.
|
|
|
|
|
|
|
+ // Write nothing if default section is empty
|
|
|
if len(sec.keyList) == 0 {
|
|
if len(sec.keyList) == 0 {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Count and generate alignment length and buffer spaces using the
|
|
|
|
|
+ // longest key. Keys may be modifed if they contain certain characters so
|
|
|
|
|
+ // we need to take that into account in our calculation.
|
|
|
|
|
+ alignLength := 0
|
|
|
|
|
+ if PrettyFormat {
|
|
|
|
|
+ for _, kname := range sec.keyList {
|
|
|
|
|
+ keyLength := len(kname)
|
|
|
|
|
+ // First case will surround key by ` and second by """
|
|
|
|
|
+ if strings.ContainsAny(kname, "\"=:") {
|
|
|
|
|
+ keyLength += 2
|
|
|
|
|
+ } else if strings.Contains(kname, "`") {
|
|
|
|
|
+ keyLength += 6
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if keyLength > alignLength {
|
|
|
|
|
+ alignLength = keyLength
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ alignSpaces := bytes.Repeat([]byte(" "), alignLength)
|
|
|
|
|
+
|
|
|
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 {
|
|
|
|
|
+ if len(indent) > 0 && sname != DEFAULT_SECTION {
|
|
|
|
|
+ buf.WriteString(indent)
|
|
|
|
|
+ }
|
|
|
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 {
|
|
if _, err = buf.WriteString(key.Comment + LineBreak); err != nil {
|
|
|
- return err
|
|
|
|
|
|
|
+ return 0, err
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if len(indent) > 0 && sname != DEFAULT_SECTION {
|
|
|
|
|
+ buf.WriteString(indent)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
switch {
|
|
switch {
|
|
|
- case key.isAutoIncr:
|
|
|
|
|
|
|
+ case key.isAutoIncrement:
|
|
|
kname = "-"
|
|
kname = "-"
|
|
|
- case strings.Contains(kname, "`") || strings.Contains(kname, `"`):
|
|
|
|
|
- kname = `"""` + kname + `"""`
|
|
|
|
|
- case strings.Contains(kname, `=`) || strings.Contains(kname, `:`):
|
|
|
|
|
|
|
+ case strings.ContainsAny(kname, "\"=:"):
|
|
|
kname = "`" + kname + "`"
|
|
kname = "`" + kname + "`"
|
|
|
|
|
+ case strings.Contains(kname, "`"):
|
|
|
|
|
+ kname = `"""` + kname + `"""`
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, err = buf.WriteString(kname); err != nil {
|
|
|
|
|
+ return 0, err
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if key.isBooleanType {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Write out alignment spaces before "=" sign
|
|
|
|
|
+ if PrettyFormat {
|
|
|
|
|
+ buf.Write(alignSpaces[:alignLength-len(kname)])
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
val := key.value
|
|
val := key.value
|
|
|
- // In case key value contains "\n", "`" or "\"".
|
|
|
|
|
- if strings.Contains(val, "\n") || strings.Contains(val, "`") || strings.Contains(val, `"`) {
|
|
|
|
|
|
|
+ // In case key value contains "\n", "`", "\"", "#" or ";"
|
|
|
|
|
+ if strings.ContainsAny(val, "\n`") {
|
|
|
val = `"""` + val + `"""`
|
|
val = `"""` + val + `"""`
|
|
|
|
|
+ } else if strings.ContainsAny(val, "#;") {
|
|
|
|
|
+ val = "`" + val + "`"
|
|
|
}
|
|
}
|
|
|
- if _, err = buf.WriteString(kname + equalSign + val + LineBreak); err != nil {
|
|
|
|
|
- return err
|
|
|
|
|
|
|
+ if _, err = buf.WriteString(equalSign + val + LineBreak); err != nil {
|
|
|
|
|
+ return 0, err
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Put a line between sections.
|
|
|
|
|
|
|
+ // Put a line between sections
|
|
|
if _, err = buf.WriteString(LineBreak); err != nil {
|
|
if _, err = buf.WriteString(LineBreak); err != nil {
|
|
|
- return err
|
|
|
|
|
|
|
+ return 0, err
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- fw, err := os.Create(filename)
|
|
|
|
|
|
|
+ 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 {
|
|
if err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
- if _, err = buf.WriteTo(fw); err != nil {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if _, err = f.WriteToIndent(fw, indent); err != nil {
|
|
|
|
|
+ fw.Close()
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
- return fw.Close()
|
|
|
|
|
|
|
+ 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, "")
|
|
|
}
|
|
}
|