Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

discover: new config parser #15

Merged
merged 1 commit into from
Oct 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
ip addresses of nodes in cloud environments based on meta information
like tags provided by the environment.

The configuration for the providers is provided as a list of `key=val
key=val ...` tuples where the values can be URL encoded. The provider is
determined through the `provider` key. Effectively, only spaces have to
be encoded with a `+` and on the command line you have to observe
quoting rules with your shell.
The configuration for the providers is provided as a list of `key=val key=val
...` tuples. If either the key or the value contains a space (` `), a backslash
(`\`) or double quotes (`"`) then it needs to be quoted with double quotes.
Within a quoted string you can use the backslash to escape double quotes or the
backslash itself, e.g. `key=val "some key"="some value"`

Duplicate keys are reported as error and the provider is determined through the
`provider` key.

### Supported Providers

The following cloud providers have implementations in the go-discover/provider
sub packages. Additional providers can be added through the [Register](https://godoc.org/github.com/hashicorp/go-discover#Register)
sub packages. Additional providers can be added through the
[Register](https://godoc.org/github.com/hashicorp/go-discover#Register)
function.

* Amazon AWS [Config options](http://godoc.org/github.com/hashicorp/go-discover/provider/aws)
Expand Down
213 changes: 189 additions & 24 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,21 @@ package discover

import (
"fmt"
"net/url"
"sort"
"strconv"
"strings"
)

// Config stores key/value pairs for the discovery
// functions to use.
type Config map[string]string

// Parse parses a "key=val key=val ..." string into
// a config map. Values are URL escaped.
// Parse parses a "key=val key=val ..." string into a config map. Keys
// and values which contain spaces, backslashes or double-quotes must be
// quoted with double quotes. Use the backslash to escape special
// characters within quoted strings, e.g. "some key"="some \"value\"".
func Parse(s string) (Config, error) {
s = strings.TrimSpace(s)
if s == "" {
return nil, nil
}

c := Config{}
for _, v := range strings.Fields(s) {
p := strings.SplitN(v, "=", 2)
if len(p) != 2 {
return nil, fmt.Errorf("invalid format: %s", v)
}
key := p[0]
val, err := url.QueryUnescape(p[1])
if err != nil {
return nil, fmt.Errorf("invalid format: %s", v)
}
c[key] = val
}
return c, nil
return parse(s)
}

// String formats a config map into the "key=val key=val ..."
Expand All @@ -48,14 +32,195 @@ func (c Config) String() string {
sort.Strings(keys)
keys = append([]string{"provider"}, keys...)

quote := func(s string) string {
if strings.ContainsAny(s, ` "\`) {
return strconv.Quote(s)
}
return s
}

var vals []string
for _, k := range keys {
v := c[k]
if v == "" {
continue
}
v = k + "=" + url.QueryEscape(v)
vals = append(vals, v)
vals = append(vals, quote(k)+"="+quote(v))
}
return strings.Join(vals, " ")
}

func parse(in string) (Config, error) {
m := Config{}
s := []rune(strings.TrimSpace(in))
state := stateKey
key := ""
for {
// exit condition
if len(s) == 0 {
break
}

// get the next token
item, val, n := lex(s)
s = s[n:]
fmt.Printf("parse: state: %q item: %q val: '%s' n: %d rest: '%s'\n", state, item, val, n, string(s))

switch state {

case stateKey:
switch item {
case itemText:
key = val
if _, exists := m[key]; exists {
return nil, fmt.Errorf("%s: duplicate key", key)
}
state = stateEqual
default:
return nil, fmt.Errorf("%s: %s", key, val)
}

case stateEqual:
switch item {
case itemEqual:
state = stateVal
default:
return nil, fmt.Errorf("%s: missing '='", key)
}

case stateVal:
switch item {
case itemText:
m[key] = val
state = stateKey
case itemError:
return nil, fmt.Errorf("%s: %s", key, val)
default:
return nil, fmt.Errorf("%s: missing value", key)
}
}
}

//fmt.Printf("parse: state: %q rest: '%s'\n", state, string(s))
switch state {
case stateEqual:
return nil, fmt.Errorf("%s: missing '='", key)
case stateVal:
return nil, fmt.Errorf("%s: missing value", key)
}
if len(m) == 0 {
return nil, nil
}
return m, nil
}

type itemType string

const (
itemText itemType = "TEXT"
itemEqual = "EQUAL"
itemError = "ERROR"
)

func (t itemType) String() string {
return string(t)
}

type state string

const (

// lexer states
stateStart state = "start"
stateEqual = "equal"
stateText = "text"
stateQText = "qtext"
stateQTextEnd = "qtextend"
stateQTextEsc = "qtextesc"

// parser states
stateKey = "key"
stateVal = "val"
)

func lex(s []rune) (itemType, string, int) {
isEqual := func(r rune) bool { return r == '=' }
isEscape := func(r rune) bool { return r == '\\' }
isQuote := func(r rune) bool { return r == '"' }
isSpace := func(r rune) bool { return r == ' ' }

unquote := func(r []rune) (string, error) {
v := strings.TrimSpace(string(r))
return strconv.Unquote(v)
}

var quote rune
state := stateStart
for i, r := range s {
// fmt.Println("lex:", "i:", i, "r:", string(r), "state:", string(state), "head:", string(s[:i]), "tail:", string(s[i:]))
switch state {
case stateStart:
switch {
case isSpace(r):
// state = stateStart
case isEqual(r):
state = stateEqual
case isQuote(r):
quote = r
state = stateQText
default:
state = stateText
}

case stateEqual:
return itemEqual, "", i

case stateText:
switch {
case isEqual(r) || isSpace(r):
v := strings.TrimSpace(string(s[:i]))
return itemText, v, i
default:
// state = stateText
}

case stateQText:
switch {
case r == quote:
state = stateQTextEnd
case isEscape(r):
state = stateQTextEsc
default:
// state = stateQText
}

case stateQTextEsc:
state = stateQText

case stateQTextEnd:
v, err := unquote(s[:i])
if err != nil {
return itemError, err.Error(), i
}
return itemText, v, i
}
}

// fmt.Println("lex:", "state:", string(state))
switch state {
case stateEqual:
return itemEqual, "", len(s)
case stateQText:
return itemError, "unbalanced quotes", len(s)
case stateQTextEsc:
return itemError, "unterminated escape sequence", len(s)
case stateQTextEnd:
v, err := unquote(s)
if err != nil {
return itemError, err.Error(), len(s)
}
return itemText, v, len(s)
default:
return itemText, strings.TrimSpace(string(s)), len(s)
}
}
33 changes: 24 additions & 9 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,33 @@ func TestConfigParse(t *testing.T) {
c Config
err error
}{
{"", nil, nil},
{" ", nil, nil},
{"provider=aws foo", nil, errors.New(`invalid format: foo`)},
{"project_name=Test zone_pattern=us-(?west|east).%2b tag_value=consul+server credentials_file=xxx",
map[string]string{
// happy flows
{``, nil, nil},
{`key=a`, Config{"key": "a"}, nil},
{`key=a key2=b`, Config{"key": "a", "key2": "b"}, nil},
{`key=a+b key2=c/d`, Config{"key": "a+b", "key2": "c/d"}, nil},
{` key=a key2=b `, Config{"key": "a", "key2": "b"}, nil},
{` key = a key2 = b `, Config{"key": "a", "key2": "b"}, nil},
{` "k e y" = "a \" b" key2=c`, Config{"k e y": "a \" b", "key2": "c"}, nil},

{`provider=aws foo`, nil, errors.New(`foo: missing '='`)},
{`project_name=Test zone_pattern=us-(?west|east).+ tag_value="consul server" credentials_file=xxx`,
Config{
"project_name": "Test",
"zone_pattern": "us-(?west|east).+",
"tag_value": "consul server",
"credentials_file": "xxx",
},
nil,
},

// errors
{`key`, nil, errors.New(`key: missing '='`)},
{`key=`, nil, errors.New(`key: missing value`)},
{`key="a`, nil, errors.New(`key: unbalanced quotes`)},
{`key="\`, nil, errors.New(`key: unterminated escape sequence`)},
{`key=a key=b`, nil, errors.New(`key: duplicate key`)},
{`key key2`, nil, errors.New(`key: missing '='`)},
}

for _, tt := range tests {
Expand All @@ -43,10 +58,10 @@ func TestConfigString(t *testing.T) {
tests := []struct {
in, out string
}{
{"", ""},
{" ", ""},
{"b=c a=b", "a=b b=c"},
{"a=b provider=foo x=y", "provider=foo a=b x=y"},
{``, ``},
{` `, ``},
{`b=c "a a"="b b"`, `"a a"="b b" b=c`},
{`a=b provider=foo x=y`, `provider=foo a=b x=y`},
}

for _, tt := range tests {
Expand Down