diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4d84e88 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9844781 --- /dev/null +++ b/README.md @@ -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: +n has value: 10000, type: int, err: +var distance has value: 13.42, type: float64, error value: +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 + +``` diff --git a/conf_test.go b/conf_test.go new file mode 100644 index 0000000..33364ed --- /dev/null +++ b/conf_test.go @@ -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() + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..97b3c7c --- /dev/null +++ b/config.go @@ -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 +} diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..5ee17e1 --- /dev/null +++ b/example/main.go @@ -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) +} diff --git a/go.mod b/go.mod index 40a226c..11fee30 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/dmfed/conf +module code.uint32.ru/tiny/conf -go 1.16 +go 1.24 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/parser.go b/parser.go index ccdc818..75c7799 100644 --- a/parser.go +++ b/parser.go @@ -5,67 +5,62 @@ import ( "io" "os" "regexp" - "strconv" "strings" ) var ( - configKeyValueRe = regexp.MustCompile(`(\w+) *= *(.+)\b[\t| |\n]*`) - configOptionRe = regexp.MustCompile(`(\w+)`) + configKeyValueRe = regexp.MustCompile(`(\w+) *= *(.+)\b[\t| |\n]*`) + configOptionRe = regexp.MustCompile(`(\w+)`) + configCommentedOut = "#" ) -type Config struct { - Values map[string]string -} - -func (c *Config) Get(key string) (s string) { - if val, ok := c.Values[key]; ok { - s = val - } - return -} - -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) { +// ParseFile reads values from file. It returns nil and error if os.Open(filename) fails. +// 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. +// Key or option must be a single word. In example line: +// "option1 option2" +// 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 +// captured. They can be later accesed separately with Setting's Split() method. +func ParseFile(filename string) (*Config, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() - c := parseReader(file) - return c, nil + return parseReader(file), 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 { 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) for scanner.Scan() { line := scanner.Text() - if strings.HasPrefix(line, "#") { + if strings.HasPrefix(line, configCommentedOut) { continue } switch { case configKeyValueRe.MatchString(line): kvpair := configKeyValueRe.FindStringSubmatch(line) - c.Values[kvpair[1]] = kvpair[2] + c.Settings[kvpair[1]] = kvpair[2] case configOptionRe.MatchString(line): opt := configOptionRe.FindString(line) - c.Values[opt] = "" + c.Options[opt] = struct{}{} } } return &c diff --git a/parser_test.go b/parser_test.go deleted file mode 100644 index df8d6f3..0000000 --- a/parser_test.go +++ /dev/null @@ -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() - } -} diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..f734e6d --- /dev/null +++ b/settings.go @@ -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 +}