package config import "io" import "fmt" import "math" import "bufio" import "errors" import "strconv" import "strings" import "unicode" type line any type comment string type entry struct { key string; value Value } var _ io.WriterTo = new(File) // File represents a config file. It preserves the order of the lines, as well // as blank lines and comments. type File struct { lines []line keys map[string] int } // NewFile creates a blank file with nothing in it. func NewFile () *File { return &File { keys: make(map[string] int), } } // Parse parses a config file from a reader. This function operates on a // best-effort basis: A file will always be returned, and any errors encountered // will be joined together. For a description of the format, see the README.md // of this package. func Parse (reader io.Reader) (*File, error) { file := &File { keys: make(map[string] int), } errs := []error { } scanner := bufio.NewScanner(reader) for scanner.Scan() { text := strings.TrimSpace(scanner.Text()) switch { case text == "", strings.HasPrefix(text, "#"): file.lines = append(file.lines, comment(text)) default: entry, err := parseEntry(text) if err == nil { file.keys[entry.key] = len(file.lines) file.lines = append(file.lines, entry) } else { errs = append (errs, err ) } } } errs = append(errs, scanner.Err()) return file, errors.Join(errs...) } func parseEntry (str string) (entry, error) { key, value, ok := strings.Cut(str, "=") if !ok { return entry { }, ErrMalformedEntry } key = strings.TrimSpace(key) if !KeyValid(key) { return entry { }, ErrMalformedKey } value = strings.TrimSpace(value) parsedValue, err := ParseValue(value) if err != nil { return entry { }, err } return entry { key: key, value: parsedValue }, nil } // ParseValue parses a value from a string. For any Value v, // ParseValue(v.String()) should hold data exactly equal to v. This function // does not trim whitespace. func ParseValue (str string) (Value, error) { first := firstRune(str) switch { case str == "": return ValueString(""), nil case first == '"': value, ok := unescape(str) if !ok { return nil, ErrMalformedEscapeSequence } value, ok = unquote(value) if !ok { return nil, ErrMalformedStringValue } return ValueString(value), nil case first == '-' || (first >= '0' && first <= '9'): value, err := strconv.ParseFloat(str, 64) if err != nil { return nil, ErrMalformedNumberValue } return ValueNumber(value), nil case str == "false": return ValueBool(false), nil case str == "true": return ValueBool(true), nil case str == "Inf": return ValueNumber(math.Inf(1)), nil case str == "NaN": return ValueNumber(math.NaN()), nil default: value, ok := unescape(str) if !ok { return nil, ErrMalformedEscapeSequence } return ValueString(value), nil } } // Has returns whether the key exists. If the key is invalid, it returns false, // ErrMalformedKey. func (this *File) Has (key string) (bool, error) { if !KeyValid(key) { return false, ErrMalformedKey } if index, ok := this.keys[key]; ok { if _, ok := this.lines[index].(entry); ok { return true, nil } } return false, nil } // Get gets the keyed value. If the value is unspecified, it returns nil, // ErrNonexistentEntry. If the key is invalid, it returns nil, ErrMalformedKey. func (this *File) Get (key string) (Value, error) { if !KeyValid(key) { return nil, ErrMalformedKey } if index, ok := this.keys[key]; ok { if lin, ok := this.lines[index].(entry); ok { return lin.value, nil } } return nil, ErrNonexistentEntry } // Set sets a value. If the key is invalid, it returns ErrMalformedKey. func (this *File) Set (key string, value Value) error { if !KeyValid(key) { return ErrMalformedKey } ent := entry { key: key, value: value, } if index, ok := this.keys[key]; ok { this.lines[index] = ent return nil } this.keys[key] = len(this.lines) this.lines = append(this.lines, ent) return nil } // Reset removes the value from the file. If the value is set again, it will be // added back at the same location. Note that because of this, the positions of // lines are not forgotten until the file is written and reloaded. This is why // the method is called Reset and not Remove. If the key is invalid, it returns // ErrMalformedKey. func (this *File) Reset (key string) error { if !KeyValid(key) { return ErrMalformedKey } for index, lin := range this.lines { if lin, ok := lin.(entry); ok { if lin.key == key { this.lines[index] = nil } } } return nil } // Map creates and returns a map of keys to values. func (this *File) Map () map[string] Value { mp := make(map[string] Value) for key, index := range this.keys { if lin, ok := this.lines[index].(entry); ok { mp[key] = lin.value } } return mp } // Diff returns a set of keys that are different from the other file. func (this *File) Diff (other *File) map[string] struct { } { diff := make(map[string] struct { }) // - keys only we have // - keys we both have, but are different for key, index := range this.keys { thisEntry, ok := this.lines[index].(entry) if !ok { continue } otherIndex, ok := other.keys[key] if !ok { diff[key] = struct { } { } continue } otherEntry, ok := other.lines[otherIndex].(entry) if !ok { diff[key] = struct { } { } continue } if !thisEntry.value.Equals(otherEntry.value) { diff[key] = struct { } { } } } // - keys only they have for key := range other.keys { if otherHas, _ := other.Has(key); !otherHas { continue } if thisHas, _ := this.Has(key); !thisHas { diff[key] = struct { } { } } } return diff } // WriteTo writes the data in this file to an io.Writer. func (this *File) WriteTo (writer io.Writer) (n int64, err error) { for _, lin := range this.lines { nint := 0 switch lin := lin.(type) { case comment: nint, err = fmt.Fprintln(writer, string(lin)) case entry: nint, err = fmt.Fprintf(writer, "%s=%v\n", lin.key, lin.value) } n += int64(nint) if err != nil { return n, err } } return n, nil } // KeyValid returns whether a key contains only valid runes. They are: // - Letters // - Digits // - '-' // - '.' func KeyValid (key string) bool { for _, char := range key { valid := char == '.' || char == '-' || unicode.IsLetter(char) || unicode.IsDigit(char) if !valid { return false } } return true }