// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // Package ini provides INI file read and write functionality in Go. package ini import ( "bufio" "bytes" "errors" "fmt" "io" "os" "regexp" "runtime" "strconv" "strings" "sync" "time" ) const ( DEFAULT_SECTION = "DEFAULT" // Maximum allowed depth when recursively substituing variable names. _DEPTH_VALUES = 99 _VERSION = "1.6.0" ) func Version() string { return _VERSION } var ( LineBreak = "\n" // Variable regexp pattern: %(variable)s varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`) // Write spaces around "=" to look better. PrettyFormat = true ) func init() { if runtime.GOOS == "windows" { LineBreak = "\r\n" } } func inSlice(str string, s []string) bool { for _, v := range s { if str == v { return true } } return false } // dataSource is a interface that returns file content. type dataSource interface { ReadCloser() (io.ReadCloser, error) } type sourceFile struct { name string } func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { return os.Open(s.name) } type bytesReadCloser struct { reader io.Reader } func (rc *bytesReadCloser) Read(p []byte) (n int, err error) { return rc.reader.Read(p) } func (rc *bytesReadCloser) Close() error { return nil } type sourceData struct { data []byte } func (s *sourceData) ReadCloser() (io.ReadCloser, error) { return &bytesReadCloser{bytes.NewReader(s.data)}, nil } // ____ __. // | |/ _|____ ___.__. // | <_/ __ < | | // | | \ ___/\___ | // |____|__ \___ > ____| // \/ \/\/ // Key represents a key under a section. type Key struct { s *Section Comment string name string value string isAutoIncr bool } // Name returns name of key. func (k *Key) Name() string { return k.name } // Value returns raw value of key for performance purpose. func (k *Key) Value() string { return k.value } // String returns string representation of value. func (k *Key) String() string { val := k.value if strings.Index(val, "%") == -1 { return val } for i := 0; i < _DEPTH_VALUES; i++ { vr := varPattern.FindString(val) if len(vr) == 0 { break } // Take off leading '%(' and trailing ')s'. noption := strings.TrimLeft(vr, "%(") noption = strings.TrimRight(noption, ")s") // Search in the same section. nk, err := k.s.GetKey(noption) if err != nil { // Search again in default section. nk, _ = k.s.f.Section("").GetKey(noption) } // Substitute by new value and take off leading '%(' and trailing ')s'. val = strings.Replace(val, vr, nk.value, -1) } return val } // Validate accepts a validate function which can // return modifed result as key value. func (k *Key) Validate(fn func(string) string) string { return fn(k.String()) } // parseBool returns the boolean value represented by the string. // // It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, 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) } // Uint returns uint type valued. func (k *Key) Uint() (uint, error) { u, e := strconv.ParseUint(k.String(), 10, 64) return uint(u), e } // Uint64 returns uint64 type value. func (k *Key) Uint64() (uint64, error) { return strconv.ParseUint(k.String(), 10, 64) } // Duration returns time.Duration type value. func (k *Key) Duration() (time.Duration, error) { return time.ParseDuration(k.String()) } // TimeFormat parses with given format and returns time.Time type value. func (k *Key) TimeFormat(format string) (time.Time, error) { return time.Parse(format, k.String()) } // Time parses with RFC3339 format and returns time.Time type value. func (k *Key) Time() (time.Time, error) { return k.TimeFormat(time.RFC3339) } // MustString returns default value if key value is empty. func (k *Key) MustString(defaultVal string) string { val := k.String() if len(val) == 0 { return defaultVal } return val } // MustBool always returns value without error, // it returns false if error occurs. func (k *Key) MustBool(defaultVal ...bool) bool { val, err := k.Bool() if len(defaultVal) > 0 && err != nil { return defaultVal[0] } return val } // MustFloat64 always returns value without error, // it returns 0.0 if error occurs. func (k *Key) MustFloat64(defaultVal ...float64) float64 { val, err := k.Float64() if len(defaultVal) > 0 && err != nil { return defaultVal[0] } return val } // MustInt always returns value without error, // it returns 0 if error occurs. func (k *Key) MustInt(defaultVal ...int) int { val, err := k.Int() if len(defaultVal) > 0 && err != nil { return defaultVal[0] } return val } // MustInt64 always returns value without error, // it returns 0 if error occurs. func (k *Key) MustInt64(defaultVal ...int64) int64 { val, err := k.Int64() if len(defaultVal) > 0 && err != nil { return defaultVal[0] } return val } // MustUint always returns value without error, // it returns 0 if error occurs. func (k *Key) MustUint(defaultVal ...uint) uint { val, err := k.Uint() if len(defaultVal) > 0 && err != nil { return defaultVal[0] } return val } // MustUint64 always returns value without error, // it returns 0 if error occurs. func (k *Key) MustUint64(defaultVal ...uint64) uint64 { val, err := k.Uint64() if len(defaultVal) > 0 && err != nil { return defaultVal[0] } return val } // MustDuration always returns value without error, // it returns zero value if error occurs. func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration { val, err := k.Duration() if len(defaultVal) > 0 && err != nil { return defaultVal[0] } return val } // MustTimeFormat always parses with given format and returns value without error, // it returns zero value if error occurs. func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time { val, err := k.TimeFormat(format) if len(defaultVal) > 0 && err != nil { return defaultVal[0] } return val } // MustTime always parses with RFC3339 format and returns value without error, // it returns zero value if error occurs. func (k *Key) MustTime(defaultVal ...time.Time) time.Time { return k.MustTimeFormat(time.RFC3339, defaultVal...) } // In always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) In(defaultVal string, candidates []string) string { val := k.String() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InFloat64 always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 { val := k.MustFloat64() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InInt always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InInt(defaultVal int, candidates []int) int { val := k.MustInt() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InInt64 always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 { val := k.MustInt64() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InUint always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InUint(defaultVal uint, candidates []uint) uint { val := k.MustUint() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InUint64 always returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 { val := k.MustUint64() for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InTimeFormat always parses with given format and returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time { val := k.MustTimeFormat(format) for _, cand := range candidates { if val == cand { return val } } return defaultVal } // InTime always parses with RFC3339 format and returns value without error, // it returns default value if error occurs or doesn't fit into candidates. func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time { return k.InTimeFormat(time.RFC3339, defaultVal, candidates) } // RangeFloat64 checks if value is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 { val := k.MustFloat64() if val < min || val > max { return defaultVal } return val } // RangeInt checks if value is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeInt(defaultVal, min, max int) int { val := k.MustInt() if val < min || val > max { return defaultVal } return val } // RangeInt64 checks if value is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeInt64(defaultVal, min, max int64) int64 { val := k.MustInt64() if val < min || val > max { return defaultVal } return val } // RangeTimeFormat checks if value with given format is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time { val := k.MustTimeFormat(format) if val.Unix() < min.Unix() || val.Unix() > max.Unix() { return defaultVal } return val } // RangeTime checks if value with RFC3339 format is in given range inclusively, // and returns default value if it's not. func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time { return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max) } // Strings returns list of string devide by given delimiter. func (k *Key) Strings(delim string) []string { str := k.String() if len(str) == 0 { return []string{} } vals := strings.Split(str, delim) for i := range vals { vals[i] = strings.TrimSpace(vals[i]) } return vals } // Float64s returns list of float64 devide by given delimiter. func (k *Key) Float64s(delim string) []float64 { strs := k.Strings(delim) vals := make([]float64, len(strs)) for i := range strs { vals[i], _ = strconv.ParseFloat(strs[i], 64) } return vals } // Ints returns list of int devide by given delimiter. func (k *Key) Ints(delim string) []int { strs := k.Strings(delim) vals := make([]int, len(strs)) for i := range strs { vals[i], _ = strconv.Atoi(strs[i]) } return vals } // Int64s returns list of int64 devide by given delimiter. func (k *Key) Int64s(delim string) []int64 { strs := k.Strings(delim) vals := make([]int64, len(strs)) for i := range strs { vals[i], _ = strconv.ParseInt(strs[i], 10, 64) } return vals } // Uints returns list of uint devide by given delimiter. func (k *Key) Uints(delim string) []uint { strs := k.Strings(delim) vals := make([]uint, len(strs)) for i := range strs { u, _ := strconv.ParseUint(strs[i], 10, 64) vals[i] = uint(u) } return vals } // Uint64s returns list of uint64 devide by given delimiter. func (k *Key) Uint64s(delim string) []uint64 { strs := k.Strings(delim) vals := make([]uint64, len(strs)) for i := range strs { vals[i], _ = strconv.ParseUint(strs[i], 10, 64) } return vals } // TimesFormat parses with given format and returns list of time.Time devide by given delimiter. func (k *Key) TimesFormat(format, delim string) []time.Time { strs := k.Strings(delim) vals := make([]time.Time, len(strs)) for i := range strs { vals[i], _ = time.Parse(format, strs[i]) } return vals } // Times parses with RFC3339 format and returns list of time.Time devide by given delimiter. func (k *Key) Times(delim string) []time.Time { return k.TimesFormat(time.RFC3339, delim) } // SetValue changes key value. func (k *Key) SetValue(v string) { 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() } key := s.keys[name] if s.f.BlockMode { s.f.lock.RUnlock() } if key == nil { // Check if it is a child-section. sname := s.name for { if i := strings.LastIndex(sname, "."); i > -1 { sname = sname[:i] sec, err := s.f.GetSection(sname) if err != nil { continue } return sec.GetKey(name) } else { break } } return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name) } return key, nil } // Key assumes named Key exists in section and returns a zero-value when not. func (s *Section) Key(name string) *Key { key, err := s.GetKey(name) if err != nil { // It's OK here because the only possible error is empty key name, // but if it's empty, this piece of code won't be executed. key, _ = s.NewKey(name, "") return key } return key } // Keys returns list of keys of section. func (s *Section) Keys() []*Key { keys := make([]*Key, len(s.keyList)) for i := range s.keyList { keys[i] = s.Key(s.keyList[i]) } return keys } // KeyStrings returns list of key names of section. func (s *Section) KeyStrings() []string { list := make([]string, len(s.keyList)) copy(list, s.keyList) return list } // KeysHash returns keys hash consisting of names and values. func (s *Section) KeysHash() map[string]string { if s.f.BlockMode { s.f.lock.RLock() defer s.f.lock.RUnlock() } hash := map[string]string{} for key, value := range s.keysHash { hash[key] = value } return hash } // DeleteKey deletes a key from section. func (s *Section) DeleteKey(name string) { if s.f.BlockMode { s.f.lock.Lock() defer s.f.lock.Unlock() } for i, k := range s.keyList { if k == name { s.keyList = append(s.keyList[:i], s.keyList[i+1:]...) delete(s.keys, name) return } } } // ___________.__.__ // \_ _____/|__| | ____ // | __) | | | _/ __ \ // | \ | | |_\ ___/ // \___ / |__|____/\___ > // \/ \/ // File represents a combination of a or more INI file(s) in memory. type File struct { // Should make things safe, but sometimes doesn't matter. BlockMode bool // Make sure data is safe in multiple goroutines. lock sync.RWMutex // Allow combination of multiple data sources. dataSources []dataSource // Actual data is stored here. sections map[string]*Section // To keep data in order. sectionList []string NameMapper } // newFile initializes File object with given data sources. func newFile(dataSources []dataSource) *File { return &File{ BlockMode: true, dataSources: dataSources, sections: make(map[string]*Section), sectionList: make([]string, 0, 10), } } func parseDataSource(source interface{}) (dataSource, error) { switch s := source.(type) { case string: return sourceFile{s}, nil case []byte: return &sourceData{s}, nil default: return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s) } } // Load loads and parses from INI data sources. // Arguments can be mixed of file name with string type, or raw data in []byte. func Load(source interface{}, others ...interface{}) (_ *File, err error) { sources := make([]dataSource, len(others)+1) sources[0], err = parseDataSource(source) if err != nil { return nil, err } for i := range others { sources[i+1], err = parseDataSource(others[i]) if err != nil { return nil, err } } f := newFile(sources) return f, f.Reload() } // Empty returns an empty file object. func Empty() *File { // Ignore error here, we sure our data is good. f, _ := Load([]byte("")) return f } // NewSection creates a new section. func (f *File) NewSection(name string) (*Section, error) { if len(name) == 0 { return nil, errors.New("error creating new section: empty section name") } if f.BlockMode { f.lock.Lock() defer f.lock.Unlock() } if inSlice(name, f.sectionList) { return f.sections[name], nil } f.sectionList = append(f.sectionList, name) f.sections[name] = newSection(f, name) return f.sections[name], nil } // NewSections creates a list of sections. func (f *File) NewSections(names ...string) (err error) { for _, name := range names { if _, err = f.NewSection(name); err != nil { return err } } return nil } // GetSection returns section by given name. func (f *File) GetSection(name string) (*Section, error) { if len(name) == 0 { name = DEFAULT_SECTION } if f.BlockMode { f.lock.RLock() defer f.lock.RUnlock() } sec := f.sections[name] if sec == nil { return nil, fmt.Errorf("error when getting section: section '%s' not exists", name) } return sec, nil } // Section assumes named section exists and returns a zero-value when not. func (f *File) Section(name string) *Section { sec, err := f.GetSection(name) if err != nil { // Note: It's OK here because the only possible error is empty section name, // but if it's empty, this piece of code won't be executed. sec, _ = f.NewSection(name) return sec } return sec } // Section returns list of Section. func (f *File) Sections() []*Section { sections := make([]*Section, len(f.sectionList)) for i := range f.sectionList { sections[i] = f.Section(f.sectionList[i]) } return sections } // SectionStrings returns list of section names. func (f *File) SectionStrings() []string { list := make([]string, len(f.sectionList)) copy(list, f.sectionList) return list } // DeleteSection deletes a section. func (f *File) DeleteSection(name string) { if f.BlockMode { f.lock.Lock() defer f.lock.Unlock() } if len(name) == 0 { name = DEFAULT_SECTION } for i, s := range f.sectionList { if s == name { f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...) delete(f.sections, name) return } } } func cutComment(str string) string { i := strings.Index(str, "#") if i == -1 { return str } return str[:i] } func checkMultipleLines(buf *bufio.Reader, line, val, valQuote string) (string, error) { isEnd := false 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) } } return val, nil } func checkContinuationLines(buf *bufio.Reader, val string) (string, bool, error) { isEnd := false for { valLen := len(val) if valLen == 0 || val[valLen-1] != '\\' { break } val = val[:valLen-1] next, err := buf.ReadString('\n') if err != nil { if err != io.EOF { return "", isEnd, err } isEnd = true } next = strings.TrimSpace(next) if len(next) == 0 { break } val += next } return val, isEnd, nil } // 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) if err != nil { return err } 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. section, err = f.NewSection(strings.TrimSpace(line[1 : length-1])) 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 firstChar == `"` { if lineRightLength >= 3 && lineRight[0:3] == `"""` { valQuote = `"""` } else { valQuote = `"` } } else if firstChar == `'` { valQuote = `'` } if len(valQuote) > 0 { qLen := len(valQuote) pos := strings.LastIndex(lineRight[qLen:], valQuote) // For multiple-line value check. if pos == -1 { if valQuote == `"` || valQuote == `'` { return fmt.Errorf("error parsing line: single quote does not allow multiple-line value: %s", line) } val = lineRight[qLen:] + "\n" val, err = checkMultipleLines(buf, line, val, valQuote) if err != nil { return err } } else { val = lineRight[qLen : pos+qLen] } } else { val = strings.TrimSpace(cutComment(lineRight)) val, isEnd, err = checkContinuationLines(buf, val) if err != nil { return err } } 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 } func (f *File) reload(s dataSource) error { r, err := s.ReadCloser() if err != nil { return err } defer r.Close() return f.parse(r) } // Reload reloads and parses all data sources. func (f *File) Reload() (err error) { for _, s := range f.dataSources { if err = f.reload(s); err != nil { return err } } return nil } // Append appends one or more data sources and reloads automatically. func (f *File) Append(source interface{}, others ...interface{}) error { ds, err := parseDataSource(source) if err != nil { return err } f.dataSources = append(f.dataSources, ds) for _, s := range others { ds, err = parseDataSource(s) if err != nil { return err } f.dataSources = append(f.dataSources, ds) } return f.Reload() } // WriteToIndent writes file content into io.Writer with given value indention. func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { equalSign := "=" if PrettyFormat { equalSign = " = " } // Use buffer to make sure target is safe until finish encoding. buf := bytes.NewBuffer(nil) for i, sname := range f.sectionList { sec := f.Section(sname) if len(sec.Comment) > 0 { if sec.Comment[0] != '#' && sec.Comment[0] != ';' { sec.Comment = "; " + sec.Comment } if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil { return 0, err } } if i > 0 { if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil { return 0, err } } else { // Write nothing if default section is empty. if len(sec.keyList) == 0 { continue } } for _, kname := range sec.keyList { key := sec.Key(kname) if len(key.Comment) > 0 { if len(indent) > 0 && sname != DEFAULT_SECTION { buf.WriteString(indent) } if key.Comment[0] != '#' && key.Comment[0] != ';' { key.Comment = "; " + key.Comment } if _, err = buf.WriteString(key.Comment + LineBreak); err != nil { return 0, err } } if len(indent) > 0 && sname != DEFAULT_SECTION { buf.WriteString(indent) } switch { case key.isAutoIncr: kname = "-" case strings.Contains(kname, "`") || strings.Contains(kname, `"`): kname = `"""` + kname + `"""` case strings.Contains(kname, `=`) || strings.Contains(kname, `:`): kname = "`" + kname + "`" } val := key.value // In case key value contains "\n", "`" or "\"". if strings.Contains(val, "\n") || strings.Contains(val, "`") || strings.Contains(val, `"`) || strings.Contains(val, "#") { val = `"""` + val + `"""` } if _, err = buf.WriteString(kname + equalSign + val + LineBreak); err != nil { return 0, err } } // Put a line between sections. if _, err = buf.WriteString(LineBreak); err != nil { return 0, err } } return buf.WriteTo(w) } // WriteTo writes file content into io.Writer. func (f *File) WriteTo(w io.Writer) (int64, error) { return f.WriteToIndent(w, "") } // SaveToIndent writes content to file system with given value indention. func (f *File) SaveToIndent(filename, indent string) error { // Note: Because we are truncating with os.Create, // so it's safer to save to a temporary file location and rename afte done. tmpPath := filename + "." + strconv.Itoa(time.Now().Nanosecond()) + ".tmp" defer os.Remove(tmpPath) fw, err := os.Create(tmpPath) if err != nil { return err } if _, err = f.WriteToIndent(fw, indent); err != nil { fw.Close() return err } fw.Close() // Remove old file and rename the new one. os.Remove(filename) return os.Rename(tmpPath, filename) } // SaveTo writes content to file system. func (f *File) SaveTo(filename string) error { return f.SaveToIndent(filename, "") }