Skip to content

Commit

Permalink
Merge pull request #1 from emar-kar/options
Browse files Browse the repository at this point in the history
add options
  • Loading branch information
emar-kar authored Mar 8, 2023
2 parents d6c4d4e + 9f64e87 commit 509ec63
Show file tree
Hide file tree
Showing 13 changed files with 602 additions and 101 deletions.
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,80 @@ It's also possible to override the values to use when interpolating values by pr
```

Will get ```testing/whatever``` as the value

## Options
The ConfigParser supports almost all custom options available in the Python version.

* Delimiters - allows to set custom **key-value** pair delimiters.
* CommentPrefixes - allows to set custom comment line prefix. If line starts with one of the given `Prefixes` it will be passed during parsing.
* InlineCommentPrefixes - allows to set custom inline comment delimiter. This option checks if the line contains any of the given `Prefixes` and if so, splits the string by the prefix and returns the 0 index of the slice.
* Strict - if set to `true`, parser will return new wrapped `ErrAlreadyExist` for duplicates of *sections* or *options* in one source.
* Interpolation - allows to set custom behaviour for values interpolation. Interface was added, which defaults to `chainmap.ChainMap` instance.
```go
type Interpolator interface {
Add(...chainmap.Dict)
Len() int
Get(string) string
}
```
* Converters - allows to set custom values parsers.
```go
type ConvertFunc func(string) (any, error)
```
`ConvertFunc` can modify requested value if needed e.g.,
```go
package main

import (
"fmt"
"strings"

"github.com/bigkevmcd/go-configparser"
)

