236 lines
5.3 KiB
Go
236 lines
5.3 KiB
Go
// 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
|
|
}
|
|
}
|
|
}
|