camfish/ini.go
2024-12-31 01:27:53 -05:00

169 lines
3.8 KiB
Go

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
}