func main() {
stringConv := func(s string) (any, error) {
return s + "_updated", nil
}

conv := configparser.Converter{
configparser.String: stringConv,
}

p, err := configparser.ParseReaderWithOptions(
strings.NewReader("[section]\noption=value\n\n"),
configparser.Converters(conv),
)
// handle err

v, err := p.Get("section", "option")
// handle err

fmt.Println(v == "value_updated") // true
}
```
Those functions triggered inside `ConfigParser.Get*` methods if presented and wraps the return value.
> NOTE: Since `ConvertFunc` returns `any`, the caller should guarantee type assertion to the requested type after custom processing!
```go
type Converter map[string]ConvertFunc
```
`Converter` is a `map` type, which supports *int* (for `int64`), *string*, *bool*, *float* (for `float64`) keys.

---
Default options, which are always preset:
```go
func defaultOptions() *options {
return &options{
interpolation: chainmap.New(),
defaultSection: defaultSectionName,
delimiters: ":=",
commentPrefixes: Prefixes{"#", ";"},
converters: Converter{
StringConv: defaultGet,
IntConv: defaultGetInt64,
FloatConv: defaultGetFloat64,
BoolConv: defaultGetBool,
},
}
}
```
11 changes: 11 additions & 0 deletions chainmap/chainmap.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package chainmap

// Dict is a simple string->string map.
type Dict map[string]string

// ChainMap contains a slice of Dicts for interpolation values.
type ChainMap struct {
maps []Dict
}

// New creates a new ChainMap.
func New(dicts ...Dict) *ChainMap {
chainMap := &ChainMap{
maps: make([]Dict, 0),
Expand All @@ -15,10 +18,18 @@ func New(dicts ...Dict) *ChainMap {
return chainMap
}

// Add adds given dicts to the ChainMap.
func (c *ChainMap) Add(dicts ...Dict) {
c.maps = append(c.maps, dicts...)
}

// Len returns the ammount of Dicts in the ChainMap.
func (c *ChainMap) Len() int {
return len(c.maps)
}

// Get gets the last value with the given key from the ChainMap.
// If key does not exist returns empty string.
func (c *ChainMap) Get(key string) string {
var value string

Expand Down
14 changes: 13 additions & 1 deletion chainmap/chainmap_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package chainmap_test

import (
"github.com/bigkevmcd/go-configparser/chainmap"
"testing"

"github.com/bigkevmcd/go-configparser/chainmap"

gc "gopkg.in/check.v1"
)

Expand Down Expand Up @@ -51,3 +52,14 @@ func (s *ChainMapSuite) TestGet3(c *gc.C) {
result := chainMap.Get("value")
c.Assert(result, gc.Equals, "3")
}

func (s *ChainMapSuite) TestAdd(c *gc.C) {
chainMap := chainmap.New(s.dict1)

result := chainMap.Get("value")
c.Assert(result, gc.Equals, "3")

chainMap.Add(s.dict2)
result = chainMap.Get("value")
c.Assert(result, gc.Equals, "4")
}
72 changes: 47 additions & 25 deletions configparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package configparser
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
Expand All @@ -12,18 +13,13 @@ import (
"unicode"
)

const (
defaultSectionName = "DEFAULT"
maxInterpolationDepth int = 10
)

var (
sectionHeader = regexp.MustCompile(`^\[([^]]+)\]$`)
keyValue = regexp.MustCompile(`([^:=\s][^:=]*)\s*(?P<vi>[:=])\s*(.*)$`)
keyWNoValue = regexp.MustCompile(`([^:=\s][^:=]*)\s*((?P<vi>[:=])\s*(.*)$)?`)
sectionHeader = regexp.MustCompile(`^\[([^]]+)\]`)
interpolater = regexp.MustCompile(`%\(([^)]*)\)s`)
)

var ErrAlreadyExist = errors.New("already exist")

var boolMapping = map[string]bool{
"1": true,
"true": true,
Expand Down Expand Up @@ -51,7 +47,7 @@ type ConfigParser struct {

// Keys returns a sorted slice of keys
func (d Dict) Keys() []string {
var keys []string
keys := make([]string, 0, len(d))

for key := range d {
keys = append(keys, key)
Expand All @@ -74,26 +70,25 @@ func New() *ConfigParser {
return &ConfigParser{
config: make(Config),
defaults: newSection(defaultSectionName),
opt: &options{},
opt: defaultOptions(),
}
}

// NewWithOptions creates a new ConfigParser with options.
func NewWithOptions(opts ...OptFunc) *ConfigParser {
opt := &options{}
func NewWithOptions(opts ...optFunc) *ConfigParser {
opt := defaultOptions()
for _, fn := range opts {
fn(opt)
}

return &ConfigParser{
config: make(Config),
defaults: newSection(defaultSectionName),
defaults: newSection(opt.defaultSection),
opt: opt,
}
}

// NewWithDefaults allows creation of a new ConfigParser with a pre-existing
// Dict.
// NewWithDefaults allows creation of a new ConfigParser with a pre-existing Dict.
func NewWithDefaults(defaults Dict) (*ConfigParser, error) {
p := New()
for key, value := range defaults {
Expand Down Expand Up @@ -123,7 +118,7 @@ func ParseReader(in io.Reader) (*ConfigParser, error) {
}

// ParseReaderWithOptions parses a ConfigParser from the provided input with given options.
func ParseReaderWithOptions(in io.Reader, opts ...OptFunc) (*ConfigParser, error) {
func ParseReaderWithOptions(in io.Reader, opts ...optFunc) (*ConfigParser, error) {
p := NewWithOptions(opts...)
err := p.ParseReader(in)

Expand All @@ -145,7 +140,7 @@ func Parse(filename string) (*ConfigParser, error) {
}

// ParseWithOptions takes a filename and parses it into a ConfigParser value with given options.
func ParseWithOptions(filename string, opts ...OptFunc) (*ConfigParser, error) {
func ParseWithOptions(filename string, opts ...optFunc) (*ConfigParser, error) {
p := NewWithOptions(opts...)
data, err := os.ReadFile(filename)
if err != nil {
Expand Down Expand Up @@ -201,10 +196,22 @@ func (p *ConfigParser) SaveWithDelimiter(filename, delimiter string) error {
func (p *ConfigParser) ParseReader(in io.Reader) error {
reader := bufio.NewReader(in)
var lineNo int
var err error
var curSect *Section

for err == nil {
keyValue := regexp.MustCompile(
fmt.Sprintf(
`([^%[1]s\s][^%[1]s]*)\s*(?P<vi>[%[1]s]+)\s*(.*)$`,
p.opt.delimiters,
),
)
keyWNoValue := regexp.MustCompile(
fmt.Sprintf(
`([^%[1]s\s][^%[1]s]*)\s*((?P<vi>[%[1]s]+)\s*(.*)$)?`,
p.opt.delimiters,
),
)

for {
l, _, err := reader.ReadLine()
if err != nil {
break
Expand All @@ -216,30 +223,45 @@ func (p *ConfigParser) ParseReader(in io.Reader) error {
line := strings.TrimFunc(string(l), unicode.IsSpace) // ensures sectionHeader regex will match

// Skip comment lines and empty lines
if strings.HasPrefix(line, "#") || line == "" {
if p.opt.commentPrefixes.HasPrefix(line) || line == "" {
continue
}

if match := sectionHeader.FindStringSubmatch(line); len(match) > 0 {
section := match[1]
if section == defaultSectionName {
section := p.opt.inlineCommentPrefixes.Split(match[1])
if section == p.opt.defaultSection {
curSect = p.defaults
} else if _, present := p.config[section]; !present {
curSect = newSection(section)
p.config[section] = curSect
} else if p.opt.strict {
return fmt.Errorf("section %q error: %w", section, ErrAlreadyExist)
}
} else if match = keyValue.FindStringSubmatch(line); len(match) > 0 {
if curSect == nil {
return fmt.Errorf("missing section header: %d %s", lineNo, line)
}
key := strings.TrimSpace(match[1])
value := match[3]
if p.opt.strict {
if err := p.inOptions(key); err != nil {
return err
}
}

value := p.opt.inlineCommentPrefixes.Split(match[3])
if err := curSect.Add(key, value); err != nil {
return fmt.Errorf("failed to add %q = %q: %w", key, value, err)
}
} else if match = keyWNoValue.FindStringSubmatch(line); len(match) > 0 && p.opt.allowNoValue && curSect != nil {
} else if match = keyWNoValue.FindStringSubmatch(line); len(match) > 0 &&
p.opt.allowNoValue && curSect != nil {
key := strings.TrimSpace(match[1])
value := match[4]
if p.opt.strict {
if err := p.inOptions(key); err != nil {
return err
}
}

value := p.opt.inlineCommentPrefixes.Split(match[4])
if err := curSect.Add(key, value); err != nil {
return fmt.Errorf("failed to add %q = %q: %w", key, value, err)
}
Expand Down
51 changes: 13 additions & 38 deletions configparser_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package configparser_test

import (
"io"
"os"
"path"
"strings"
"testing"

"github.com/bigkevmcd/go-configparser"
. "gopkg.in/check.v1"
gc "gopkg.in/check.v1"

"io/ioutil"
"os"

"path"
"testing"
"github.com/bigkevmcd/go-configparser"
)

func Test(t *testing.T) { TestingT(t) }
Expand Down Expand Up @@ -74,7 +72,7 @@ func (s *ConfigParserSuite) TestSaveWithDelimiter(c *C) {
f, err := os.Open(tempfile)
c.Assert(err, IsNil)

data, err := ioutil.ReadAll(f)
data, err := io.ReadAll(f)
c.Assert(err, IsNil)
f.Close()
c.Assert(string(data), Equals, "[othersection]\nmyoption = myvalue\nnewoption = novalue\n\n[testing]\nmyoption = value\n\n")
Expand Down Expand Up @@ -106,45 +104,22 @@ func (s *ConfigParserSuite) TestSaveWithDelimiterAndDefaults(c *C) {
f, err := os.Open(tempfile)
c.Assert(err, IsNil)

data, err := ioutil.ReadAll(f)
data, err := io.ReadAll(f)
c.Assert(err, IsNil)
f.Close()
c.Assert(string(data), Equals, "[DEFAULT]\ntesting = value\n\n[othersection]\nmyoption = myvalue\nnewoption = novalue\n\n[testing]\nmyoption = value\n\n")
}

// ParseFromReader() parses the Config data from an io.Reader.
func (s *ConfigParserSuite) TestParseFromReader(c *gc.C) {
func (s *ConfigParserSuite) TestParseFromReader(c *C) {
parsed, err := configparser.ParseReader(strings.NewReader("[DEFAULT]\ntesting = value\n\n[othersection]\nmyoption = myvalue\nnewoption = novalue\nfinal = foo[bar]\n\n[testing]\nmyoption = value\nemptyoption\n\n"))
c.Assert(err, gc.IsNil)
c.Assert(err, IsNil)

result, err := parsed.Items("othersection")
c.Assert(err, gc.IsNil)
c.Assert(result, gc.DeepEquals, configparser.Dict{"myoption": "myvalue", "newoption": "novalue", "final": "foo[bar]"})
}

// If AllowNoValue is set to true, parser should recognize options without values.
func (s *ConfigParserSuite) TestParseReaderWithOptionsWNoValue(c *gc.C) {
parsed, err := configparser.ParseReaderWithOptions(
strings.NewReader("[empty]\noption\n\n"), configparser.AllowNoValue,
)
c.Assert(err, gc.IsNil)

ok, err := parsed.HasOption("empty", "option")
c.Assert(err, gc.IsNil)
c.Assert(ok, Equals, true)
}

func assertSuccessful(c *gc.C, err error) {
c.Assert(err, gc.IsNil)
c.Assert(err, IsNil)
c.Assert(result, DeepEquals, configparser.Dict{"myoption": "myvalue", "newoption": "novalue", "final": "foo[bar]"})
}

func (s *ConfigParserSuite) TestParseWithOptions(c *gc.C) {
parsed, err := configparser.ParseWithOptions(
"testdata/example.cfg", configparser.AllowNoValue,
)
c.Assert(err, gc.IsNil)

ok, err := parsed.HasOption("empty", "foo")
c.Assert(err, gc.IsNil)
c.Assert(ok, Equals, true)
func assertSuccessful(c *C, err error) {
c.Assert(err, IsNil)
}
Loading

0 comments on commit 509ec63

Please sign in to comment.