211 lines
4.4 KiB
Go
211 lines
4.4 KiB
Go
![]() |
// module conf
|
||
|
//
|
||
|
// import "code.uint32.ru/tiny/conf"
|
||
|
//
|
||
|
// Module conf implements a very simple config parser with two types of
|
||
|
// values: key value pairs and single word options. Each of these must be
|
||
|
// put on separate line in a file like this:
|
||
|
//
|
||
|
// key1 = value
|
||
|
// key2 = value1, value2, value3
|
||
|
// key3=v1,v2,v3
|
||
|
// option1
|
||
|
// option2
|
||
|
//
|
||
|
// Values can also be read from any io.Reader or io.ReadCloser. Note that
|
||
|
// values in key value pairs mey either be separated with spaces or not.
|
||
|
// Typical use case would look like this:
|
||
|
//
|
||
|
// config, err := conf.ParseFile("filename")
|
||
|
// if err != nil {
|
||
|
// // Means we failed to read from file
|
||
|
// // config variable is now nil and unusable
|
||
|
// }
|
||
|
// value, err := config.Find("mykey")
|
||
|
// if err != nil {
|
||
|
// // Means that key has not been found
|
||
|
// }
|
||
|
// // value now holds conf.Setting.
|
||
|
// n, err := value.Float64() // tries to parse float from Setting.Value field.
|
||
|
// //if err is nil then n holds float64.
|
||
|
//
|
||
|
// There is also a quicker Get() method which returns no errors
|
||
|
// ("i'm feeling lucky" way to lookup values). If it does not find
|
||
|
// requested key, the returned Setting has empty string in Value field.
|
||
|
//
|
||
|
// value2 := config.Get("otherkey")
|
||
|
// mybool, err := value2.Bool() // tries to interpret Setting.Value field as bool
|
||
|
// // mybool now holds boolean if "otherkey" was found and error returned
|
||
|
// // by Bool() method is nil.
|
||
|
//
|
||
|
// Even shorter syntax would be:
|
||
|
//
|
||
|
// listnumbers, err := config.Get("numbers").IntSlice()
|
||
|
// // Note that we'd still like to check for errors even if
|
||
|
// // we're sure the key exists to make sure all values are converted.
|
||
|
// // listnumbers holds slice of ints. If err is nil all of found values
|
||
|
// // have been converted successfully.
|
||
|
//
|
||
|
// To check whether single-word options were found use:
|
||
|
//
|
||
|
// if config.HasOption("wordoption") {
|
||
|
// // do something
|
||
|
// }
|
||
|
//
|
||
|
// See description of module's other methods which are
|
||
|
// quite self-explanatory.
|
||
|
package conf
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
ErrFormat = errors.New("conf: line does not match \"key = value\" pattern")
|
||
|
ErrDuplicateKey = errors.New("conf: duplicate key found")
|
||
|
)
|
||
|
|
||
|
// Conf holds key value pairs.
|
||
|
type Conf struct {
|
||
|
values []*kv // values store key value pairs ("key = value" in config file)
|
||
|
}
|
||
|
|
||
|
type kv struct {
|
||
|
k string
|
||
|
v Value
|
||
|
}
|
||
|
|
||
|
// Open reads values from file.
|
||
|
func Open(filename string) (*Conf, error) {
|
||
|
c := new(Conf)
|
||
|
if err := c.readFile(filename); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return c, nil
|
||
|
}
|
||
|
|
||
|
// Read reads from r and returns Conf.
|
||
|
func Read(r io.Reader) (*Conf, error) {
|
||
|
c := new(Conf)
|
||
|
if err := c.read(r); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return c, nil
|
||
|
}
|
||
|
|
||
|
// Find looks up a the key and returns Value associated with it.
|
||
|
// If bool is false then the returned Value would be zero.
|
||
|
func (c *Conf) Find(key string) (Value, bool) {
|
||
|
return c.get(key, "")
|
||
|
}
|
||
|
|
||
|
// Get returns a Value. If key was not found the returned Value
|
||
|
// will be empty.
|
||
|
func (c *Conf) Get(key string) Value {
|
||
|
v, _ := c.get(key, "")
|
||
|
return v
|
||
|
}
|
||
|
|
||
|
// GetDefault looks up a Value for requested key.
|
||
|
// If lookup fails it returns Value set to def.
|
||
|
func (c *Conf) GetDefault(key, def string) Value {
|
||
|
v, _ := c.get(key, def)
|
||
|
return v
|
||
|
}
|
||
|
|
||
|
func (c *Conf) Keys() []string {
|
||
|
list := make([]string, 0, len(c.values))
|
||
|
|
||
|
for i := range c.values {
|
||
|
list = append(list, c.values[i].k)
|
||
|
}
|
||
|
|
||
|
return list
|
||
|
}
|
||
|
|
||
|
func (c *Conf) Read(r io.Reader) error {
|
||
|
return c.read(r)
|
||
|
}
|
||
|
|
||
|
func (c *Conf) ReadFile(name string) error {
|
||
|
return c.readFile(name)
|
||
|
}
|
||
|
|
||
|
func (c *Conf) get(key string, def string) (Value, bool) {
|
||
|
var (
|
||
|
v Value
|
||
|
found bool
|
||
|
)
|
||
|
for i := range c.values {
|
||
|
if c.values[i].k == key {
|
||
|
v = c.values[i].v
|
||
|
found = true
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if def != "" && !found {
|
||
|
v = Value(def)
|
||
|
}
|
||
|
|
||
|
return v, found
|
||
|
}
|
||
|
|
||
|
func (c *Conf) readFile(name string) error {
|
||
|
file, err := os.Open(name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if err := c.read(file); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return file.Close()
|
||
|
}
|
||
|
|
||
|
func (c *Conf) read(r io.Reader) error {
|
||
|
uniq := make(map[string]struct{})
|
||
|
|
||
|
scanner := bufio.NewScanner(r)
|
||
|
|
||
|
for scanner.Scan() {
|
||
|
line := scanner.Text()
|
||
|
|
||
|
line = trim(stripComment(line))
|
||
|
if line == "" {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
key, value, ok := toKV(line, separatorKV)
|
||
|
if !ok {
|
||
|
return ErrFormat
|
||
|
}
|
||
|
|
||
|
if _, ok := uniq[key]; ok {
|
||
|
return fmt.Errorf("%w: %s", ErrDuplicateKey, key)
|
||
|
}
|
||
|
|
||
|
uniq[key] = struct{}{}
|
||
|
|
||
|
kv := &kv{
|
||
|
k: key,
|
||
|
v: Value(value),
|
||
|
}
|
||
|
|
||
|
c.values = append(c.values, kv)
|
||
|
}
|
||
|
|
||
|
if err := scanner.Err(); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|