// Package ini is a tiny dependency-free reader for the boring INI files lectio // uses for config and calendar data: [sections] (including "a/b" subsections), // "key = value" pairs (keys may be dotted, e.g. name.pl), and "#"/";" comments. // It preserves order and never interprets values beyond trimming. package ini import ( "bufio" "bytes" "fmt" "strings" ) type Pair struct{ Key, Val string } type Section struct { Name string Pairs []Pair } // Parse reads INI bytes into ordered sections. Pairs before the first [section] // go into a leading section with an empty Name. func Parse(data []byte) ([]Section, error) { secs := []Section{{Name: ""}} sc := bufio.NewScanner(bytes.NewReader(data)) sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) line := 0 for sc.Scan() { line++ raw := stripComment(sc.Text()) s := strings.TrimSpace(raw) if s == "" { continue } if strings.HasPrefix(s, "[") { if !strings.HasSuffix(s, "]") { return nil, fmt.Errorf("ini: line %d: unclosed section header %q", line, s) } name := strings.TrimSpace(s[1 : len(s)-1]) secs = append(secs, Section{Name: name}) continue } eq := strings.IndexByte(s, '=') if eq < 0 { return nil, fmt.Errorf("ini: line %d: expected key = value, got %q", line, s) } key := strings.TrimSpace(s[:eq]) val := strings.TrimSpace(s[eq+1:]) cur := &secs[len(secs)-1] cur.Pairs = append(cur.Pairs, Pair{key, val}) } return secs, sc.Err() } // stripComment removes a trailing "#" or ";" comment (our values never contain // an unescaped # or ;). func stripComment(line string) string { for i := 0; i < len(line); i++ { if line[i] == '#' || line[i] == ';' { return line[:i] } } return line } // List splits a comma-separated value, trimming spaces and dropping empties. func List(val string) []string { var out []string for _, p := range strings.Split(val, ",") { if t := strings.TrimSpace(p); t != "" { out = append(out, t) } } return out }