diff --git a/config/config.go b/config/config.go index 36de2ff..033c780 100644 --- a/config/config.go +++ b/config/config.go @@ -11,7 +11,7 @@ package config import ( "bufio" "fmt" - "log" + "maps" "os" "sort" "strings" @@ -33,14 +33,15 @@ func SetEnvPrefix(pfx string) { prefix = pfx } +const keyValueSplitLength = 2 + func addLine(line string) { if strings.HasPrefix(line, "#") || line == "" { return } - lineParts := strings.SplitN(line, "=", 2) - if len(lineParts) != 2 { - log.Print("skipping line: ", line) + lineParts := strings.SplitN(line, "=", keyValueSplitLength) + if len(lineParts) != keyValueSplitLength { return // silently ignore empty keys } @@ -49,7 +50,7 @@ func addLine(line string) { 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. func LoadFile(path string) error { file, err := os.Open(path) @@ -67,18 +68,16 @@ func LoadFile(path string) error { return scanner.Err() } -// 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). +// 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 - } + maps.Copy(vars, cmap[iniconf.DefaultSection]) smap, ok := cmap[section] if !ok { @@ -88,9 +87,7 @@ func LoadFileFor(path, section string, strict bool) error { return nil } - for key, value := range smap { - vars[key] = value - } + maps.Copy(vars, smap) return nil } @@ -107,7 +104,7 @@ func Get(key string) string { // GetDefault retrieves a value from either a configuration file or // 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. func GetDefault(key, def string) string { 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 -// environment. If the key isn't present, it will call log.Fatal, printing -// the missing key. +// environment. If the key isn't present, it will panic. func Require(key string) string { if v, ok := vars[key]; ok { return v @@ -131,7 +127,7 @@ func Require(key string) string { envMessage = " (note: looked for the key " + prefix + key 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 @@ -139,7 +135,8 @@ func Require(key string) string { // ListKeys returns a slice of the currently known keys. func ListKeys() []string { - keyList := []string{} + var keyList []string + for k := range vars { keyList = append(keyList, k) } diff --git a/config/config_test.go b/config/config_test.go index 6d40c99..210523e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,27 +1,26 @@ -package config +package config_test import ( "os" "testing" + + "git.wntrmute.dev/kyle/goutils/config" ) const ( testFilePath = "testdata/test.env" - // Keys + // Key constants. kOrder = "ORDER" kSpecies = "SPECIES" kName = "COMMON_NAME" - // Env eOrder = "corvus" eSpecies = "corvus corax" eName = "northern raven" - // File fOrder = "stringiformes" fSpecies = "strix aluco" - // Name isn't set in the file to test fall through. ) func init() { @@ -31,8 +30,8 @@ func init() { } func TestLoadEnvOnly(t *testing.T) { - order := Get(kOrder) - species := Get(kSpecies) + order := config.Get(kOrder) + species := config.Get(kSpecies) if order != eOrder { t.Errorf("want %s, have %s", eOrder, order) } @@ -43,14 +42,14 @@ func TestLoadEnvOnly(t *testing.T) { } func TestLoadFile(t *testing.T) { - err := LoadFile(testFilePath) + err := config.LoadFile(testFilePath) if err != nil { t.Fatal(err) } - order := Get(kOrder) - species := Get(kSpecies) - name := Get(kName) + order := config.Get(kOrder) + species := config.Get(kSpecies) + name := config.Get(kName) if order != fOrder { t.Errorf("want %s, have %s", fOrder, order) diff --git a/config/iniconf/iniconf.go b/config/iniconf/iniconf.go index 22838d2..df0da00 100644 --- a/config/iniconf/iniconf.go +++ b/config/iniconf/iniconf.go @@ -2,6 +2,7 @@ package iniconf import ( "bufio" + "errors" "fmt" "io" "os" @@ -23,30 +24,31 @@ var ( 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) +func ParseFile(fileName string) (ConfigMap, error) { + file, err := os.Open(fileName) if err != nil { - return + return nil, err } 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{} +func ParseReader(r io.Reader) (ConfigMap, error) { + cfg := ConfigMap{} buf := bufio.NewReader(r) var ( line string longLine bool currentSection string + err error ) for { line, longLine, err = readConfigLine(buf, line, longLine) - if err == io.EOF { + if errors.Is(err, io.EOF) { err = nil break } else if err != nil { @@ -62,11 +64,12 @@ func ParseReader(r io.Reader) (cfg ConfigMap, err error) { break } } - return + + return cfg, err } // 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() if err != nil { return "", false, err @@ -94,14 +97,14 @@ func processConfigLine(cfg ConfigMap, line string, currentSection string) (strin 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. func handleSectionLine(cfg ConfigMap, line string) (string, error) { section := configSection.ReplaceAllString(line, "$1") if section == "" { - return "", fmt.Errorf("invalid structure in file") + return "", errors.New("invalid structure in file") } if !cfg.SectionInConfig(section) { 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. -func (c ConfigMap) ListSections() (sections []string) { +func (c ConfigMap) ListSections() []string { + sections := make([]string, 0, len(c)) for section := range c { sections = append(sections, section) } - return + return sections } // 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) if err != nil { - return + return err } defer file.Close() for _, section := range c.ListSections() { sName := fmt.Sprintf("[ %s ]\n", section) - _, err = file.Write([]byte(sName)) - if err != nil { - return + if _, err = file.WriteString(sName); err != nil { + return err } for k, v := range c[section] { line := fmt.Sprintf("%s = %s\n", k, v) - _, err = file.Write([]byte(line)) - if err != nil { - return + if _, err = file.WriteString(line); err != nil { + return err } } - _, err = file.Write([]byte{0x0a}) - if err != nil { - return + if _, err = file.Write([]byte{0x0a}); err != nil { + return err } } - return + return nil } // 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. -func (c ConfigMap) GetValue(section, key string) (val string, present bool) { +func (c ConfigMap) GetValue(section, key string) (string, bool) { if c == nil { - return + return "", false } if section == "" { section = DefaultSection } - _, ok := c[section] - if !ok { - return + if _, ok := c[section]; !ok { + return "", false } - val, present = c[section][key] - return + val, present := c[section][key] + return val, present } // GetValueDefault retrieves the value from a key map if present, // 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) if !ok { return value @@ -226,7 +226,7 @@ func (c ConfigMap) GetValueDefault(section, key, value string) (val string) { } // 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 { return nil, false } @@ -235,13 +235,12 @@ func (c ConfigMap) SectionKeys(section string) (keys []string, present bool) { section = DefaultSection } - cm := c - s, ok := cm[section] + s, ok := c[section] if !ok { return nil, false } - keys = make([]string, 0, len(s)) + keys := make([]string, 0, len(s)) for key := range s { keys = append(keys, key) } diff --git a/config/iniconf/iniconf_test.go b/config/iniconf/iniconf_test.go index 26fa570..35a11cd 100644 --- a/config/iniconf/iniconf_test.go +++ b/config/iniconf/iniconf_test.go @@ -1,18 +1,19 @@ -package iniconf +package iniconf_test import ( "errors" - "fmt" "os" "sort" "testing" + + "git.wntrmute.dev/kyle/goutils/config/iniconf" ) // FailWithError is a utility for dumping errors and failing the test. func FailWithError(t *testing.T, err error) { - fmt.Println("failed") + t.Log("failed") if err != nil { - fmt.Println("[!] ", err.Error()) + t.Log("[!] ", err.Error()) } t.FailNow() } @@ -49,47 +50,50 @@ func stringSlicesEqual(slice1, slice2 []string) bool { func TestGoodConfig(t *testing.T) { testFile := "testdata/test.conf" - fmt.Printf("[+] validating known-good config... ") - cmap, err := ParseFile(testFile) + t.Logf("[+] validating known-good config... ") + cmap, err := iniconf.ParseFile(testFile) if err != nil { FailWithError(t, err) } else if len(cmap) != 2 { FailWithError(t, err) } - fmt.Println("ok") + t.Log("ok") } func TestGoodConfig2(t *testing.T) { testFile := "testdata/test2.conf" - fmt.Printf("[+] validating second known-good config... ") - cmap, err := ParseFile(testFile) - if err != nil { + t.Logf("[+] validating second known-good config... ") + cmap, err := iniconf.ParseFile(testFile) + switch { + case err != nil: FailWithError(t, err) - } else if len(cmap) != 1 { + case len(cmap) != 1: FailWithError(t, err) - } else if len(cmap["default"]) != 3 { + case len(cmap["default"]) != 3: FailWithError(t, err) + default: + // nothing to do here } - fmt.Println("ok") + t.Log("ok") } func TestBadConfig(t *testing.T) { testFile := "testdata/bad.conf" - fmt.Printf("[+] ensure invalid config file fails... ") - _, err := ParseFile(testFile) + t.Logf("[+] ensure invalid config file fails... ") + _, err := iniconf.ParseFile(testFile) if err == nil { - err = fmt.Errorf("invalid config file should fail") + err = errors.New("invalid config file should fail") FailWithError(t, err) } - fmt.Println("ok") + t.Log("ok") } 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 testOut = "testdata/test.out" - cmap, err := ParseFile(testFile) + cmap, err := iniconf.ParseFile(testFile) if err != nil { FailWithError(t, err) } @@ -100,7 +104,7 @@ func TestWriteConfigFile(t *testing.T) { FailWithError(t, err) } - cmap2, err := ParseFile(testOut) + cmap2, err := iniconf.ParseFile(testOut) if err != nil { FailWithError(t, err) } @@ -110,25 +114,25 @@ func TestWriteConfigFile(t *testing.T) { sort.Strings(sectionList1) sort.Strings(sectionList2) if !stringSlicesEqual(sectionList1, sectionList2) { - err = fmt.Errorf("section lists don't match") + err = errors.New("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") + err = errors.New("config key doesn't match") FailWithError(t, err) } } } - fmt.Println("ok") + t.Log("ok") } func TestQuotedValue(t *testing.T) { testFile := "testdata/test.conf" - fmt.Printf("[+] validating quoted value... ") - cmap, _ := ParseFile(testFile) + t.Logf("[+] validating quoted value... ") + cmap, _ := iniconf.ParseFile(testFile) val := cmap["sectionName"]["key4"] if val != " space at beginning and end " { 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 " { FailWithError(t, errors.New("Wrong value in single quotes ["+val+"]")) } - fmt.Println("ok") + t.Log("ok") } diff --git a/config/path.go b/config/path.go index fd09523..cab046e 100644 --- a/config/path.go +++ b/config/path.go @@ -1,5 +1,4 @@ //go:build !linux -// +build !linux package config diff --git a/config/path_test.go b/config/path_test.go index 95f20d4..011da00 100644 --- a/config/path_test.go +++ b/config/path_test.go @@ -1,7 +1,11 @@ -package config +package config_test -import "testing" +import ( + "testing" + + "git.wntrmute.dev/kyle/goutils/config" +) func TestDefaultPath(t *testing.T) { - t.Log(DefaultConfigPath("demoapp", "app.conf")) + t.Log(config.DefaultConfigPath("demoapp", "app.conf")) }