package camfish import "os" import "io" import "fmt" import "iter" import "slices" import "strings" import "strconv" import "path/filepath" type iniConfig map[string] []iniValue type iniValue struct { value string file string line int column int } // ParseINI parses a string containing INI configuration data. func ParseINI(filename, input string) (MutableConfig, error) { ini := make(iniConfig) configErr := ConfigError { File: filename, } section := "" for index, line := range strings.Split(input, "\n") { configErr.Line = index + 1 configErr.Key = "" line = strings.TrimSpace(line) if line == "" { continue } if strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, "[") { // section heading if !strings.HasSuffix(line, "]") { configErr.Err = ErrSectionHeadingMalformed return nil, configErr } section = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "["), "]")) if section == "" { configErr.Err = ErrSectionHeadingMalformed return nil, configErr } continue } // split key/value key, value, ok := strings.Cut(line, "=") if !ok { configErr.Err = ErrPairMalformed return nil, configErr } // key key = strings.TrimSpace(key) if key == "" { configErr.Err = ErrKeyEmpty return nil, configErr } configErr.Key = key if section != "" { key = fmt.Sprintf("%s.%s", section, key) } // value value = strings.TrimSpace(value) if strings.HasPrefix(value, "\"") || strings.HasPrefix(value, "'") { unquoted, err := strconv.Unquote(value) if err != nil { configErr.Column = strings.Index(line, "=") + 2 configErr.Err = err return nil, configErr } value = unquoted } ini.Add(key, value) } return ini, nil } // DecodeINI decodes INI data from an io.Reader. The entire reader is consumed. func DecodeINI(filename string, input io.Reader) (MutableConfig, error) { buffer, err := io.ReadAll(input) if err != nil { return nil, err } return ParseINI(filename, string(buffer)) } func (ini iniConfig) Add(key, value string) { key = canonicalINIKey(key) ini[key] = append(ini[key], iniValue { value: value, }) } func (ini iniConfig) Del(key string) { key = canonicalINIKey(key) delete(ini, key) } func (ini iniConfig) Get(key string) string { key = canonicalINIKey(key) slice, ok := ini[key] if !ok { return "" } if len(slice) == 0 { return "" } return slice[0].value } func (ini iniConfig) GetAll(key string) iter.Seq2[int, string] { return func(yield func(int, string) bool) { for index, value := range ini[key] { if !yield(index, value.value) { return } } } } func (ini iniConfig) Set(key, value string) { key = canonicalINIKey(key) valueInfo := iniValue { } if prevValues := ini[key]; len(prevValues) > 0 { valueInfo = prevValues[0] } valueInfo.value = value ini[key] = []iniValue { valueInfo } } func (ini iniConfig) NewConfigError(key string, index int, wrapped error) ConfigError { if values, ok := ini[key]; ok { if index > 0 && index < len(values) { value := values[index] return ConfigError { File: value.file, Key: key, Line: value.line, Column: value.column, Err: wrapped, } } } return ConfigError { Key: key, Err: wrapped, } } func canonicalINIKey(key string) string { return strings.ToLower(key) } func mergeINI(inis ...iniConfig) iniConfig { ini := make(iniConfig) for index := len(inis) - 1; index >= 0; index -- { for key, values := range inis[index] { if _, exists := ini[key]; exists { continue } ini[key] = slices.Clone(values) } } return ini } func configFiles(program string) ([]string, error) { userConfig, err := os.UserConfigDir() if err != nil { return nil, err } return []string { filepath.Join("/etc", program), filepath.Join(userConfig, program), }, nil }