basic version
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Dmitry Fedotov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
180
README.md
Normal file
180
README.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# module conf
|
||||||
|
|
||||||
|
**go get code.uint32.ru/tiny/conf** to download.
|
||||||
|
|
||||||
|
**import "code.uint32.ru/tiny/conf"** to use in your code.
|
||||||
|
|
||||||
|
Module conf implements a very simple config parser with two types of
|
||||||
|
values: key value pairs and single word options. Each of these must be
|
||||||
|
put on separate line in a file like this:
|
||||||
|
```
|
||||||
|
key1 = value
|
||||||
|
key2 = value1, value2, value3
|
||||||
|
option1
|
||||||
|
option2
|
||||||
|
```
|
||||||
|
Values can also be read from any io.Reader or io.ReadCloser. Note that
|
||||||
|
values in key value pairs mey either be separated with spaces or not.
|
||||||
|
|
||||||
|
Typical use case would look like this:
|
||||||
|
```go
|
||||||
|
config, err := conf.ParseFile("filename")
|
||||||
|
if err != nil {
|
||||||
|
// Means we failed to read from file
|
||||||
|
// config variable is now nil and unusable
|
||||||
|
}
|
||||||
|
value, err := config.Find("mykey")
|
||||||
|
if err != nil {
|
||||||
|
// Means that key has not been found
|
||||||
|
}
|
||||||
|
// value now holds conf.Setting type which can be accessed directly.
|
||||||
|
fmt.Println(value.Value)
|
||||||
|
n, err := value.Float64() // tries to parse float from Setting.Value field.
|
||||||
|
// if err is nil then n holds float64.
|
||||||
|
```
|
||||||
|
Shorter syntax is also available with Get() method which drops errors if
|
||||||
|
key was not found and simply returns Setting with empty Value field:
|
||||||
|
```go
|
||||||
|
listnumbers, err := config.Get("numbers").IntSlice()
|
||||||
|
// listnumbers holds slice of ints. If err is nil all of found values
|
||||||
|
// have converted successfully.
|
||||||
|
```
|
||||||
|
Note that we'd still like to check for errors in above example even if
|
||||||
|
we're sure the key exists. This way we'll configrm that all values have been converted.
|
||||||
|
|
||||||
|
There is also a GetDefault() method:
|
||||||
|
```go
|
||||||
|
def := config.GetDefault("non-existant-key", "myvalue")
|
||||||
|
fmt.Println(def.Value) // "myvalue"
|
||||||
|
```
|
||||||
|
To check whether single-word options were found use:
|
||||||
|
```go
|
||||||
|
if config.HasOption("wordoption") {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
```
|
||||||
|
See description of module's types and methods which are
|
||||||
|
quite self-explanatory.
|
||||||
|
|
||||||
|
See also **[https://pkg.go.dev/code.uint32.ru/tiny/conf](https://pkg.go.dev/code.uint32.ru/tiny/conf)** for a complete description of module's
|
||||||
|
functions.
|
||||||
|
|
||||||
|
Below is listing of a working example of a program parsing config (also found **example/main.go** in the repository).
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.uint32.ru/tiny/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testConf = []byte(`
|
||||||
|
# commented
|
||||||
|
port=10000
|
||||||
|
servers = 10.0.0.1, 10.0.0.2, 10.0.0.3
|
||||||
|
bool0=0
|
||||||
|
booltrue = true
|
||||||
|
distance=13.42
|
||||||
|
iamsure = hopefully you're not mistaken
|
||||||
|
float = 13.1984
|
||||||
|
color`)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := bytes.NewReader(testConf) // creating io.Reader from []byte()
|
||||||
|
config := conf.ParseReader(r) // we could call conf.ParseFile("filename") here
|
||||||
|
|
||||||
|
// First of all we can access parsed values directly:
|
||||||
|
for key, value := range config.Settings {
|
||||||
|
fmt.Println(key, value)
|
||||||
|
}
|
||||||
|
for opt := range config.Options {
|
||||||
|
fmt.Println(opt)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Find( key string) returns instance of Setting and an
|
||||||
|
// error if key was not found
|
||||||
|
//
|
||||||
|
// type Setting struct {
|
||||||
|
// Value string // value if found
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// You can access Setting's Value field (type string) directly.
|
||||||
|
port, err := config.Find("port")
|
||||||
|
fmt.Printf("variable port has type: %T, port.Value == %v, type of port.Value is: %T, error returned: %v\n", port, port.Value, port.Value, err)
|
||||||
|
|
||||||
|
// We can cast Setting.Value to a desired type including int, float64,
|
||||||
|
// bool and string. Method will return an error if Setting Value field
|
||||||
|
// can not be interpreted as desired type.
|
||||||
|
n, err := port.Int()
|
||||||
|
fmt.Printf("n has value: %v, type: %T, err: %v\n", n, n, err)
|
||||||
|
|
||||||
|
// Another syntax for getting Setting instance is to use Get() method
|
||||||
|
// which never returns errors. Get will return Setting with empty string
|
||||||
|
// in Value filed if requested key was now found.
|
||||||
|
d := config.Get("distance")
|
||||||
|
distance, err := d.Float64()
|
||||||
|
fmt.Printf("var distance has value: %v, type: %T, error value: %v\n", distance, distance, err)
|
||||||
|
|
||||||
|
// Get() syntax can be is slightly shorter if we're "sure" that key exists in
|
||||||
|
// the config.
|
||||||
|
sure := config.Get("iamsure").String() // String method never returns errors
|
||||||
|
// or simply sure := config.Get("iamsure").Value:
|
||||||
|
fmt.Println(sure)
|
||||||
|
|
||||||
|
// or like this:
|
||||||
|
fl, _ := config.Get("float").Float64() // we're dropping error here. Bad if value fails to convert.
|
||||||
|
fmt.Println("fl, _ := config.Get(\"float\").Float64() Value of var fl is:", fl)
|
||||||
|
|
||||||
|
// We can access comma-separated values of key like this:
|
||||||
|
servers := config.Get("servers").StringSlice()
|
||||||
|
fmt.Println("Found servers:")
|
||||||
|
for i, s := range servers {
|
||||||
|
fmt.Println(i+1, "\t", s)
|
||||||
|
}
|
||||||
|
// There is also a GetDefault() method which sets output Setting value
|
||||||
|
// to requested default if key was not found.
|
||||||
|
def := config.GetDefault("non-existant-key", "myvalue")
|
||||||
|
fmt.Println(def.Value) // "myvalue"
|
||||||
|
|
||||||
|
// You can use HasOption method to find whether single-word options were
|
||||||
|
// present in the the config
|
||||||
|
if config.HasOption("color") {
|
||||||
|
fmt.Println("Hooray, we've found option \"color\"!")
|
||||||
|
// do something useful
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below code finds two keys with bool values in the Config
|
||||||
|
// and outputs those.
|
||||||
|
var t, f bool
|
||||||
|
t, _ = config.Get("booltrue").Bool()
|
||||||
|
f, _ = config.Get("bool0").Bool()
|
||||||
|
fmt.Printf("t's type is: %T, value: %v, f's type is: %T, value: %v\n\n", t, t, f, f)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Here is the full output of the above code:
|
||||||
|
```
|
||||||
|
distance 13.42
|
||||||
|
iamsure hopefully you're not mistaken
|
||||||
|
float 13.1984
|
||||||
|
port 10000
|
||||||
|
servers 10.0.0.1, 10.0.0.2, 10.0.0.3
|
||||||
|
bool0 0
|
||||||
|
booltrue true
|
||||||
|
color
|
||||||
|
|
||||||
|
variable port has type: conf.Setting, port.Value == 10000, type of port.Value is: string, error returned: <nil>
|
||||||
|
n has value: 10000, type: int, err: <nil>
|
||||||
|
var distance has value: 13.42, type: float64, error value: <nil>
|
||||||
|
hopefully you're not mistaken
|
||||||
|
fl, _ := config.Get("float").Float64() Value of var fl is: 13.1984
|
||||||
|
Found servers:
|
||||||
|
1 10.0.0.1
|
||||||
|
2 10.0.0.2
|
||||||
|
3 10.0.0.3
|
||||||
|
Hooray, we've found option "color"!
|
||||||
|
t's type is: bool, value: true, f's type is: bool, value: false
|
||||||
|
|
||||||
|
```
|
106
conf_test.go
Normal file
106
conf_test.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testConf = []byte(`port=10000
|
||||||
|
# removed
|
||||||
|
two = two words
|
||||||
|
commas = abc, def, ghi
|
||||||
|
token=test
|
||||||
|
bool1=1
|
||||||
|
bool2=true
|
||||||
|
|
||||||
|
editor = vim
|
||||||
|
distance=13.42
|
||||||
|
floats=0.5,2.37,6
|
||||||
|
floatswithstring = 0.5, hello, 0.9
|
||||||
|
no missedme
|
||||||
|
color`)
|
||||||
|
|
||||||
|
func TestPackage(t *testing.T) {
|
||||||
|
r := bytes.NewReader(testConf)
|
||||||
|
c := parseReader(r)
|
||||||
|
if _, err := c.Get("floatswithstring").Float64Slice(); err == nil {
|
||||||
|
t.Log("Float64Slice accepting incorrect values")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if _, err := c.Get("floats").Float64Slice(); err != nil {
|
||||||
|
t.Log("Float64Slice failing on correct values")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if v := c.Get("token").String(); v != "test" {
|
||||||
|
t.Log("failed finding key value")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if v := c.Get("editor").String(); v != "vim" {
|
||||||
|
t.Log("failed finding key value")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if v, _ := c.Get("port").Int(); v != 10000 {
|
||||||
|
t.Log("failed finding int")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if v, _ := c.Get("distance").Float64(); v != 13.42 {
|
||||||
|
t.Log("failed finding key value")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if c.HasOption("color") != true {
|
||||||
|
t.Log("failed finding option")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if v, _ := c.Get("bool1").Bool(); v != true {
|
||||||
|
t.Log("failed finding bool1 value")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if v, _ := c.Get("bool2").Bool(); v != true {
|
||||||
|
t.Log("failed finding bool2 value")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if v := c.Get("two").String(); v != "two words" {
|
||||||
|
t.Log("failed finding key value")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if v := c.Get("commas").String(); v != "abc, def, ghi" {
|
||||||
|
t.Log("failed finding key value")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if v := c.Get("nonexistent").String(); v != "" {
|
||||||
|
t.Log("returned non-empty string for nonexistent key")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if c.HasOption("removed") {
|
||||||
|
t.Log("commented out line shows up in config")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
splitted := c.Get("commas").StringSlice()
|
||||||
|
if len(splitted) != 3 {
|
||||||
|
t.Log("could not split string")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
abc := splitted[0]
|
||||||
|
ghi := splitted[2]
|
||||||
|
if abc != "abc" || ghi != "ghi" {
|
||||||
|
t.Log("Split() returned incorrect values")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if c.HasOption("no") != true {
|
||||||
|
t.Log("should capture one option per line even if line holds two")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if c.HasOption("missedme") == true {
|
||||||
|
t.Log("should only capture one option per line")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
st := Setting{}
|
||||||
|
if v, err := st.Float64(); v != 0.0 || err == nil {
|
||||||
|
t.Log("empty string erroneously converts to float")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if def := c.GetDefault("non-existant-key", "myvalue"); def.Value != "myvalue" {
|
||||||
|
t.Log("GetDefault fails to apply default value")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
109
config.go
Normal file
109
config.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// module conf
|
||||||
|
//
|
||||||
|
// import "code.uint32.ru/tiny/conf"
|
||||||
|
//
|
||||||
|
// Module conf implements a very simple config parser with two types of
|
||||||
|
// values: key value pairs and single word options. Each of these must be
|
||||||
|
// put on separate line in a file like this:
|
||||||
|
//
|
||||||
|
// key1 = value
|
||||||
|
// key2 = value1, value2, value3
|
||||||
|
// key3=v1,v2,v3
|
||||||
|
// option1
|
||||||
|
// option2
|
||||||
|
//
|
||||||
|
// Values can also be read from any io.Reader or io.ReadCloser. Note that
|
||||||
|
// values in key value pairs mey either be separated with spaces or not.
|
||||||
|
// Typical use case would look like this:
|
||||||
|
//
|
||||||
|
// config, err := conf.ParseFile("filename")
|
||||||
|
// if err != nil {
|
||||||
|
// // Means we failed to read from file
|
||||||
|
// // config variable is now nil and unusable
|
||||||
|
// }
|
||||||
|
// value, err := config.Find("mykey")
|
||||||
|
// if err != nil {
|
||||||
|
// // Means that key has not been found
|
||||||
|
// }
|
||||||
|
// // value now holds conf.Setting.
|
||||||
|
// n, err := value.Float64() // tries to parse float from Setting.Value field.
|
||||||
|
// //if err is nil then n holds float64.
|
||||||
|
//
|
||||||
|
// There is also a quicker Get() method which returns no errors
|
||||||
|
// ("i'm feeling lucky" way to lookup values). If it does not find
|
||||||
|
// requested key, the returned Setting has empty string in Value field.
|
||||||
|
//
|
||||||
|
// value2 := config.Get("otherkey")
|
||||||
|
// mybool, err := value2.Bool() // tries to interpret Setting.Value field as bool
|
||||||
|
// // mybool now holds boolean if "otherkey" was found and error returned
|
||||||
|
// // by Bool() method is nil.
|
||||||
|
//
|
||||||
|
// Even shorter syntax would be:
|
||||||
|
//
|
||||||
|
// listnumbers, err := config.Get("numbers").IntSlice()
|
||||||
|
// // Note that we'd still like to check for errors even if
|
||||||
|
// // we're sure the key exists to make sure all values are converted.
|
||||||
|
// // listnumbers holds slice of ints. If err is nil all of found values
|
||||||
|
// // have been converted successfully.
|
||||||
|
//
|
||||||
|
// To check whether single-word options were found use:
|
||||||
|
//
|
||||||
|
// if config.HasOption("wordoption") {
|
||||||
|
// // do something
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// See description of module's other methods which are
|
||||||
|
// quite self-explanatory.
|
||||||
|
package conf
|
||||||
|
|
||||||
|
// Config holds parsed keys and values. Settings and Options can be
|
||||||
|
// accessed with Config.Settings and Config.Options maps directly.
|
||||||
|
type Config struct {
|
||||||
|
// Settings store key value pairs ("key = value" in config file)
|
||||||
|
// all key value pairs found when parsing input are accumulated in this map.
|
||||||
|
Settings map[string]string
|
||||||
|
// Options map stores single word options ("option" in config file)
|
||||||
|
Options map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find looks up a Setting and returns it. If returned error is not nil
|
||||||
|
// the requested key was not found and returned Setting has empty string in Value
|
||||||
|
// field.
|
||||||
|
func (c *Config) Find(key string) (s Setting, err error) {
|
||||||
|
v, ok := c.Settings[key]
|
||||||
|
if !ok {
|
||||||
|
err = ErrNotFound
|
||||||
|
}
|
||||||
|
s.Value = v
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a Setting. If key was not found the returned Setting Value
|
||||||
|
// will be empty string.
|
||||||
|
func (c *Config) Get(key string) (s Setting) {
|
||||||
|
s.Value = c.Settings[key]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefault looks up a Setting with requested key.
|
||||||
|
// If lookup fails it returns Setting with Value field set to def.
|
||||||
|
func (c *Config) GetDefault(key, def string) (s Setting) {
|
||||||
|
v, ok := c.Settings[key]
|
||||||
|
switch ok {
|
||||||
|
case true:
|
||||||
|
s.Value = v
|
||||||
|
default:
|
||||||
|
s.Value = def
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasOption returns true if line:
|
||||||
|
//
|
||||||
|
// "key"
|
||||||
|
//
|
||||||
|
// was found in the parsed file
|
||||||
|
func (c *Config) HasOption(option string) (exists bool) {
|
||||||
|
_, exists = c.Options[option]
|
||||||
|
return
|
||||||
|
}
|
91
example/main.go
Normal file
91
example/main.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.uint32.ru/tiny/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testConf = []byte(`
|
||||||
|
# commented
|
||||||
|
port=10000
|
||||||
|
servers = 10.0.0.1, 10.0.0.2, 10.0.0.3
|
||||||
|
bool0=0
|
||||||
|
booltrue = true
|
||||||
|
distance=13.42
|
||||||
|
iamsure = hopefully you're not mistaken
|
||||||
|
float = 13.1984
|
||||||
|
color`)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := bytes.NewReader(testConf) // creating io.Reader from []byte()
|
||||||
|
config := conf.ParseReader(r) // we could call conf.ParseFile("filename") here
|
||||||
|
|
||||||
|
// First of all we can access parsed values directly:
|
||||||
|
for key, value := range config.Settings {
|
||||||
|
fmt.Println(key, value)
|
||||||
|
}
|
||||||
|
for opt := range config.Options {
|
||||||
|
fmt.Println(opt)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Find( key string) returns instance of Setting and an
|
||||||
|
// error if key was not found
|
||||||
|
//
|
||||||
|
// type Setting struct {
|
||||||
|
// Value string // value if found
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// You can access Setting's Value field (type string) directly.
|
||||||
|
port, err := config.Find("port")
|
||||||
|
fmt.Printf("variable port has type: %T, port.Value == %v, type of port.Value is: %T, error returned: %v\n", port, port.Value, port.Value, err)
|
||||||
|
|
||||||
|
// We can cast Setting.Value to a desired type including int, float64,
|
||||||
|
// bool and string. Method will return an error if Setting Value field
|
||||||
|
// can not be interpreted as desired type.
|
||||||
|
n, err := port.Int()
|
||||||
|
fmt.Printf("n has value: %v, type: %T, err: %v\n", n, n, err)
|
||||||
|
|
||||||
|
// Another syntax for getting Setting instance is to use Get() method
|
||||||
|
// which never returns errors. Get will return Setting with empty string
|
||||||
|
// in Value filed if requested key was now found.
|
||||||
|
d := config.Get("distance")
|
||||||
|
distance, err := d.Float64()
|
||||||
|
fmt.Printf("var distance has value: %v, type: %T, error value: %v\n", distance, distance, err)
|
||||||
|
|
||||||
|
// Get() syntax can be is slightly shorter if we're "sure" that key exists in
|
||||||
|
// the config.
|
||||||
|
sure := config.Get("iamsure").String() // String method never returns errors
|
||||||
|
// or simply sure := config.Get("iamsure").Value:
|
||||||
|
fmt.Println(sure)
|
||||||
|
|
||||||
|
// or like this:
|
||||||
|
fl, _ := config.Get("float").Float64() // we're dropping error here. Bad if value fails to convert.
|
||||||
|
fmt.Println("fl, _ := config.Get(\"float\").Float64() Value of var fl is:", fl)
|
||||||
|
|
||||||
|
// We can access comma-separated values of key like this:
|
||||||
|
servers := config.Get("servers").StringSlice()
|
||||||
|
fmt.Println("Found servers:")
|
||||||
|
for i, s := range servers {
|
||||||
|
fmt.Println(i+1, "\t", s)
|
||||||
|
}
|
||||||
|
// There is also a GetDefault() method
|
||||||
|
def := config.GetDefault("non-existant-key", "myvalue")
|
||||||
|
fmt.Println(def.Value) // "myvalue"
|
||||||
|
|
||||||
|
// You can use HasOption method to find whether single-word options were
|
||||||
|
// present in the the config
|
||||||
|
if config.HasOption("color") {
|
||||||
|
fmt.Println("Hooray, we've found option \"color\"!")
|
||||||
|
// do something useful
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below code finds two keys with bool values in the Config
|
||||||
|
// and outputs those.
|
||||||
|
var t, f bool
|
||||||
|
t, _ = config.Get("booltrue").Bool()
|
||||||
|
f, _ = config.Get("bool0").Bool()
|
||||||
|
fmt.Printf("t's type is: %T, value: %v, f's type is: %T, value: %v\n\n", t, t, f, f)
|
||||||
|
}
|
4
go.mod
4
go.mod
@@ -1,3 +1,3 @@
|
|||||||
module github.com/dmfed/conf
|
module code.uint32.ru/tiny/conf
|
||||||
|
|
||||||
go 1.16
|
go 1.24
|
||||||
|
67
parser.go
67
parser.go
@@ -5,67 +5,62 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
configKeyValueRe = regexp.MustCompile(`(\w+) *= *(.+)\b[\t| |\n]*`)
|
configKeyValueRe = regexp.MustCompile(`(\w+) *= *(.+)\b[\t| |\n]*`)
|
||||||
configOptionRe = regexp.MustCompile(`(\w+)`)
|
configOptionRe = regexp.MustCompile(`(\w+)`)
|
||||||
|
configCommentedOut = "#"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
// ParseFile reads values from file. It returns nil and error if os.Open(filename) fails.
|
||||||
Values map[string]string
|
// It would be wise to always check returned error.
|
||||||
}
|
// ParseFile captures two types of values: "key = value" and "option". Either key value pair or
|
||||||
|
// option must be put in its own line in the file.
|
||||||
func (c *Config) Get(key string) (s string) {
|
// Key or option must be a single word. In example line:
|
||||||
if val, ok := c.Values[key]; ok {
|
// "option1 option2"
|
||||||
s = val
|
// only option1 will be captured. option2 needs to be in a separate line in the file to take effect.
|
||||||
}
|
// In line "key = value1,value2,value3" all of value1, value2, and value3 will be
|
||||||
return
|
// captured. They can be later accesed separately with Setting's Split() method.
|
||||||
}
|
func ParseFile(filename string) (*Config, error) {
|
||||||
|
|
||||||
func (c *Config) GetBool(key string) (b bool) {
|
|
||||||
_, b = c.Values[key]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) GetInt(key string) (n int) {
|
|
||||||
if val, ok := c.Values[key]; ok {
|
|
||||||
num, err := strconv.Atoi(val)
|
|
||||||
if err == nil {
|
|
||||||
n = num
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func Parse(filename string) (*Config, error) {
|
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
c := parseReader(file)
|
return parseReader(file), nil
|
||||||
return c, nil
|
}
|
||||||
|
|
||||||
|
// ParseReader reads from r and returns Config. See also ParseFile.
|
||||||
|
func ParseReader(r io.Reader) *Config {
|
||||||
|
return parseReader(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseReadCloser reads from r, returns Config and calls r.Close().
|
||||||
|
// See also ParseFile.
|
||||||
|
func ParseReadCloser(r io.ReadCloser) *Config {
|
||||||
|
defer r.Close()
|
||||||
|
return parseReader(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseReader(r io.Reader) *Config {
|
func parseReader(r io.Reader) *Config {
|
||||||
var c Config
|
var c Config
|
||||||
c.Values = make(map[string]string)
|
c.Settings = make(map[string]string)
|
||||||
|
c.Options = make(map[string]struct{})
|
||||||
scanner := bufio.NewScanner(r)
|
scanner := bufio.NewScanner(r)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, "#") {
|
if strings.HasPrefix(line, configCommentedOut) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case configKeyValueRe.MatchString(line):
|
case configKeyValueRe.MatchString(line):
|
||||||
kvpair := configKeyValueRe.FindStringSubmatch(line)
|
kvpair := configKeyValueRe.FindStringSubmatch(line)
|
||||||
c.Values[kvpair[1]] = kvpair[2]
|
c.Settings[kvpair[1]] = kvpair[2]
|
||||||
case configOptionRe.MatchString(line):
|
case configOptionRe.MatchString(line):
|
||||||
opt := configOptionRe.FindString(line)
|
opt := configOptionRe.FindString(line)
|
||||||
c.Values[opt] = ""
|
c.Options[opt] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &c
|
return &c
|
||||||
|
@@ -1,31 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testConf = []byte(`network
|
|
||||||
server=10.0.0.10
|
|
||||||
port=10000
|
|
||||||
token=test
|
|
||||||
editor=vim
|
|
||||||
color`)
|
|
||||||
|
|
||||||
func TestParser(t *testing.T) {
|
|
||||||
r := bytes.NewReader(testConf)
|
|
||||||
c := parseReader(r)
|
|
||||||
if c.Get("token") != "test" {
|
|
||||||
fmt.Println("failed finding key value")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if c.GetInt("port") != 10000 {
|
|
||||||
fmt.Println("failed finding int")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if c.GetBool("color") != true {
|
|
||||||
fmt.Println("failed finding bool")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
157
settings.go
Normal file
157
settings.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("key not found")
|
||||||
|
ErrParsingBool = errors.New("value can not be interpreted as bool")
|
||||||
|
ErrCouldNotConvert = errors.New("could not cast one or more values to required type")
|
||||||
|
)
|
||||||
|
|
||||||
|
var valuesSeparator = ","
|
||||||
|
|
||||||
|
var boolMap = map[string]bool{
|
||||||
|
// what evaluates to true
|
||||||
|
"true": true,
|
||||||
|
"t": true,
|
||||||
|
"1": true,
|
||||||
|
"yes": true,
|
||||||
|
"on": true,
|
||||||
|
// what evaluates to false
|
||||||
|
"false": false,
|
||||||
|
"f": false,
|
||||||
|
"0": false,
|
||||||
|
"no": false,
|
||||||
|
"off": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting represents key-value pair read from config file.
|
||||||
|
// It's Value field holds the value of key parsed from the configuration
|
||||||
|
type Setting struct {
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int converts Setting Value to int. Returned error
|
||||||
|
// will be non nil if convesion failed.
|
||||||
|
func (st Setting) Int() (int, error) {
|
||||||
|
return parseInt(st.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntSlice splits Setting Value (separator is ",") and adds
|
||||||
|
// each of resulting values to []int if possible.
|
||||||
|
// If one or more values can not be converted to float64 those will be dropped
|
||||||
|
// and method will return conf.ErrCouldNotConvert.
|
||||||
|
// Check error to be sure that all required values were parsed.
|
||||||
|
func (st Setting) IntSlice() ([]int, error) {
|
||||||
|
return parseIntSlice(st.Value, valuesSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* func (st Setting) split(sep string) Setting {
|
||||||
|
st.sep = sep //Choose separator to split values ?
|
||||||
|
return st
|
||||||
|
} */
|
||||||
|
|
||||||
|
// Float64 converts Setting Value to float64. Returned error
|
||||||
|
// will be non nil if convesion failed.
|
||||||
|
func (st Setting) Float64() (float64, error) {
|
||||||
|
return parseFloat64(st.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64Slice splits Setting Value (separator is ",") and adds
|
||||||
|
// each of resulting values to []float64 if possible.
|
||||||
|
// If one or more values can not be converted to float64 those will be dropped
|
||||||
|
// and method will return conf.ErrCouldNotConvert.
|
||||||
|
// Check error to be sure that all required values were parsed.
|
||||||
|
func (st Setting) Float64Slice() ([]float64, error) {
|
||||||
|
return parseFloat64Slice(st.Value, valuesSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns option Value as string
|
||||||
|
// This method also implements Stringer interface from fmt module
|
||||||
|
func (st Setting) String() string {
|
||||||
|
return st.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlice splits Setting's Value (separator is ",") and adds
|
||||||
|
// each of resulting values to []string trimming leading and trailing spaces
|
||||||
|
// from each string.
|
||||||
|
func (st Setting) StringSlice() []string {
|
||||||
|
return tidySplit(st.Value, valuesSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool tries to interpret Setting's Value as bool
|
||||||
|
// "1", "true", "on", "yes" (case insensitive) yields true
|
||||||
|
// "0", "false", "off", "no" (case insensitive) yields false
|
||||||
|
// If nothing matches will return false and conf.ErrParsingBool
|
||||||
|
func (st Setting) Bool() (bool, error) {
|
||||||
|
return parseBool(st.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInt(s string) (n int, err error) {
|
||||||
|
n, err = strconv.Atoi(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIntSlice(s, sep string) ([]int, error) {
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
err error
|
||||||
|
slice []int
|
||||||
|
digits []string
|
||||||
|
)
|
||||||
|
digits = tidySplit(s, sep)
|
||||||
|
for _, d := range digits {
|
||||||
|
n, err = strconv.Atoi(d)
|
||||||
|
if err != nil {
|
||||||
|
err = ErrCouldNotConvert
|
||||||
|
break
|
||||||
|
}
|
||||||
|
slice = append(slice, n)
|
||||||
|
}
|
||||||
|
return slice, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFloat64(s string) (n float64, err error) {
|
||||||
|
n, err = strconv.ParseFloat(s, 64)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFloat64Slice(s, sep string) ([]float64, error) {
|
||||||
|
var (
|
||||||
|
n float64
|
||||||
|
err error
|
||||||
|
slice []float64
|
||||||
|
digits []string
|
||||||
|
)
|
||||||
|
digits = tidySplit(s, sep)
|
||||||
|
for _, d := range digits {
|
||||||
|
n, err = strconv.ParseFloat(d, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = ErrCouldNotConvert
|
||||||
|
break
|
||||||
|
}
|
||||||
|
slice = append(slice, n)
|
||||||
|
}
|
||||||
|
return slice, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBool(s string) (value bool, err error) {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
value, ok := boolMap[s]
|
||||||
|
if !ok {
|
||||||
|
err = ErrParsingBool
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func tidySplit(s, sep string) []string {
|
||||||
|
splitted := strings.Split(s, sep)
|
||||||
|
for i, str := range splitted {
|
||||||
|
splitted[i] = strings.Trim(str, " ")
|
||||||
|
}
|
||||||
|
return splitted
|
||||||
|
}
|
Reference in New Issue
Block a user