Restructure the code a bit.

Add a more complete wrapper function called Configure and ConfigureWithDefaults.
Add ability to write a configmap back to a new file.
Changed my mind about multiple equals signs because some data like base64 uses equals signs.
Added more tests.
Bump to version 0.1.0 to signal closer to completeness/stability.
Changed domain name from gitea.wisellama.rocks to git.wisellama.rocks.
main v0.1.0
Sean Hickey 2022-11-09 00:16:36 -08:00
parent 5cc5aa419e
commit 4481060613
12 changed files with 429 additions and 245 deletions

View File

@ -1,8 +1,9 @@
all: linter test
test:
go test ./
go test
lint: linter
linter:
golangci-lint run

View File

@ -1,16 +1,18 @@
# gosimpleconf
```sh
go get gitea.wisellama.rocks/Wisellama/gosimpleconf@v0.0.4
go get git.wisellama.rocks/Wisellama/gosimpleconf@v0.1.0
```
This is a small library for parsing super simple configuration files.
This is a small library for parsing super simple configuration files
similar to Unix conf files.
It expects a file with the following format:
* Lines with key-values pairs separated by `=` (e.g. `foo = bar`)
* Lines that start with `#` are ignored (used for comments)
* Empty and whitespace-only lines are ignore
* Lines with data that don't have an `=` will cause an error.
* Empty and whitespace-only lines are ignored
* Lines with some data but no `=` will cause an error.
* Lines with multiple `=` are split on the literal first `=` seen, any remaining are part of the value.
The key-values pairs are simply parsed as strings and plopped into a
map for you to do whatever your program wants with them. If you need a
@ -25,15 +27,21 @@ url = www.wisellama.rocks
number_of_widgets = 1337
pi = 3.141592653589793238462643383
environment = production
# Quotes are optional
name = Dude guy
description = "Just some dude it was"
# Additional equals are just part of the value
base64_tacos = dGFjb3M=
a=b=c
# key: 'a', value: 'b=c'
```
Then to load that config file into a map:
```go
configMap, err := gosimpleconf.Load("file.conf")
configMap, err := gosimpleconf.Configure("file.conf")
```
Then everything is a string. You can parse it as needed.
@ -42,19 +50,43 @@ var piStr string = configMap["pi"]
pi, err := strconv.ParseFloat(value, 64)
```
To override values with cli flags, after parsing the conf file you can
use these functions to also parse the flags:
```go
flagMap := SetupFlagOverrides(configMap)
configMap = ParseFlags(configMap, flagmap)
```
### Command-Line Flag Overrides
Then you could, for example, override the url value without modifying
the config file:
```go
By default, the `Configure()` function will figure out which keys
exist in the config map and set those as cli flags that you can
override. For example, with the config file above, you could override
the `url` key with a cli flag:
```sh
./program --url=newthing.wisellama.rocks
```
If you don't want to support cli flag overrides, you can directly call
`ReadFile("file.conf")` instead.
If you want to support other cli flags that don't map to a config
setting, they must be set up before calling `Configure()`. This is
because the `ParseFlags()` function calls `flag.Parse()` to get all
the values for the config map overrides. You could also work around
this by calling `ReadFile` and `ParseFlags` yourself and add your
other logic in between the two calls.
### Default Config
You can specify a default configuration to return if the given config
file doesn't exist. Here's an example:
```go
defaultConfig := gosimpleconf.ConfigMap{
"game.title": "Some Game",
"log.writeToFile": "false",
}
configMap, err := gosimpleconf.ConfigureWithDefaults("file_that_doesnt_exist.conf", defaultConfig)
```
Then if the config file can't be found, the default config map values
will be used instead. You can still override the default values with
cli flags.
## Why?
I've always seen configuration files similar to this show up in

95
config_file.go Normal file
View File

@ -0,0 +1,95 @@
package gosimpleconf
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
type ConfigMap map[string]string
func Load(filename string) (ConfigMap, error) {
return ReadFile(filename)
}
func ReadFile(filename string) (ConfigMap, error) {
var err error
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
parsedMap, err := parseFile(file)
if err != nil {
return nil, err
}
return parsedMap, nil
}
func WriteFile(filename string, configMap ConfigMap) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
for k, v := range configMap {
fmt.Fprintf(file, "%s = %s\n", k, v)
}
return nil
}
func parseFile(config io.Reader) (ConfigMap, error) {
var err error
parsedMap := make(ConfigMap)
scanner := bufio.NewScanner(config)
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if len(trimmed) == 0 {
// ignore empty lines
continue
}
if trimmed[0] == '#' {
// ignore comments
continue
}
split := strings.Split(trimmed, "=")
if len(split) < 2 {
return nil, fmt.Errorf("failed to parse config line: %v", trimmed)
}
key := trimToken(split[0])
value := strings.Join(split[1:], "=") // Re-add any ='s that were part of the value
value = trimToken(value)
parsedMap[key] = value
}
if err = scanner.Err(); err != nil {
return nil, err
}
return parsedMap, nil
}
func trimToken(value string) string {
// Trim off whitespace
v := strings.TrimSpace(value)
// Trim off any surrounding quotes, everything is a string anyway
v = strings.TrimPrefix(v, "\"")
v = strings.TrimSuffix(v, "\"")
return v
}

104
config_file_test.go Normal file
View File

@ -0,0 +1,104 @@
package gosimpleconf
import (
"strings"
"testing"
)
func TestParseWithValidConfig(t *testing.T) {
var err error
configFileStr := `
# Some config file
foo=bar
asdf = 1234
# Things
wat.wat= thing
foo.wat =stuff
immaempty=
technically valid = haha hmm...
#Done
`
configFile := strings.NewReader(configFileStr)
expectedMap := make(ConfigMap)
expectedMap["foo"] = "bar"
expectedMap["asdf"] = "1234"
expectedMap["wat.wat"] = "thing"
expectedMap["foo.wat"] = "stuff"
expectedMap["immaempty"] = ""
expectedMap["technically valid"] = "haha hmm..."
parsedMap, err := parseFile(configFile)
if err != nil {
t.Errorf("failed while parsing the file")
}
validateMap(t, parsedMap, expectedMap)
}
func TestParseWithNoEquals(t *testing.T) {
var err error
configFileStr := `
Something here with no equals to split on
`
configFile := strings.NewReader(configFileStr)
_, err = parseFile(configFile)
if err == nil {
t.Errorf("parse did not thrown an error when it was supposed to")
}
}
func TestParseWithExtraQuotes(t *testing.T) {
var err error
configFileStr := `
some.name = "This is in quotes"
extra.quotes = ""This will keep the quotes""
internal.quotes = "Some "thing" is here"
"This should work too" = shrug I guess
`
configFile := strings.NewReader(configFileStr)
expectedMap := make(ConfigMap)
expectedMap["some.name"] = "This is in quotes"
expectedMap["extra.quotes"] = "\"This will keep the quotes\""
expectedMap["internal.quotes"] = "Some \"thing\" is here"
expectedMap["This should work too"] = "shrug I guess"
configMap, err := parseFile(configFile)
if err != nil {
t.Errorf("failed while parsing the file")
}
validateMap(t, configMap, expectedMap)
}
func TestParseExtraEquals(t *testing.T) {
configFileStr := `
foo=bar=true
base64_tacos = dGFjb3M=
"equals=splits" the key/value, quotes and other =equals= don't matter
yup=soup
`
configFile := strings.NewReader(configFileStr)
expectedMap := make(ConfigMap)
expectedMap["foo"] = "bar=true"
expectedMap["base64_tacos"] = "dGFjb3M="
expectedMap["equals"] = "splits\" the key/value, quotes and other =equals= don't matter"
expectedMap["yup"] = "soup"
configMap, err := parseFile(configFile)
if err != nil {
t.Errorf("failed while parsing the file")
}
validateMap(t, configMap, expectedMap)
}

36
conversions.go Normal file
View File

@ -0,0 +1,36 @@
package gosimpleconf
import (
"log"
"strconv"
)
func Bool(value string) bool {
v, err := strconv.ParseBool(value)
if err != nil {
log.Printf("error parsing bool %v - %v\n", v, err)
v = false
}
return v
}
func Int64(value string) int64 {
v, err := strconv.ParseInt(value, 10, 64)
if err != nil {
log.Printf("error parsing int %v - %v\n", v, err)
v = 0
}
return v
}
func Float64(value string) float64 {
v, err := strconv.ParseFloat(value, 64)
if err != nil {
log.Printf("error parsing float %v - %v\n", v, err)
v = 0.0
}
return v
}

28
flag_overrides.go Normal file
View File

@ -0,0 +1,28 @@
package gosimpleconf
import "flag"
type FlagMap map[string]*string
func SetupFlagOverrides(configMap ConfigMap) FlagMap {
flagMap := make(FlagMap)
for k, v := range configMap {
f := flag.String(k, v, "")
flagMap[k] = f
}
return flagMap
}
func ParseFlags(configMap ConfigMap, flagMap FlagMap) ConfigMap {
flag.Parse()
for k, v := range flagMap {
if v != nil && len(*v) > 0 {
configMap[k] = trimToken(*v)
}
}
return configMap
}

41
flag_overrides_test.go Normal file
View File

@ -0,0 +1,41 @@
package gosimpleconf
import (
"flag"
"strings"
"testing"
)
func TestFlagOverrides(t *testing.T) {
var err error
configFileStr := `
foo=bar
asdf = 1234
foo.wat =stuff
immaempty=
`
configFile := strings.NewReader(configFileStr)
expectedMap := make(ConfigMap)
expectedMap["foo"] = "bar"
expectedMap["asdf"] = "1234"
// expect to override the 'foo.wat' value with a cli flag
expectedMap["foo.wat"] = "iwasoverridden"
expectedMap["immaempty"] = ""
configMap, err := parseFile(configFile)
if err != nil {
t.Errorf("failed while parsing the file")
}
flagMap := SetupFlagOverrides(configMap)
err = flag.Set("foo.wat", "iwasoverridden")
if err != nil {
t.Errorf("failed to set cli flag override")
}
configMap = ParseFlags(configMap, flagMap)
validateMap(t, configMap, expectedMap)
}

2
go.mod
View File

@ -1,3 +1,3 @@
module gitea.wisellama.rocks/Wisellama/gosimpleconf
module git.wisellama.rocks/Wisellama/gosimpleconf
go 1.17

View File

@ -1,112 +1,31 @@
package gosimpleconf
import (
"bufio"
"flag"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
)
import "os"
type ConfigMap map[string]string
type FlagMap map[string]*string
func Load(filename string) (ConfigMap, error) {
func Configure(filename string) (ConfigMap, error) {
var err error
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
parsedMap, err := parseFile(file)
configMap, err := ReadFile(filename)
if err != nil {
return nil, err
}
return parsedMap, nil
flagMap := SetupFlagOverrides(configMap)
configMap = ParseFlags(configMap, flagMap)
return configMap, nil
}
func SetupFlagOverrides(configMap ConfigMap) FlagMap {
flagMap := make(FlagMap)
for k, v := range configMap {
f := flag.String(k, v, "")
flagMap[k] = f
}
return flagMap
}
func ParseFlags(configMap ConfigMap, flagMap FlagMap) ConfigMap {
flag.Parse()
for k, v := range flagMap {
if v != nil && len(*v) > 0 {
configMap[k] = trimToken(*v)
}
}
return configMap
}
func Bool(value string) bool {
v, err := strconv.ParseBool(value)
if err != nil {
log.Printf("error parsing bool %v - %v\n", v, err)
v = false
}
return v
}
func parseFile(config io.Reader) (ConfigMap, error) {
func ConfigureWithDefaults(filename string, defaultConf ConfigMap) (ConfigMap, error) {
var err error
parsedMap := make(ConfigMap)
scanner := bufio.NewScanner(config)
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if len(trimmed) == 0 {
// ignore empty lines
continue
}
if trimmed[0] == '#' {
// ignore comments
continue
}
split := strings.Split(trimmed, "=")
if len(split) != 2 {
return nil, fmt.Errorf("failed to parse config line: %v", trimmed)
}
key := trimToken(split[0])
value := trimToken(split[1])
parsedMap[key] = value
}
if err = scanner.Err(); err != nil {
_, err = os.Stat(filename)
if os.IsNotExist(err) {
// Config file does not exist, return a default configMap
return defaultConf, nil
} else if err != nil {
return nil, err
}
return parsedMap, nil
}
func trimToken(value string) string {
// Trim off whitespace
v := strings.TrimSpace(value)
// Trim off any surrounding quotes, everything is a string anyway
v = strings.TrimPrefix(v, "\"")
v = strings.TrimSuffix(v, "\"")
return v
return Configure(filename)
}

View File

@ -1,150 +1,40 @@
package gosimpleconf
import (
"flag"
"strings"
"testing"
)
import "testing"
func TestParseWithValidConfig(t *testing.T) {
var err error
configFileStr := `
# Some config file
func TestConfigureWithDefaults(t *testing.T) {
defaultMap := ConfigMap{
"some": "thing",
"cool": "1337",
}
foo=bar
asdf = 1234
filename := "idontexist_123456789"
# Things
wat.wat= thing
foo.wat =stuff
immaempty=
technically valid = haha hmm...
#Done
`
configFile := strings.NewReader(configFileStr)
expectedMap := make(ConfigMap)
expectedMap["foo"] = "bar"
expectedMap["asdf"] = "1234"
expectedMap["wat.wat"] = "thing"
expectedMap["foo.wat"] = "stuff"
expectedMap["immaempty"] = ""
expectedMap["technically valid"] = "haha hmm..."
parsedMap, err := parseFile(configFile)
conf, err := ConfigureWithDefaults(filename, defaultMap)
if err != nil {
t.Errorf("failed while parsing the file")
t.Errorf("unexpected error: %v", err)
}
validateMap(t, parsedMap, expectedMap)
validateMap(t, conf, defaultMap)
}
func TestParseWithExtraEquals(t *testing.T) {
var err error
configFileStr := `
foo=bar=true
`
configFile := strings.NewReader(configFileStr)
_, err = parseFile(configFile)
if err == nil {
t.Errorf("parse did not thrown an error when it was supposed to")
func TestConfigure(t *testing.T) {
filename := "test.conf"
expectedMap := ConfigMap{
"url": "www.wisellama.rocks",
"number_of_widgets": "1337",
"pi": "3.141592653589793238462643383",
"environment": "production",
"name": "Dude guy",
"description": "Just some dude it was",
"base64_tacos": "dGFjb3M=",
"a": "b=c",
}
}
func TestParseWithNoEquals(t *testing.T) {
var err error
configFileStr := `
Something here with no equals to split on
`
configFile := strings.NewReader(configFileStr)
_, err = parseFile(configFile)
if err == nil {
t.Errorf("parse did not thrown an error when it was supposed to")
}
}
func TestFlagOverrides(t *testing.T) {
var err error
configFileStr := `
foo=bar
asdf = 1234
foo.wat =stuff
immaempty=
`
configFile := strings.NewReader(configFileStr)
expectedMap := make(ConfigMap)
expectedMap["foo"] = "bar"
expectedMap["asdf"] = "1234"
// expect to override the 'foo.wat' value with a cli flag
expectedMap["foo.wat"] = "iwasoverridden"
expectedMap["immaempty"] = ""
configMap, err := parseFile(configFile)
conf, err := Configure(filename)
if err != nil {
t.Errorf("failed while parsing the file")
t.Errorf("unexpected error: %v", err)
}
flagMap := SetupFlagOverrides(configMap)
err = flag.Set("foo.wat", "iwasoverridden")
if err != nil {
t.Errorf("failed to set cli flag override")
}
configMap = ParseFlags(configMap, flagMap)
validateMap(t, configMap, expectedMap)
}
func TestParseWithExtraQuotes(t *testing.T) {
var err error
configFileStr := `
some.name = "This is in quotes"
extra.quotes = ""This will keep the quotes""
internal.quotes = "Some "thing" is here"
"This should work too" = shrug I guess
`
configFile := strings.NewReader(configFileStr)
expectedMap := make(ConfigMap)
expectedMap["some.name"] = "This is in quotes"
expectedMap["extra.quotes"] = "\"This will keep the quotes\""
expectedMap["internal.quotes"] = "Some \"thing\" is here"
expectedMap["This should work too"] = "shrug I guess"
configMap, err := parseFile(configFile)
if err != nil {
t.Errorf("failed while parsing the file")
}
validateMap(t, configMap, expectedMap)
}
func validateMap(t *testing.T, given map[string]string, expected map[string]string) {
if given == nil {
t.Errorf("given map was nil")
}
if expected == nil {
t.Errorf("expected map was nil")
}
expectedLen := len(expected)
givenLen := len(given)
if expectedLen != givenLen {
t.Errorf("size mismatch on maps - expected %v, given %v", expectedLen, givenLen)
}
for k, v := range expected {
if v != given[k] {
t.Errorf("incorrect value for key %v - expected %v, given %v", k, expected[k], given[k])
}
}
validateMap(t, conf, expectedMap)
}

13
test.conf Normal file
View File

@ -0,0 +1,13 @@
url = www.wisellama.rocks
number_of_widgets = 1337
pi = 3.141592653589793238462643383
environment = production
# Quotes are optional
name = Dude guy
description = "Just some dude it was"
# Additional equals are just part of the value
base64_tacos = dGFjb3M=
a=b=c
# key: 'a', value: 'b=c'

25
test_helper.go Normal file
View File

@ -0,0 +1,25 @@
package gosimpleconf
import "testing"
func validateMap(t *testing.T, given map[string]string, expected map[string]string) {
if given == nil {
t.Errorf("given map was nil")
}
if expected == nil {
t.Errorf("expected map was nil")
}
expectedLen := len(expected)
givenLen := len(given)
if expectedLen != givenLen {
t.Errorf("size mismatch on maps - expected %v, given %v", expectedLen, givenLen)
}
for k, v := range expected {
if v != given[k] {
t.Errorf("incorrect value for key %v - expected %v, given %v", k, expected[k], given[k])
}
}
}