204 lines
3.9 KiB
Go
204 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// line represents a single line in the configuration file.
|
|
type line struct {
|
|
kind lineKind
|
|
option string
|
|
value string
|
|
disabled bool
|
|
}
|
|
|
|
// lineKind represents the different kinds of configuration line.
|
|
type lineKind int
|
|
|
|
const (
|
|
_ lineKind = iota
|
|
ignore
|
|
parseError
|
|
comment
|
|
blank
|
|
value
|
|
)
|
|
|
|
func (l line) String() string {
|
|
switch l.kind {
|
|
case ignore, parseError, comment, blank:
|
|
return l.value
|
|
case value:
|
|
s := l.option
|
|
if l.disabled {
|
|
s = "; " + s
|
|
}
|
|
if l.value != "" {
|
|
s += " " + l.value
|
|
}
|
|
return s
|
|
}
|
|
panic("unexpected line kind")
|
|
}
|
|
|
|
func removeDross(s string) string {
|
|
return strings.Map(func(r rune) rune {
|
|
if r < 32 || r > 0x7f || unicode.IsControl(r) {
|
|
return -1
|
|
}
|
|
return r
|
|
}, s)
|
|
}
|
|
|
|
func parseLine(s string) line {
|
|
if s == "" {
|
|
return line{kind: blank}
|
|
}
|
|
if s[0] == '#' {
|
|
return line{kind: comment, value: s}
|
|
}
|
|
s = removeDross(s)
|
|
fields := strings.Fields(s)
|
|
if len(fields) == 0 {
|
|
return line{kind: blank}
|
|
}
|
|
// Strip leading semicolons (but record that we found them)
|
|
semi := false
|
|
for len(fields[0]) > 0 && fields[0][0] == ';' {
|
|
semi = true
|
|
fields[0] = fields[0][1:]
|
|
}
|
|
// Lose the first field if it was all semicolons
|
|
if fields[0] == "" {
|
|
fields = fields[1:]
|
|
}
|
|
switch len(fields) {
|
|
case 0:
|
|
// This can only happen if the line starts
|
|
// with a semicolon but has no other information
|
|
return line{kind: ignore}
|
|
case 1:
|
|
return line{
|
|
kind: value,
|
|
option: strings.ToUpper(fields[0]),
|
|
disabled: semi,
|
|
}
|
|
case 2:
|
|
return line{
|
|
kind: value,
|
|
option: strings.ToUpper(fields[0]),
|
|
value: fields[1],
|
|
disabled: semi,
|
|
}
|
|
}
|
|
return line{kind: parseError, value: s}
|
|
}
|
|
|
|
// Config represents a "standard" configuration file.
|
|
type Config struct {
|
|
options map[string]int // index of each option in lines.
|
|
lines []line
|
|
}
|
|
|
|
// index returns the index of the given option in
|
|
// c.lines, or -1 if not found.
|
|
func (c *Config) index(option string) int {
|
|
if i, ok := c.options[option]; ok {
|
|
return i
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// addLine adds a line to the config, ignoring
|
|
// duplicate options and "ignore" lines.
|
|
func (c *Config) addLine(l line) {
|
|
switch l.kind {
|
|
case ignore:
|
|
return
|
|
case value:
|
|
if c.index(l.option) >= 0 {
|
|
return
|
|
}
|
|
c.options[l.option] = len(c.lines)
|
|
c.lines = append(c.lines, l)
|
|
default:
|
|
c.lines = append(c.lines, l)
|
|
}
|
|
}
|
|
|
|
// ReadConfig reads a configuration file from path and returns it.
|
|
func ReadConfig(path string) (*Config, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
r := bufio.NewReader(f)
|
|
c := &Config{options: make(map[string]int)}
|
|
for {
|
|
s, err := r.ReadString('\n')
|
|
if s != "" {
|
|
if err == nil {
|
|
// strip newline unless we encountered an error without finding one.
|
|
s = s[:len(s)-1]
|
|
}
|
|
c.addLine(parseLine(s))
|
|
}
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// Set sets an option to a value, adding the option if necessary. If
|
|
// the option was previously disabled, it will be enabled.
|
|
func (c *Config) Set(option string, val string) {
|
|
if i := c.index(option); i >= 0 {
|
|
line := &c.lines[i]
|
|
line.disabled = false
|
|
line.value = val
|
|
return
|
|
}
|
|
c.addLine(line{
|
|
kind: value,
|
|
option: option,
|
|
value: val,
|
|
})
|
|
}
|
|
|
|
// Enable sets the enabled status of an option. It is
|
|
// ignored if the option does not already exist.
|
|
func (c *Config) Enable(option string, enabled bool) {
|
|
if i := c.index(option); i >= 0 {
|
|
c.lines[i].disabled = !enabled
|
|
}
|
|
}
|
|
|
|
// Write writes the configuration file to the writer.
|
|
func (c *Config) Write(w io.Writer) {
|
|
for _, line := range c.lines {
|
|
fmt.Println(line)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
c, err := ReadConfig("/tmp/cfg")
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
c.Enable("NEEDSPEELING", false)
|
|
c.Set("SEEDSREMOVED", "")
|
|
c.Set("NUMBEROFBANANAS", "1024")
|
|
c.Set("NUMBEROFSTRAWBERRIES", "62000")
|
|
c.Write(os.Stdout)
|
|
}
|