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,
}
}