169 lines
3.8 KiB
Go
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
|
|
}
|