package config import ( "bufio" "errors" "fmt" "io" "slices" ) // Config holds all parsed configuration entries from a Git config file. // // A Config preserves the ordering of entries as they appeared in the source. // // Lookups are matched case-insensitively for section and key names, // and subsections must match exactly. // // Includes aren't supported yet; they will be supported in a later revision. // // Labels: MT-Safe. type Config struct { entries []Entry } // Parse reads and parses Git configuration entries from r. func Parse(r io.Reader) (*Config, error) { parser := &configParser{ reader: bufio.NewReader(r), lineNum: 1, currentSection: "", currentSubsec: "", peeked: 0, hasPeeked: false, } return parser.parse() } // Entry represents a single parsed configuration directive. type Entry struct { // Section is the section name in canonical lowercase form. Section string // Subsection is the subsection name, // retaining the exact form parsed from the input. Subsection string // Key is the key name in canonical lowercase form. Key string // Kind reports whether this entry has no value or an explicit value. Kind Kind // Value is the interpreted value of the configuration entry, // including unescaped characters where appropriate. Value string } // Entries returns a copy of all parsed configuration entries // in the order they appeared. // // Modifying the returned slice does not affect the Config. func (config *Config) Entries() []Entry { return slices.Clone(config.entries) } type configParser struct { reader *bufio.Reader lineNum int currentSection string currentSubsec string peeked byte hasPeeked bool } func (p *configParser) parse() (*Config, error) { cfg := &Config{ entries: nil, } err := p.skipBOM() if err != nil { return nil, err } for { ch, err := p.nextChar() if errors.Is(err, io.EOF) { break } if err != nil { return nil, err } // Skip leading whitespace between entries. if isWhitespace(ch) { continue } // Comments if ch == '#' || ch == ';' { err := p.skipToEOL() if err != nil && !errors.Is(err, io.EOF) { return nil, err } continue } // Section header if ch == '[' { err := p.parseSection() if err != nil { return nil, err } continue } // Key-value pair if isLetter(ch) { p.unreadChar(ch) err := p.parseKeyValue(cfg) if err != nil { return nil, err } continue } return nil, p.parseError(fmt.Sprintf("unexpected character %q", ch)) } return cfg, nil } func (p *configParser) parseError(reason string) error { return &ParseError{ Line: p.lineNum, reason: reason, } }