Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bb1362c0e | |||
| 30ffbbdbc5 | |||
| b893e99864 | |||
| c7c51568d8 |
@@ -10,9 +10,12 @@ package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~kisom/goutils/config/iniconf"
|
||||
)
|
||||
|
||||
// NB: Rather than define a singleton type, everything is defined at
|
||||
@@ -67,6 +70,34 @@ func LoadFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFileFor scans the ini file at path, loading the default section
|
||||
// and overriding any keys found under section. If strict is true, the
|
||||
// named section must exist (i.e. to catch typos in the section name).
|
||||
func LoadFileFor(path, section string, strict bool) error {
|
||||
cmap, err := iniconf.ParseFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key, value := range cmap[iniconf.DefaultSection] {
|
||||
vars[key] = value
|
||||
}
|
||||
|
||||
smap, ok := cmap[section]
|
||||
if !ok {
|
||||
if strict {
|
||||
return fmt.Errorf("config: section '%s' wasn't found in the config file", section)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for key, value := range smap {
|
||||
vars[key] = value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a value from either a configuration file or the
|
||||
// environment. Note that values from a file will override environment
|
||||
// variables.
|
||||
|
||||
223
config/iniconf/iniconf.go
Normal file
223
config/iniconf/iniconf.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package iniconf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// ConfigMap is shorthand for the type used as a config struct.
|
||||
type ConfigMap map[string]map[string]string
|
||||
|
||||
var (
|
||||
configSection = regexp.MustCompile(`^\s*\[\s*(\w+)\s*\]\s*$`)
|
||||
quotedConfigLine = regexp.MustCompile(`^\s*(\w+)\s*=\s*["'](.*)["']\s*$`)
|
||||
configLine = regexp.MustCompile(`^\s*(\w+)\s*=\s*(.*)\s*$`)
|
||||
commentLine = regexp.MustCompile(`^#.*$`)
|
||||
blankLine = regexp.MustCompile(`^\s*$`)
|
||||
)
|
||||
|
||||
// DefaultSection is the label for the default ini file section.
|
||||
var DefaultSection = "default"
|
||||
|
||||
// ParseFile attempts to load the named config file.
|
||||
func ParseFile(fileName string) (cfg ConfigMap, err error) {
|
||||
var file *os.File
|
||||
file, err = os.Open(fileName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
return ParseReader(file)
|
||||
}
|
||||
|
||||
// ParseReader reads a configuration from an io.Reader.
|
||||
func ParseReader(r io.Reader) (cfg ConfigMap, err error) {
|
||||
cfg = ConfigMap{}
|
||||
buf := bufio.NewReader(r)
|
||||
|
||||
var (
|
||||
line string
|
||||
longLine bool
|
||||
currentSection string
|
||||
lineBytes []byte
|
||||
isPrefix bool
|
||||
)
|
||||
|
||||
for {
|
||||
err = nil
|
||||
lineBytes, isPrefix, err = buf.ReadLine()
|
||||
if io.EOF == err {
|
||||
err = nil
|
||||
break
|
||||
} else if err != nil {
|
||||
break
|
||||
} else if isPrefix {
|
||||
line += string(lineBytes)
|
||||
|
||||
longLine = true
|
||||
continue
|
||||
} else if longLine {
|
||||
line += string(lineBytes)
|
||||
longLine = false
|
||||
} else {
|
||||
line = string(lineBytes)
|
||||
}
|
||||
|
||||
if commentLine.MatchString(line) {
|
||||
continue
|
||||
} else if blankLine.MatchString(line) {
|
||||
continue
|
||||
} else if configSection.MatchString(line) {
|
||||
section := configSection.ReplaceAllString(line,
|
||||
"$1")
|
||||
if section == "" {
|
||||
err = fmt.Errorf("invalid structure in file")
|
||||
break
|
||||
} else if !cfg.SectionInConfig(section) {
|
||||
cfg[section] = make(map[string]string, 0)
|
||||
}
|
||||
currentSection = section
|
||||
} else if configLine.MatchString(line) {
|
||||
regex := configLine
|
||||
if quotedConfigLine.MatchString(line) {
|
||||
regex = quotedConfigLine
|
||||
}
|
||||
if currentSection == "" {
|
||||
currentSection = DefaultSection
|
||||
if !cfg.SectionInConfig(currentSection) {
|
||||
cfg[currentSection] = map[string]string{}
|
||||
}
|
||||
}
|
||||
key := regex.ReplaceAllString(line, "$1")
|
||||
val := regex.ReplaceAllString(line, "$2")
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
cfg[currentSection][key] = val
|
||||
} else {
|
||||
err = fmt.Errorf("invalid config file")
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SectionInConfig determines whether a section is in the configuration.
|
||||
func (c ConfigMap) SectionInConfig(section string) bool {
|
||||
_, ok := c[section]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ListSections returns the list of sections in the config map.
|
||||
func (c ConfigMap) ListSections() (sections []string) {
|
||||
for section := range c {
|
||||
sections = append(sections, section)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WriteFile writes out the configuration to a file.
|
||||
func (c ConfigMap) WriteFile(filename string) (err error) {
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for _, section := range c.ListSections() {
|
||||
sName := fmt.Sprintf("[ %s ]\n", section)
|
||||
_, err = file.Write([]byte(sName))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range c[section] {
|
||||
line := fmt.Sprintf("%s = %s\n", k, v)
|
||||
_, err = file.Write([]byte(line))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = file.Write([]byte{0x0a})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AddSection creates a new section in the config map.
|
||||
func (c ConfigMap) AddSection(section string) {
|
||||
if nil != c[section] {
|
||||
c[section] = map[string]string{}
|
||||
}
|
||||
}
|
||||
|
||||
// AddKeyVal adds a key value pair to a config map.
|
||||
func (c ConfigMap) AddKeyVal(section, key, val string) {
|
||||
if section == "" {
|
||||
section = DefaultSection
|
||||
}
|
||||
|
||||
if nil == c[section] {
|
||||
c.AddSection(section)
|
||||
}
|
||||
|
||||
c[section][key] = val
|
||||
}
|
||||
|
||||
// GetValue retrieves the value from a key map.
|
||||
func (c ConfigMap) GetValue(section, key string) (val string, present bool) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if section == "" {
|
||||
section = DefaultSection
|
||||
}
|
||||
|
||||
_, ok := c[section]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
val, present = c[section][key]
|
||||
return
|
||||
}
|
||||
|
||||
// GetValueDefault retrieves the value from a key map if present,
|
||||
// otherwise the default value.
|
||||
func (c ConfigMap) GetValueDefault(section, key, value string) (val string) {
|
||||
kval, ok := c.GetValue(section, key)
|
||||
if !ok {
|
||||
return value
|
||||
}
|
||||
return kval
|
||||
}
|
||||
|
||||
// SectionKeys returns the sections in the config map.
|
||||
func (c ConfigMap) SectionKeys(section string) (keys []string, present bool) {
|
||||
if c == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if section == "" {
|
||||
section = DefaultSection
|
||||
}
|
||||
|
||||
cm := c
|
||||
s, ok := cm[section]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
keys = make([]string, 0, len(s))
|
||||
for key := range s {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
return keys, true
|
||||
}
|
||||
142
config/iniconf/iniconf_test.go
Normal file
142
config/iniconf/iniconf_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package iniconf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FailWithError is a utility for dumping errors and failing the test.
|
||||
func FailWithError(t *testing.T, err error) {
|
||||
fmt.Println("failed")
|
||||
if err != nil {
|
||||
fmt.Println("[!] ", err.Error())
|
||||
}
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// UnlinkIfExists removes a file if it exists.
|
||||
func UnlinkIfExists(file string) {
|
||||
_, err := os.Stat(file)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
panic("failed to remove " + file)
|
||||
}
|
||||
os.Remove(file)
|
||||
}
|
||||
|
||||
// stringSlicesEqual compares two string lists, checking that they
|
||||
// contain the same elements.
|
||||
func stringSlicesEqual(slice1, slice2 []string) bool {
|
||||
if len(slice1) != len(slice2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range slice1 {
|
||||
if slice1[i] != slice2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for i := range slice2 {
|
||||
if slice1[i] != slice2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestGoodConfig(t *testing.T) {
|
||||
testFile := "testdata/test.conf"
|
||||
fmt.Printf("[+] validating known-good config... ")
|
||||
cmap, err := ParseFile(testFile)
|
||||
if err != nil {
|
||||
FailWithError(t, err)
|
||||
} else if len(cmap) != 2 {
|
||||
FailWithError(t, err)
|
||||
}
|
||||
fmt.Println("ok")
|
||||
}
|
||||
|
||||
func TestGoodConfig2(t *testing.T) {
|
||||
testFile := "testdata/test2.conf"
|
||||
fmt.Printf("[+] validating second known-good config... ")
|
||||
cmap, err := ParseFile(testFile)
|
||||
if err != nil {
|
||||
FailWithError(t, err)
|
||||
} else if len(cmap) != 1 {
|
||||
FailWithError(t, err)
|
||||
} else if len(cmap["default"]) != 3 {
|
||||
FailWithError(t, err)
|
||||
}
|
||||
fmt.Println("ok")
|
||||
}
|
||||
|
||||
func TestBadConfig(t *testing.T) {
|
||||
testFile := "testdata/bad.conf"
|
||||
fmt.Printf("[+] ensure invalid config file fails... ")
|
||||
_, err := ParseFile(testFile)
|
||||
if err == nil {
|
||||
err = fmt.Errorf("invalid config file should fail")
|
||||
FailWithError(t, err)
|
||||
}
|
||||
fmt.Println("ok")
|
||||
}
|
||||
|
||||
func TestWriteConfigFile(t *testing.T) {
|
||||
fmt.Printf("[+] ensure config file is written properly... ")
|
||||
const testFile = "testdata/test.conf"
|
||||
const testOut = "testdata/test.out"
|
||||
|
||||
cmap, err := ParseFile(testFile)
|
||||
if err != nil {
|
||||
FailWithError(t, err)
|
||||
}
|
||||
|
||||
defer UnlinkIfExists(testOut)
|
||||
err = cmap.WriteFile(testOut)
|
||||
if err != nil {
|
||||
FailWithError(t, err)
|
||||
}
|
||||
|
||||
cmap2, err := ParseFile(testOut)
|
||||
if err != nil {
|
||||
FailWithError(t, err)
|
||||
}
|
||||
|
||||
sectionList1 := cmap.ListSections()
|
||||
sectionList2 := cmap2.ListSections()
|
||||
sort.Strings(sectionList1)
|
||||
sort.Strings(sectionList2)
|
||||
if !stringSlicesEqual(sectionList1, sectionList2) {
|
||||
err = fmt.Errorf("section lists don't match")
|
||||
FailWithError(t, err)
|
||||
}
|
||||
|
||||
for _, section := range sectionList1 {
|
||||
for _, k := range cmap[section] {
|
||||
if cmap[section][k] != cmap2[section][k] {
|
||||
err = fmt.Errorf("config key doesn't match")
|
||||
FailWithError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println("ok")
|
||||
}
|
||||
|
||||
func TestQuotedValue(t *testing.T) {
|
||||
testFile := "testdata/test.conf"
|
||||
fmt.Printf("[+] validating quoted value... ")
|
||||
cmap, _ := ParseFile(testFile)
|
||||
val := cmap["sectionName"]["key4"]
|
||||
if val != " space at beginning and end " {
|
||||
FailWithError(t, errors.New("Wrong value in double quotes ["+val+"]"))
|
||||
}
|
||||
|
||||
val = cmap["sectionName"]["key5"]
|
||||
if val != " is quoted with single quotes " {
|
||||
FailWithError(t, errors.New("Wrong value in single quotes ["+val+"]"))
|
||||
}
|
||||
fmt.Println("ok")
|
||||
}
|
||||
5
config/iniconf/testdata/bad.conf
vendored
Normal file
5
config/iniconf/testdata/bad.conf
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
[]
|
||||
|
||||
key
|
||||
another key
|
||||
key = val
|
||||
13
config/iniconf/testdata/test.conf
vendored
Normal file
13
config/iniconf/testdata/test.conf
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
[ sectionName ]
|
||||
key1=some value
|
||||
key2 = some other value
|
||||
# we want to explain the importance and great forethought
|
||||
# in this next value.
|
||||
key3 = unintuitive value
|
||||
key4 = " space at beginning and end "
|
||||
key5 = ' is quoted with single quotes '
|
||||
|
||||
[ anotherSection ]
|
||||
key1 = a value
|
||||
key2 = yet another value
|
||||
key1 = overwrites previous value of a value
|
||||
3
config/iniconf/testdata/test2.conf
vendored
Normal file
3
config/iniconf/testdata/test2.conf
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
key1 = some value
|
||||
key2 = some other value
|
||||
key3 = unintuitive value
|
||||
19
config/path.go
Normal file
19
config/path.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// DefaultConfigPath returns a sensible default configuration file path.
|
||||
func DefaultConfigPath(dir, base string) string {
|
||||
user, err := user.Current()
|
||||
if err != nil || user.HomeDir == "" {
|
||||
return filepath.Join(dir, base)
|
||||
}
|
||||
|
||||
return filepath.Join(user.HomeDir, dir, base)
|
||||
}
|
||||
43
config/path_linux.go
Normal file
43
config/path_linux.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// canUseXDGConfigDir checks whether the XDG config directory exists
|
||||
// and is accessible by the current user. If it is present, it will
|
||||
// be returned. Note that if the directory does not exist, it is
|
||||
// presumed unusable.
|
||||
func canUseXDGConfigDir() (string, bool) {
|
||||
xdgDir := os.Getenv("XDG_CONFIG_DIR")
|
||||
if xdgDir == "" {
|
||||
userDir := os.Getenv("HOME")
|
||||
if userDir == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
xdgDir = filepath.Join(userDir, ".config")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(xdgDir)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return xdgDir, true
|
||||
}
|
||||
|
||||
// DefaultConfigPath returns a sensible default configuration file path.
|
||||
func DefaultConfigPath(dir, base string) string {
|
||||
dirPath, ok := canUseXDGConfigDir()
|
||||
if !ok {
|
||||
dirPath = "/etc"
|
||||
}
|
||||
|
||||
return filepath.Join(dirPath, dir, base)
|
||||
}
|
||||
7
config/path_test.go
Normal file
7
config/path_test.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaultPath(t *testing.T) {
|
||||
t.Log(DefaultConfigPath("demoapp", "app.conf"))
|
||||
}
|
||||
@@ -45,6 +45,7 @@ func (b *Buffer) Rewind() {
|
||||
|
||||
// Close clears all the data out of the buffer and sets the read position to 0.
|
||||
func (b *Buffer) Close() error {
|
||||
b.Clear()
|
||||
b.data = nil
|
||||
b.pos = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,9 +5,15 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type WriteStringCloser interface {
|
||||
Write([]byte) (int, error)
|
||||
WriteString(string) (int, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Tee emulates the Unix tee(1) command.
|
||||
type Tee struct {
|
||||
f *os.File
|
||||
f WriteStringCloser
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user