184 lines
4.0 KiB
Go
184 lines
4.0 KiB
Go
// conf implements a very simple config parser with permissive input syntax.
|
|
//
|
|
// It parses input file or io.Reader looking for key value pairs separated by equal
|
|
// sign into conf.Map which is a map[string]conf.Value under the hood.
|
|
//
|
|
// Input syntax rules are as follows:
|
|
//
|
|
// 1. Lines starting with "#" are considered to be comments and are discarded. If a line starts with any number of whitespace characters followed by "#" it is also discarded as comment.
|
|
//
|
|
// 2. If a line is not a comment it must follow the "key = value" pattern. Any of the following is valid:
|
|
//
|
|
// key=value
|
|
// key= value
|
|
// key =value
|
|
//
|
|
// Leading and trailing whitespace characters are trimmed before parsing line. Whitespaces surrounding
|
|
// key and value strings are also trimmed.
|
|
//
|
|
// 3. Keys in the input must be unique.
|
|
//
|
|
// Map object has Find, Get and GetDefault mathod used to retrieve Value from
|
|
// Map by corresponding key.
|
|
//
|
|
// Value is essentially a string which can be converted into several types using
|
|
// Value's methods:
|
|
//
|
|
// String() returns Value as string
|
|
// StringSlice() interprets Value as a comma-separated list (whitespace around elements is trimmed)
|
|
// Int() converts Value to int
|
|
// IntSlice() tries to convert Value to []int
|
|
// Float64() converts Value to float64
|
|
// Float64Slice() tries to convert to []float64
|
|
// Map() tries to interpret Value as comma-separated list of key-value pairs like in "key1: value1, key2: value2, key3: value3"
|
|
// URL() calls url.Parse to convert Value to *url.URL
|
|
//
|
|
// See documentation for description of all available methods.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// m, _ := conf.ReadFile("myfile")
|
|
// retries, err := m.Get("retries").Int()
|
|
// if err != nil {
|
|
// // handle err
|
|
// }
|
|
// addr, err := m.Get("addr").URL()
|
|
// if err != nil {
|
|
// // handle err
|
|
// }
|
|
//
|
|
// See example folder.
|
|
package conf
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"slices"
|
|
)
|
|
|
|
var (
|
|
ErrFormat = errors.New("conf: line does not match \"key = value\" pattern")
|
|
ErrDuplicateKey = errors.New("conf: duplicate key found")
|
|
)
|
|
|
|
// Map holds key value pairs.
|
|
type Map map[string]Value
|
|
|
|
// Open reads values from file.
|
|
func Open(filename string) (Map, error) {
|
|
m := make(Map)
|
|
if err := m.readFile(filename); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Read reads from r and returns Conf.
|
|
func Read(r io.Reader) (Map, error) {
|
|
m := make(Map)
|
|
if err := m.read(r); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (m Map) Read(r io.Reader) error {
|
|
return m.read(r)
|
|
}
|
|
|
|
func (m Map) ReadFile(name string) error {
|
|
return m.readFile(name)
|
|
}
|
|
|
|
// Find looks up a the key and returns Value associated with it.
|
|
// If bool is false then the returned Value would be zero.
|
|
func (m Map) Find(key string) (Value, bool) {
|
|
return m.get(key, "")
|
|
}
|
|
|
|
// Get returns a Value. If key was not found the returned Value
|
|
// will be empty.
|
|
func (m Map) Get(key string) Value {
|
|
v, _ := m.get(key, "")
|
|
return v
|
|
}
|
|
|
|
// GetDefault looks up a Value for requested key.
|
|
// If lookup fails it returns Value set to def.
|
|
func (m Map) GetDefault(key, def string) Value {
|
|
v, _ := m.get(key, def)
|
|
return v
|
|
}
|
|
|
|
func (m Map) Keys() []string {
|
|
list := make([]string, 0, len(m))
|
|
|
|
for k := range m {
|
|
list = append(list, k)
|
|
}
|
|
|
|
slices.Sort(list)
|
|
|
|
return list
|
|
}
|
|
|
|
func (m Map) get(key string, def string) (Value, bool) {
|
|
v, ok := m[key]
|
|
if def != "" && !ok {
|
|
v = Value(def)
|
|
}
|
|
|
|
return v, ok
|
|
}
|
|
|
|
func (m Map) readFile(name string) error {
|
|
file, err := os.Open(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := m.read(file); err != nil {
|
|
return err
|
|
}
|
|
|
|
return file.Close()
|
|
}
|
|
|
|
func (m Map) read(r io.Reader) error {
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
lineN := 0
|
|
for scanner.Scan() {
|
|
lineN++
|
|
|
|
line := scanner.Text()
|
|
|
|
line = stripComment(trim(line))
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
key, value, ok := toKV(line, separatorKV)
|
|
if !ok {
|
|
return fmt.Errorf("line: %d, err: %w", lineN, ErrFormat)
|
|
}
|
|
|
|
if _, ok := m[key]; ok {
|
|
return fmt.Errorf("line: %d, err: %w %s", lineN, ErrDuplicateKey, key)
|
|
}
|
|
|
|
m[key] = Value(value)
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|