// 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 }