package conf import ( "bufio" "errors" "fmt" "io" "os" "strings" ) const ( strComment = "#" strEqSign = "=" ) var ( ErrFormat = errors.New("conf: line does not match \"key = value\" pattern") ErrDuplicateKey = errors.New("conf: duplicate key found") ) // Open reads values from file. It returns nil and error if os.Open(filename) fails. // It would be wise to always check returned error. // ParseFile captures two types of values: "key = value" and "option". Either key value pair or // option must be put in its own line in the file. // Key or option must be a single word. In example line: // // "option1 option2" // // only option1 will be captured. option2 needs to be in a separate line in the file to take effect. // In line "key = value1,value2,value3" all of value1, value2, and value3 will be // captured. They can be later accesed separately with Setting's Split() method. func Open(filename string) (*Conf, error) { file, err := os.Open(filename) if err != nil { return nil, err } c, err := parseReader(file) if err != nil { return nil, err } return c, file.Close() } // Read reads from r and returns Config. func Read(r io.Reader) (*Conf, error) { return parseReader(r) } func parseReader(r io.Reader) (*Conf, error) { settings := make(map[string]string) scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() line = trim(stripComment(line)) if line == "" { continue } key, value, ok := toKV(line) if !ok { return nil, ErrFormat } if _, ok := settings[key]; ok { return nil, fmt.Errorf("%w: %s", ErrDuplicateKey, key) } settings[key] = value } if err := scanner.Err(); err != nil { return nil, err } return &Conf{ settings: settings, }, nil } func toKV(s string) (string, string, bool) { k, v, ok := strings.Cut(s, strEqSign) if !ok || k == "" || v == "" { return "", "", false } return trim(k), trim(v), true } func trim(s string) string { s = strings.TrimSpace(s) return s } func stripComment(s string) string { idx := strings.Index(s, strComment) if idx > -1 { s = s[:idx] } return s }