config: apply linting feedback.

This commit is contained in:
2025-11-15 15:47:29 -08:00
parent aba5e519a4
commit 571443c282
6 changed files with 99 additions and 97 deletions

View File

@@ -11,7 +11,7 @@ package config
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"log" "maps"
"os" "os"
"sort" "sort"
"strings" "strings"
@@ -33,14 +33,15 @@ func SetEnvPrefix(pfx string) {
prefix = pfx prefix = pfx
} }
const keyValueSplitLength = 2
func addLine(line string) { func addLine(line string) {
if strings.HasPrefix(line, "#") || line == "" { if strings.HasPrefix(line, "#") || line == "" {
return return
} }
lineParts := strings.SplitN(line, "=", 2) lineParts := strings.SplitN(line, "=", keyValueSplitLength)
if len(lineParts) != 2 { if len(lineParts) != keyValueSplitLength {
log.Print("skipping line: ", line)
return // silently ignore empty keys return // silently ignore empty keys
} }
@@ -49,7 +50,7 @@ func addLine(line string) {
vars[lineParts[0]] = lineParts[1] vars[lineParts[0]] = lineParts[1]
} }
// LoadFile scans the file at path for key=value pairs and adds them // LoadFile scans the file at 'path' for key=value pairs and adds them
// to the configuration. // to the configuration.
func LoadFile(path string) error { func LoadFile(path string) error {
file, err := os.Open(path) file, err := os.Open(path)
@@ -67,18 +68,16 @@ func LoadFile(path string) error {
return scanner.Err() return scanner.Err()
} }
// LoadFileFor scans the ini file at path, loading the default section // LoadFileFor scans the ini file at 'path', loading the default section
// and overriding any keys found under section. If strict is true, the // 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). // named section must exist (i.e., to catch typos in the section name).
func LoadFileFor(path, section string, strict bool) error { func LoadFileFor(path, section string, strict bool) error {
cmap, err := iniconf.ParseFile(path) cmap, err := iniconf.ParseFile(path)
if err != nil { if err != nil {
return err return err
} }
for key, value := range cmap[iniconf.DefaultSection] { maps.Copy(vars, cmap[iniconf.DefaultSection])
vars[key] = value
}
smap, ok := cmap[section] smap, ok := cmap[section]
if !ok { if !ok {
@@ -88,9 +87,7 @@ func LoadFileFor(path, section string, strict bool) error {
return nil return nil
} }
for key, value := range smap { maps.Copy(vars, smap)
vars[key] = value
}
return nil return nil
} }
@@ -107,7 +104,7 @@ func Get(key string) string {
// GetDefault retrieves a value from either a configuration file or // GetDefault retrieves a value from either a configuration file or
// the environment. Note that value from a file will override // the environment. Note that value from a file will override
// environment variables. If a value isn't found (e.g. Get returns an // environment variables. If a value isn't found (e.g., Get returns an
// empty string), the default value will be used. // empty string), the default value will be used.
func GetDefault(key, def string) string { func GetDefault(key, def string) string {
if v := Get(key); v != "" { if v := Get(key); v != "" {
@@ -117,8 +114,7 @@ func GetDefault(key, def string) string {
} }
// Require retrieves a value from either a configuration file or the // Require retrieves a value from either a configuration file or the
// environment. If the key isn't present, it will call log.Fatal, printing // environment. If the key isn't present, it will panic.
// the missing key.
func Require(key string) string { func Require(key string) string {
if v, ok := vars[key]; ok { if v, ok := vars[key]; ok {
return v return v
@@ -131,7 +127,7 @@ func Require(key string) string {
envMessage = " (note: looked for the key " + prefix + key envMessage = " (note: looked for the key " + prefix + key
envMessage += " in the local env)" envMessage += " in the local env)"
} }
log.Fatalf("missing required configuration value %s%s", key, envMessage) panic(fmt.Sprintf("missing required configuration value %s%s", key, envMessage))
} }
return v return v
@@ -139,7 +135,8 @@ func Require(key string) string {
// ListKeys returns a slice of the currently known keys. // ListKeys returns a slice of the currently known keys.
func ListKeys() []string { func ListKeys() []string {
keyList := []string{} var keyList []string
for k := range vars { for k := range vars {
keyList = append(keyList, k) keyList = append(keyList, k)
} }

View File

@@ -1,27 +1,26 @@
package config package config_test
import ( import (
"os" "os"
"testing" "testing"
"git.wntrmute.dev/kyle/goutils/config"
) )
const ( const (
testFilePath = "testdata/test.env" testFilePath = "testdata/test.env"
// Keys // Key constants.
kOrder = "ORDER" kOrder = "ORDER"
kSpecies = "SPECIES" kSpecies = "SPECIES"
kName = "COMMON_NAME" kName = "COMMON_NAME"
// Env
eOrder = "corvus" eOrder = "corvus"
eSpecies = "corvus corax" eSpecies = "corvus corax"
eName = "northern raven" eName = "northern raven"
// File
fOrder = "stringiformes" fOrder = "stringiformes"
fSpecies = "strix aluco" fSpecies = "strix aluco"
// Name isn't set in the file to test fall through.
) )
func init() { func init() {
@@ -31,8 +30,8 @@ func init() {
} }
func TestLoadEnvOnly(t *testing.T) { func TestLoadEnvOnly(t *testing.T) {
order := Get(kOrder) order := config.Get(kOrder)
species := Get(kSpecies) species := config.Get(kSpecies)
if order != eOrder { if order != eOrder {
t.Errorf("want %s, have %s", eOrder, order) t.Errorf("want %s, have %s", eOrder, order)
} }
@@ -43,14 +42,14 @@ func TestLoadEnvOnly(t *testing.T) {
} }
func TestLoadFile(t *testing.T) { func TestLoadFile(t *testing.T) {
err := LoadFile(testFilePath) err := config.LoadFile(testFilePath)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
order := Get(kOrder) order := config.Get(kOrder)
species := Get(kSpecies) species := config.Get(kSpecies)
name := Get(kName) name := config.Get(kName)
if order != fOrder { if order != fOrder {
t.Errorf("want %s, have %s", fOrder, order) t.Errorf("want %s, have %s", fOrder, order)

View File

@@ -2,6 +2,7 @@ package iniconf
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -23,30 +24,31 @@ var (
var DefaultSection = "default" var DefaultSection = "default"
// ParseFile attempts to load the named config file. // ParseFile attempts to load the named config file.
func ParseFile(fileName string) (cfg ConfigMap, err error) { func ParseFile(fileName string) (ConfigMap, error) {
var file *os.File file, err := os.Open(fileName)
file, err = os.Open(fileName)
if err != nil { if err != nil {
return return nil, err
} }
defer file.Close() defer file.Close()
return ParseReader(file) return ParseReader(file)
} }
// ParseReader reads a configuration from an io.Reader. // ParseReader reads a configuration from an io.Reader.
func ParseReader(r io.Reader) (cfg ConfigMap, err error) { func ParseReader(r io.Reader) (ConfigMap, error) {
cfg = ConfigMap{} cfg := ConfigMap{}
buf := bufio.NewReader(r) buf := bufio.NewReader(r)
var ( var (
line string line string
longLine bool longLine bool
currentSection string currentSection string
err error
) )
for { for {
line, longLine, err = readConfigLine(buf, line, longLine) line, longLine, err = readConfigLine(buf, line, longLine)
if err == io.EOF { if errors.Is(err, io.EOF) {
err = nil err = nil
break break
} else if err != nil { } else if err != nil {
@@ -62,11 +64,12 @@ func ParseReader(r io.Reader) (cfg ConfigMap, err error) {
break break
} }
} }
return
return cfg, err
} }
// readConfigLine reads and assembles a complete configuration line, handling long lines. // readConfigLine reads and assembles a complete configuration line, handling long lines.
func readConfigLine(buf *bufio.Reader, currentLine string, longLine bool) (line string, stillLong bool, err error) { func readConfigLine(buf *bufio.Reader, currentLine string, longLine bool) (string, bool, error) {
lineBytes, isPrefix, err := buf.ReadLine() lineBytes, isPrefix, err := buf.ReadLine()
if err != nil { if err != nil {
return "", false, err return "", false, err
@@ -94,14 +97,14 @@ func processConfigLine(cfg ConfigMap, line string, currentSection string) (strin
return handleConfigLine(cfg, line, currentSection) return handleConfigLine(cfg, line, currentSection)
} }
return currentSection, fmt.Errorf("invalid config file") return currentSection, errors.New("invalid config file")
} }
// handleSectionLine processes a section header line. // handleSectionLine processes a section header line.
func handleSectionLine(cfg ConfigMap, line string) (string, error) { func handleSectionLine(cfg ConfigMap, line string) (string, error) {
section := configSection.ReplaceAllString(line, "$1") section := configSection.ReplaceAllString(line, "$1")
if section == "" { if section == "" {
return "", fmt.Errorf("invalid structure in file") return "", errors.New("invalid structure in file")
} }
if !cfg.SectionInConfig(section) { if !cfg.SectionInConfig(section) {
cfg[section] = make(map[string]string, 0) cfg[section] = make(map[string]string, 0)
@@ -139,41 +142,39 @@ func (c ConfigMap) SectionInConfig(section string) bool {
} }
// ListSections returns the list of sections in the config map. // ListSections returns the list of sections in the config map.
func (c ConfigMap) ListSections() (sections []string) { func (c ConfigMap) ListSections() []string {
sections := make([]string, 0, len(c))
for section := range c { for section := range c {
sections = append(sections, section) sections = append(sections, section)
} }
return return sections
} }
// WriteFile writes out the configuration to a file. // WriteFile writes out the configuration to a file.
func (c ConfigMap) WriteFile(filename string) (err error) { func (c ConfigMap) WriteFile(filename string) error {
file, err := os.Create(filename) file, err := os.Create(filename)
if err != nil { if err != nil {
return return err
} }
defer file.Close() defer file.Close()
for _, section := range c.ListSections() { for _, section := range c.ListSections() {
sName := fmt.Sprintf("[ %s ]\n", section) sName := fmt.Sprintf("[ %s ]\n", section)
_, err = file.Write([]byte(sName)) if _, err = file.WriteString(sName); err != nil {
if err != nil { return err
return
} }
for k, v := range c[section] { for k, v := range c[section] {
line := fmt.Sprintf("%s = %s\n", k, v) line := fmt.Sprintf("%s = %s\n", k, v)
_, err = file.Write([]byte(line)) if _, err = file.WriteString(line); err != nil {
if err != nil { return err
return
} }
} }
_, err = file.Write([]byte{0x0a}) if _, err = file.Write([]byte{0x0a}); err != nil {
if err != nil { return err
return
} }
} }
return return nil
} }
// AddSection creates a new section in the config map. // AddSection creates a new section in the config map.
@@ -197,27 +198,26 @@ func (c ConfigMap) AddKeyVal(section, key, val string) {
} }
// GetValue retrieves the value from a key map. // GetValue retrieves the value from a key map.
func (c ConfigMap) GetValue(section, key string) (val string, present bool) { func (c ConfigMap) GetValue(section, key string) (string, bool) {
if c == nil { if c == nil {
return return "", false
} }
if section == "" { if section == "" {
section = DefaultSection section = DefaultSection
} }
_, ok := c[section] if _, ok := c[section]; !ok {
if !ok { return "", false
return
} }
val, present = c[section][key] val, present := c[section][key]
return return val, present
} }
// GetValueDefault retrieves the value from a key map if present, // GetValueDefault retrieves the value from a key map if present,
// otherwise the default value. // otherwise the default value.
func (c ConfigMap) GetValueDefault(section, key, value string) (val string) { func (c ConfigMap) GetValueDefault(section, key, value string) string {
kval, ok := c.GetValue(section, key) kval, ok := c.GetValue(section, key)
if !ok { if !ok {
return value return value
@@ -226,7 +226,7 @@ func (c ConfigMap) GetValueDefault(section, key, value string) (val string) {
} }
// SectionKeys returns the sections in the config map. // SectionKeys returns the sections in the config map.
func (c ConfigMap) SectionKeys(section string) (keys []string, present bool) { func (c ConfigMap) SectionKeys(section string) ([]string, bool) {
if c == nil { if c == nil {
return nil, false return nil, false
} }
@@ -235,13 +235,12 @@ func (c ConfigMap) SectionKeys(section string) (keys []string, present bool) {
section = DefaultSection section = DefaultSection
} }
cm := c s, ok := c[section]
s, ok := cm[section]
if !ok { if !ok {
return nil, false return nil, false
} }
keys = make([]string, 0, len(s)) keys := make([]string, 0, len(s))
for key := range s { for key := range s {
keys = append(keys, key) keys = append(keys, key)
} }

View File

@@ -1,18 +1,19 @@
package iniconf package iniconf_test
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"sort" "sort"
"testing" "testing"
"git.wntrmute.dev/kyle/goutils/config/iniconf"
) )
// FailWithError is a utility for dumping errors and failing the test. // FailWithError is a utility for dumping errors and failing the test.
func FailWithError(t *testing.T, err error) { func FailWithError(t *testing.T, err error) {
fmt.Println("failed") t.Log("failed")
if err != nil { if err != nil {
fmt.Println("[!] ", err.Error()) t.Log("[!] ", err.Error())
} }
t.FailNow() t.FailNow()
} }
@@ -49,47 +50,50 @@ func stringSlicesEqual(slice1, slice2 []string) bool {
func TestGoodConfig(t *testing.T) { func TestGoodConfig(t *testing.T) {
testFile := "testdata/test.conf" testFile := "testdata/test.conf"
fmt.Printf("[+] validating known-good config... ") t.Logf("[+] validating known-good config... ")
cmap, err := ParseFile(testFile) cmap, err := iniconf.ParseFile(testFile)
if err != nil { if err != nil {
FailWithError(t, err) FailWithError(t, err)
} else if len(cmap) != 2 { } else if len(cmap) != 2 {
FailWithError(t, err) FailWithError(t, err)
} }
fmt.Println("ok") t.Log("ok")
} }
func TestGoodConfig2(t *testing.T) { func TestGoodConfig2(t *testing.T) {
testFile := "testdata/test2.conf" testFile := "testdata/test2.conf"
fmt.Printf("[+] validating second known-good config... ") t.Logf("[+] validating second known-good config... ")
cmap, err := ParseFile(testFile) cmap, err := iniconf.ParseFile(testFile)
if err != nil { switch {
case err != nil:
FailWithError(t, err) FailWithError(t, err)
} else if len(cmap) != 1 { case len(cmap) != 1:
FailWithError(t, err) FailWithError(t, err)
} else if len(cmap["default"]) != 3 { case len(cmap["default"]) != 3:
FailWithError(t, err) FailWithError(t, err)
default:
// nothing to do here
} }
fmt.Println("ok") t.Log("ok")
} }
func TestBadConfig(t *testing.T) { func TestBadConfig(t *testing.T) {
testFile := "testdata/bad.conf" testFile := "testdata/bad.conf"
fmt.Printf("[+] ensure invalid config file fails... ") t.Logf("[+] ensure invalid config file fails... ")
_, err := ParseFile(testFile) _, err := iniconf.ParseFile(testFile)
if err == nil { if err == nil {
err = fmt.Errorf("invalid config file should fail") err = errors.New("invalid config file should fail")
FailWithError(t, err) FailWithError(t, err)
} }
fmt.Println("ok") t.Log("ok")
} }
func TestWriteConfigFile(t *testing.T) { func TestWriteConfigFile(t *testing.T) {
fmt.Printf("[+] ensure config file is written properly... ") t.Logf("[+] ensure config file is written properly... ")
const testFile = "testdata/test.conf" const testFile = "testdata/test.conf"
const testOut = "testdata/test.out" const testOut = "testdata/test.out"
cmap, err := ParseFile(testFile) cmap, err := iniconf.ParseFile(testFile)
if err != nil { if err != nil {
FailWithError(t, err) FailWithError(t, err)
} }
@@ -100,7 +104,7 @@ func TestWriteConfigFile(t *testing.T) {
FailWithError(t, err) FailWithError(t, err)
} }
cmap2, err := ParseFile(testOut) cmap2, err := iniconf.ParseFile(testOut)
if err != nil { if err != nil {
FailWithError(t, err) FailWithError(t, err)
} }
@@ -110,25 +114,25 @@ func TestWriteConfigFile(t *testing.T) {
sort.Strings(sectionList1) sort.Strings(sectionList1)
sort.Strings(sectionList2) sort.Strings(sectionList2)
if !stringSlicesEqual(sectionList1, sectionList2) { if !stringSlicesEqual(sectionList1, sectionList2) {
err = fmt.Errorf("section lists don't match") err = errors.New("section lists don't match")
FailWithError(t, err) FailWithError(t, err)
} }
for _, section := range sectionList1 { for _, section := range sectionList1 {
for _, k := range cmap[section] { for _, k := range cmap[section] {
if cmap[section][k] != cmap2[section][k] { if cmap[section][k] != cmap2[section][k] {
err = fmt.Errorf("config key doesn't match") err = errors.New("config key doesn't match")
FailWithError(t, err) FailWithError(t, err)
} }
} }
} }
fmt.Println("ok") t.Log("ok")
} }
func TestQuotedValue(t *testing.T) { func TestQuotedValue(t *testing.T) {
testFile := "testdata/test.conf" testFile := "testdata/test.conf"
fmt.Printf("[+] validating quoted value... ") t.Logf("[+] validating quoted value... ")
cmap, _ := ParseFile(testFile) cmap, _ := iniconf.ParseFile(testFile)
val := cmap["sectionName"]["key4"] val := cmap["sectionName"]["key4"]
if val != " space at beginning and end " { if val != " space at beginning and end " {
FailWithError(t, errors.New("Wrong value in double quotes ["+val+"]")) FailWithError(t, errors.New("Wrong value in double quotes ["+val+"]"))
@@ -138,5 +142,5 @@ func TestQuotedValue(t *testing.T) {
if val != " is quoted with single quotes " { if val != " is quoted with single quotes " {
FailWithError(t, errors.New("Wrong value in single quotes ["+val+"]")) FailWithError(t, errors.New("Wrong value in single quotes ["+val+"]"))
} }
fmt.Println("ok") t.Log("ok")
} }

View File

@@ -1,5 +1,4 @@
//go:build !linux //go:build !linux
// +build !linux
package config package config

View File

@@ -1,7 +1,11 @@
package config package config_test
import "testing" import (
"testing"
"git.wntrmute.dev/kyle/goutils/config"
)
func TestDefaultPath(t *testing.T) { func TestDefaultPath(t *testing.T) {
t.Log(DefaultConfigPath("demoapp", "app.conf")) t.Log(config.DefaultConfigPath("demoapp", "app.conf"))
} }