// Package ini provides trivial parsing of .INI format files. package ini import ( "bufio" "fmt" "io" "regexp" "strconv" "strings" ) // Config is a parsed INI format file. type Config struct { sections []section comments []string } type section struct { name string comments []string options []option } type option struct { name, value string } var ( iniSectionRe = regexp.MustCompile(`^\[(.+)\]$`) iniOptionRe = regexp.MustCompile(`^([^\s=]+)\s*=\s*(.+?)$`) ) // Sections returns the list of sections in the file. func (c *Config) Sections() []string { var sections []string for _, sect := range c.sections { sections = append(sections, sect.name) } return sections } // Options returns the list of options in a given section. func (c *Config) Options(section string) []string { var options []string for _, sect := range c.sections { if sect.name == section { for _, opt := range sect.options { options = append(options, opt.name) } break } } return options } // OptionMap returns the map option => value for a given section. func (c *Config) OptionMap(section string) map[string]string { options := make(map[string]string) for _, sect := range c.sections { if sect.name == section { for _, opt := range sect.options { options[opt.name] = opt.value } break } } return options } // Comments returns the list of comments in a given section. // For the empty string, returns the file comments. func (c *Config) Comments(section string) []string { if section == "" { return c.comments } for _, sect := range c.sections { if sect.name == section { return sect.comments } } return nil } // AddComments appends the comment to the list of comments for the section. func (c *Config) AddComment(sect, comment string) { if sect == "" { c.comments = append(c.comments, comment) return } for i, s := range c.sections { if s.name == sect { c.sections[i].comments = append(s.comments, comment) return } } c.sections = append(c.sections, section{ name: sect, comments: []string{comment}, }) } // Parse reads the given io.Reader and returns a parsed Config object. func Parse(stream io.Reader) Config { var cfg Config var curSection string scanner := bufio.NewScanner(bufio.NewReader(stream)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { comment := strings.TrimLeft(line, ";# ") cfg.AddComment(curSection, comment) } else if len(line) > 0 { if m := iniSectionRe.FindStringSubmatch(line); len(m) > 0 { curSection = m[1] } else if m := iniOptionRe.FindStringSubmatch(line); len(m) > 0 { key := m[1] val := m[2] if !strings.Contains(val, "\"") { // If val does not contain any quote characers, we can make it // a quoted string and safely let strconv.Unquote sort out any // escapes val = "\"" + val + "\"" } if val[0] == '"' { val, _ = strconv.Unquote(val) } cfg.Set(curSection, key, val) } } } return cfg } // Write writes the sections and options to the io.Writer in INI format. func (c *Config) Write(out io.Writer) error { for _, cmt := range c.comments { fmt.Fprintln(out, "; "+cmt) } if len(c.comments) > 0 { fmt.Fprintln(out) } for _, sect := range c.sections { fmt.Fprintf(out, "[%s]\n", sect.name) for _, cmt := range sect.comments { fmt.Fprintln(out, "; "+cmt) } for _, opt := range sect.options { val := opt.value if len(val) == 0 { continue } // Quote the string if it begins or ends with space needsQuoting := val[0] == ' ' || val[len(val)-1] == ' ' if !needsQuoting { // Quote the string if it contains any unprintable characters for _, r := range val { if !strconv.IsPrint(r) { needsQuoting = true break } } } if needsQuoting { val = strconv.Quote(val) } fmt.Fprintf(out, "%s=%s\n", opt.name, val) } fmt.Fprintln(out) } return nil } // Get gets the value from the specified section and key name, or the empty // string if either the section or the key is missing. func (c *Config) Get(section, key string) string { for _, sect := range c.sections { if sect.name == section { for _, opt := range sect.options { if opt.name == key { return opt.value } } return "" } } return "" } // Set sets a value for an option in a section. If the option exists, it's // value will be overwritten. If the option does not exist, it will be added. // If the section does not exist, it will be added and the option added to it. func (c *Config) Set(sectionName, key, value string) { for i, sect := range c.sections { if sect.name == sectionName { for j, opt := range sect.options { if opt.name == key { c.sections[i].options[j].value = value return } } c.sections[i].options = append(sect.options, option{key, value}) return } } c.sections = append(c.sections, section{ name: sectionName, options: []option{{key, value}}, }) } // Delete removes the option from the specified section. func (c *Config) Delete(section, key string) { for sn, sect := range c.sections { if sect.name == section { for i, opt := range sect.options { if opt.name == key { c.sections[sn].options = append(sect.options[:i], sect.options[i+1:]...) return } } return } } }