package goconf import ( "bufio" "errors" "fmt" "io" "os" "reflect" "strconv" "strings" "time" ) const ( // formatter CRLF = '\n' Comment = "#" Spliter = " " SectionS = "[" SectionE = "]" // memory unit Byte = 1 KB = 1024 * Byte MB = 1024 * KB GB = 1024 * MB ) // Section is the key-value data object. type Section struct { data map[string]string // key:value dataOrder []string dataComments map[string][]string // key:comments Name string comments []string Comment string } // Config is the key-value configuration object. type Config struct { data map[string]*Section dataOrder []string file string Comment string Spliter string } // New return a new default Config object (Comment = '#', spliter = ' '). func New() *Config { return &Config{Comment: Comment, Spliter: Spliter, data: map[string]*Section{}} } // ParseReader parse config file by a io.Reader. func (c *Config) ParseReader(reader io.Reader) error { var ( err error line int idx int row string key string value string comments []string section *Section rd = bufio.NewReader(reader) ) for { line++ row, err = rd.ReadString(CRLF) if err == io.EOF && len(row) == 0 { // file end break } else if err != nil && err != io.EOF { return err } row = strings.TrimSpace(row) // ignore blank line // ignore Comment line if len(row) == 0 || strings.HasPrefix(row, c.Comment) { comments = append(comments, row) continue } // get secion if strings.HasPrefix(row, SectionS) { if !strings.HasSuffix(row, SectionE) { return errors.New(fmt.Sprintf("no end section: %s at :%d", SectionE, line)) } sectionStr := row[1 : len(row)-1] // store the section s, ok := c.data[sectionStr] if !ok { s = &Section{data: map[string]string{}, dataComments: map[string][]string{}, comments: comments, Comment: c.Comment, Name: sectionStr} c.data[sectionStr] = s c.dataOrder = append(c.dataOrder, sectionStr) } else { return errors.New(fmt.Sprintf("section: %s already exists at %d", sectionStr, line)) } section = s comments = []string{} continue } // get the spliter index idx = strings.Index(row, c.Spliter) if idx > 0 { // get the key and value key = strings.TrimSpace(row[:idx]) if len(row) > idx { value = strings.TrimSpace(row[idx+1:]) } } else { return errors.New(fmt.Sprintf("no spliter in key: %s at %d", row, line)) } // check section exists if section == nil { return errors.New(fmt.Sprintf("no section for key: %s at %d", key, line)) } // check key already exists if _, ok := section.data[key]; ok { return errors.New(fmt.Sprintf("section: %s already has key: %s at %d", section.Name, key, line)) } // save key-value section.data[key] = value // save comments for key section.dataComments[key] = comments section.dataOrder = append(section.dataOrder, key) // clean comments comments = []string{} } return nil } // Parse parse the specified config file. func (c *Config) Parse(file string) error { // open config file if f, err := os.Open(file); err != nil { return err } else { defer f.Close() c.file = file return c.ParseReader(f) } } // Get get a config section by key. func (c *Config) Get(section string) *Section { s, _ := c.data[section] return s } // Add add a new config section, if exist the section key then return the existing one. func (c *Config) Add(section string, comments ...string) *Section { s, ok := c.data[section] if !ok { var dataComments []string for _, comment := range comments { for _, line := range strings.Split(comment, string(CRLF)) { dataComments = append(dataComments, fmt.Sprintf("%s%s", c.Comment, line)) } } s = &Section{data: map[string]string{}, Name: section, comments: dataComments, Comment: c.Comment, dataComments: map[string][]string{}} c.data[section] = s c.dataOrder = append(c.dataOrder, section) } return s } // Remove remove the specified section. func (c *Config) Remove(section string) { if _, ok := c.data[section]; ok { for i, k := range c.dataOrder { if k == section { c.dataOrder = append(c.dataOrder[:i], c.dataOrder[i+1:]...) break } } delete(c.data, section) } } // Sections return all the config sections. func (c *Config) Sections() []string { // safe-copy sections := []string{} for _, k := range c.dataOrder { sections = append(sections, k) } return sections } // Save save current configuration to specified file, if file is "" then rewrite the original file. func (c *Config) Save(file string) error { if file == "" { file = c.file } else { c.file = file } // save core file return c.saveFile(file) } // saveFile save config info in specified file. func (c *Config) saveFile(file string) error { f, err := os.OpenFile(file, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) if err != nil { return err } defer f.Close() // sections for _, section := range c.dataOrder { data, _ := c.data[section] // comments for _, comment := range data.comments { if _, err := f.WriteString(fmt.Sprintf("%s%c", comment, CRLF)); err != nil { return err } } // section if _, err := f.WriteString(fmt.Sprintf("[%s]%c", section, CRLF)); err != nil { return err } // key-values for _, k := range data.dataOrder { v, _ := data.data[k] // comments for _, comment := range data.dataComments[k] { if _, err := f.WriteString(fmt.Sprintf("%s%c", comment, CRLF)); err != nil { return err } } // key-value if _, err := f.WriteString(fmt.Sprintf("%s%s%s%c", k, c.Spliter, v, CRLF)); err != nil { return err } } } return nil } // Reload reload the config file and return a new Config. func (c *Config) Reload() (*Config, error) { nc := &Config{Comment: c.Comment, Spliter: c.Spliter, file: c.file, data: map[string]*Section{}} if err := nc.Parse(c.file); err != nil { return nil, err } return nc, nil } // Add add a new key-value configuration for the section. func (s *Section) Add(k, v string, comments ...string) { if _, ok := s.data[k]; !ok { s.dataOrder = append(s.dataOrder, k) for _, comment := range comments { for _, line := range strings.Split(comment, string(CRLF)) { s.dataComments[k] = append(s.dataComments[k], fmt.Sprintf("%s%s", s.Comment, line)) } } } s.data[k] = v } // Remove remove the specified key configuration for the section. func (s *Section) Remove(k string) { delete(s.data, k) for i, key := range s.dataOrder { if key == k { s.dataOrder = append(s.dataOrder[:i], s.dataOrder[i+1:]...) break } } } // An NoKeyError describes a goconf key that was not found in the section. type NoKeyError struct { Key string Section string } func (e *NoKeyError) Error() string { return fmt.Sprintf("key: \"%s\" not found in [%s]", e.Key, e.Section) } // String get config string value. func (s *Section) String(key string) (string, error) { if v, ok := s.data[key]; ok { return v, nil } else { return "", &NoKeyError{Key: key, Section: s.Name} } } // Strings get config []string value. func (s *Section) Strings(key, delim string) ([]string, error) { if v, ok := s.data[key]; ok { return strings.Split(v, delim), nil } else { return nil, &NoKeyError{Key: key, Section: s.Name} } } // Int get config int value. func (s *Section) Int(key string) (int64, error) { if v, ok := s.data[key]; ok { return strconv.ParseInt(v, 10, 64) } else { return 0, &NoKeyError{Key: key, Section: s.Name} } } // Uint get config uint value. func (s *Section) Uint(key string) (uint64, error) { if v, ok := s.data[key]; ok { return strconv.ParseUint(v, 10, 64) } else { return 0, &NoKeyError{Key: key, Section: s.Name} } } // Float get config float value. func (s *Section) Float(key string) (float64, error) { if v, ok := s.data[key]; ok { return strconv.ParseFloat(v, 64) } else { return 0, &NoKeyError{Key: key, Section: s.Name} } } // Bool get config boolean value. // // "yes", "1", "y", "true", "enable" means true. // // "no", "0", "n", "false", "disable" means false. // // if the specified value unknown then return false. func (s *Section) Bool(key string) (bool, error) { if v, ok := s.data[key]; ok { v = strings.ToLower(v) return parseBool(v), nil } else { return false, &NoKeyError{Key: key, Section: s.Name} } } func parseBool(v string) bool { if v == "true" || v == "yes" || v == "1" || v == "y" || v == "enable" { return true } else if v == "false" || v == "no" || v == "0" || v == "n" || v == "disable" { return false } else { return false } } // Byte get config byte number value. // // 1kb = 1k = 1024. // // 1mb = 1m = 1024 * 1024. // // 1gb = 1g = 1024 * 1024 * 1024. func (s *Section) MemSize(key string) (int, error) { if v, ok := s.data[key]; ok { return parseMemory(v) } else { return 0, &NoKeyError{Key: key, Section: s.Name} } } func parseMemory(v string) (int, error) { unit := Byte subIdx := len(v) if strings.HasSuffix(v, "k") { unit = KB subIdx = subIdx - 1 } else if strings.HasSuffix(v, "kb") { unit = KB subIdx = subIdx - 2 } else if strings.HasSuffix(v, "m") { unit = MB subIdx = subIdx - 1 } else if strings.HasSuffix(v, "mb") { unit = MB subIdx = subIdx - 2 } else if strings.HasSuffix(v, "g") { unit = GB subIdx = subIdx - 1 } else if strings.HasSuffix(v, "gb") { unit = GB subIdx = subIdx - 2 } b, err := strconv.ParseInt(v[:subIdx], 10, 64) if err != nil { return 0, err } return int(b) * unit, nil } // Duration get config second value. // // 1s = 1sec = 1. // // 1m = 1min = 60. // // 1h = 1hour = 60 * 60. func (s *Section) Duration(key string) (time.Duration, error) { if v, ok := s.data[key]; ok { if t, err := parseTime(v); err != nil { return 0, err } else { return time.Duration(t), nil } } else { return 0, &NoKeyError{Key: key, Section: s.Name} } } func parseTime(v string) (int64, error) { unit := int64(time.Nanosecond) subIdx := len(v) if strings.HasSuffix(v, "ms") { unit = int64(time.Millisecond) subIdx = subIdx - 2 } else if strings.HasSuffix(v, "s") { unit = int64(time.Second) subIdx = subIdx - 1 } else if strings.HasSuffix(v, "sec") { unit = int64(time.Second) subIdx = subIdx - 3 } else if strings.HasSuffix(v, "m") { unit = int64(time.Minute) subIdx = subIdx - 1 } else if strings.HasSuffix(v, "min") { unit = int64(time.Minute) subIdx = subIdx - 3 } else if strings.HasSuffix(v, "h") { unit = int64(time.Hour) subIdx = subIdx - 1 } else if strings.HasSuffix(v, "hour") { unit = int64(time.Hour) subIdx = subIdx - 4 } b, err := strconv.ParseInt(v[:subIdx], 10, 64) if err != nil { return 0, err } return b * unit, nil } // Keys return all the section keys. func (s *Section) Keys() []string { keys := []string{} for k, _ := range s.data { keys = append(keys, k) } return keys } // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. // (The argument to Unmarshal must be a non-nil pointer.) type InvalidUnmarshalError struct { Type reflect.Type } func (e *InvalidUnmarshalError) Error() string { if e.Type == nil { return "goconf: Unmarshal(nil)" } if e.Type.Kind() != reflect.Ptr { return "goconf: Unmarshal(non-pointer " + e.Type.String() + ")" } return "goconf: Unmarshal(nil " + e.Type.String() + ")" } // Unmarshal parses the goconf struct and stores the result in the value // pointed to by v. // // Struct values encode as goconf objects. Each exported struct field // becomes a member of the object unless // - the field's tag is "-", or // - the field is empty and its tag specifies the "omitempty" option. // The empty values are false, 0, any // nil pointer or interface value, and any array, slice, map, or string of // length zero. The object's section and key string is the struct field name // but can be specified in the struct field's tag value. The "goconf" key in // the struct field's tag value is the key name, followed by an optional comma // and options. Examples: // // // Field is ignored by this package. // Field int `goconf:"-"` // // // Field appears in goconf section "base" as key "myName". // Field int `goconf:"base:myName"` // // // Field appears in goconf section "base" as key "myName", the value split // // by delimiter ",". // Field []string `goconf:"base:myName:,"` // // // Field appears in goconf section "base" as key "myName", the value split // // by delimiter "," and key-value is splited by "=". // Field map[int]string `goconf:"base:myName:,"` // // // Field appears in goconf section "base" as key "myName", the value // // conver to time.Duration. When has extra tag "time", then goconf can // // parse such "1h", "1s" config values. // // // // Note the extra tag "time" only effect the int64 (time.Duration is int64) // Field time.Duration `goconf:"base:myName:time"` // // // Field appears in goconf section "base" as key "myName", when has extra // // tag, then goconf can parse like "1gb", "1mb" config values. // // // // Note the extra tag "memory" only effect the int (memory size is int). // Field int `goconf:"base:myName:memory"` // func (c *Config) Unmarshal(v interface{}) error { vv := reflect.ValueOf(v) if vv.Kind() != reflect.Ptr || vv.IsNil() { return &InvalidUnmarshalError{reflect.TypeOf(v)} } rv := vv.Elem() rt := rv.Type() n := rv.NumField() // enum every struct field for i := 0; i < n; i++ { vf := rv.Field(i) tf := rt.Field(i) tag := tf.Tag.Get("goconf") // if tag empty or "-" ignore if tag == "-" || tag == "" || tag == "omitempty" { continue } tagArr := strings.SplitN(tag, ":", 3) if len(tagArr) < 2 { return errors.New(fmt.Sprintf("error tag: %s, must be section:field:delim(optional)", tag)) } section := tagArr[0] key := tagArr[1] s := c.Get(section) if s == nil { // no config section continue } value, ok := s.data[key] if !ok { // no confit key continue } switch vf.Kind() { case reflect.String: vf.SetString(value) case reflect.Bool: vf.SetBool(parseBool(value)) case reflect.Float32: if tmp, err := strconv.ParseFloat(value, 32); err != nil { return err } else { vf.SetFloat(tmp) } case reflect.Float64: if tmp, err := strconv.ParseFloat(value, 64); err != nil { return err } else { vf.SetFloat(tmp) } case reflect.Int: if len(tagArr) == 3 { format := tagArr[2] // parse memory size if format == "memory" { if tmp, err := parseMemory(value); err != nil { return err } else { vf.SetInt(int64(tmp)) } } else { return errors.New(fmt.Sprintf("unknown tag: %s in struct field: %s (support tags: \"memory\")", format, tf.Name)) } } else { if tmp, err := strconv.ParseInt(value, 10, 32); err != nil { return err } else { vf.SetInt(tmp) } } case reflect.Int8: if tmp, err := strconv.ParseInt(value, 10, 8); err != nil { return err } else { vf.SetInt(tmp) } case reflect.Int16: if tmp, err := strconv.ParseInt(value, 10, 16); err != nil { return err } else { vf.SetInt(tmp) } case reflect.Int32: if tmp, err := strconv.ParseInt(value, 10, 32); err != nil { return err } else { vf.SetInt(tmp) } case reflect.Int64: if len(tagArr) == 3 { format := tagArr[2] // parse time if format == "time" { if tmp, err := parseTime(value); err != nil { return err } else { vf.SetInt(tmp) } } else { return errors.New(fmt.Sprintf("unknown tag: %s in struct field: %s (support tags: \"time\")", format, tf.Name)) } } else { if tmp, err := strconv.ParseInt(value, 10, 64); err != nil { return err } else { vf.SetInt(tmp) } } case reflect.Uint: if tmp, err := strconv.ParseUint(value, 10, 32); err != nil { return err } else { vf.SetUint(tmp) } case reflect.Uint8: if tmp, err := strconv.ParseUint(value, 10, 8); err != nil { return err } else { vf.SetUint(tmp) } case reflect.Uint16: if tmp, err := strconv.ParseUint(value, 10, 16); err != nil { return err } else { vf.SetUint(tmp) } case reflect.Uint32: if tmp, err := strconv.ParseUint(value, 10, 32); err != nil { return err } else { vf.SetUint(tmp) } case reflect.Uint64: if tmp, err := strconv.ParseUint(value, 10, 64); err != nil { return err } else { vf.SetUint(tmp) } case reflect.Slice: delim := "," if len(tagArr) > 2 { delim = tagArr[2] } strs := strings.Split(value, delim) sli := reflect.MakeSlice(tf.Type, 0, len(strs)) for _, str := range strs { vv, err := getValue(tf.Type.Elem().String(), str) if err != nil { return err } sli = reflect.Append(sli, vv) } vf.Set(sli) case reflect.Map: delim := "," if len(tagArr) > 2 { delim = tagArr[2] } strs := strings.Split(value, delim) m := reflect.MakeMap(tf.Type) for _, str := range strs { mapStrs := strings.SplitN(str, "=", 2) if len(mapStrs) < 2 { return errors.New(fmt.Sprintf("error map: %s, must be split by \"=\"", str)) } vk, err := getValue(tf.Type.Key().String(), mapStrs[0]) if err != nil { return err } vv, err := getValue(tf.Type.Elem().String(), mapStrs[1]) if err != nil { return err } m.SetMapIndex(vk, vv) } vf.Set(m) default: return errors.New(fmt.Sprintf("cannot unmarshall unsuported kind: %s into struct field: %s", vf.Kind().String(), tf.Name)) } } return nil } // getValue parse String to the type "t" reflect.Value. func getValue(t, v string) (reflect.Value, error) { var vv reflect.Value switch t { case "bool": d := parseBool(v) vv = reflect.ValueOf(d) case "int": d, err := strconv.ParseInt(v, 10, 32) if err != nil { return vv, err } vv = reflect.ValueOf(int(d)) case "int8": d, err := strconv.ParseInt(v, 10, 8) if err != nil { return vv, err } vv = reflect.ValueOf(int8(d)) case "int16": d, err := strconv.ParseInt(v, 10, 16) if err != nil { return vv, err } vv = reflect.ValueOf(int16(d)) case "int32": d, err := strconv.ParseInt(v, 10, 32) if err != nil { return vv, err } vv = reflect.ValueOf(int32(d)) case "int64": d, err := strconv.ParseInt(v, 10, 64) if err != nil { return vv, err } vv = reflect.ValueOf(int64(d)) case "uint": d, err := strconv.ParseUint(v, 10, 32) if err != nil { return vv, err } vv = reflect.ValueOf(uint(d)) case "uint8": d, err := strconv.ParseUint(v, 10, 8) if err != nil { return vv, err } vv = reflect.ValueOf(uint8(d)) case "uint16": d, err := strconv.ParseUint(v, 10, 16) if err != nil { return vv, err } vv = reflect.ValueOf(uint16(d)) case "uint32": d, err := strconv.ParseUint(v, 10, 32) if err != nil { return vv, err } vv = reflect.ValueOf(uint32(d)) case "uint64": d, err := strconv.ParseUint(v, 10, 64) if err != nil { return vv, err } vv = reflect.ValueOf(uint64(d)) case "float32": d, err := strconv.ParseFloat(v, 32) if err != nil { return vv, err } vv = reflect.ValueOf(float32(d)) case "float64": d, err := strconv.ParseFloat(v, 64) if err != nil { return vv, err } vv = reflect.ValueOf(float64(d)) case "string": vv = reflect.ValueOf(v) default: return vv, errors.New(fmt.Sprintf("unkown type: %s", t)) } return vv, nil }