~nilium/go-ini

0f6534769635015767ee133b9d0be339a9d2e50d — Noel Cower 8 years ago d216844
Add support git-config like values

Basically, makes it possible to have lines like

         [thing "section"]
                  key = value1
                  key = value2

That evaluates to thing.section.key = [value1, value2].

As a result, this changes the result format to being more like
url.Values or what have you where it's a map of strings to slices of
strings. So, you can define multiple values for something.

Fairly useful, but weird.

Change-Id: I0feed4827069bbc1a878cbdae8560c51b38189c7
2 files changed, 153 insertions(+), 67 deletions(-)

M ini.go
M ini_test.go
M ini.go => ini.go +106 -36
@@ 7,6 7,7 @@ import (
	"io"
	"strings"
	"unicode"
	"unicode/utf8"
)

const (


@@ 43,9 44,9 @@ const defaultINICapacity int = 32
var PrefixSeparator string = "."

type iniParser struct {
	rb     []byte            // slice of rbfix for reading
	quoted [][]byte          // slice of quoted string parts
	result map[string]string // Resulting string map
	rb     []byte              // slice of rbfix for reading
	quoted [][]byte            // slice of quoted string parts
	result map[string][]string // Resulting string map
	prefix string
}



@@ 57,7 58,7 @@ func (p *iniParser) put(k, v string) {
	if len(p.prefix) > 0 {
		k = p.prefix + k
	}
	p.result[k] = v
	p.result[k] = append(p.result[k], v)
}

func advance(b []byte, from int, skip string) []byte {


@@ 79,6 80,64 @@ trySkipAgain:
	return b[from:]
}

func sanitizePrefix(prefix []byte) []byte {
	var out bytes.Buffer
	out.Grow(len(prefix))

	var (
		quoted        = false
		escaped       = false
		last     rune = -1
		chomp         = 0
		dropTail      = func() {
			if chomp > 0 {
				out.Truncate(out.Len() - chomp)
			}
			chomp = 0
		}
	)

	for _, r := range string(prefix) {
		if escaped {
			out.Write(escape(r))
			escaped = false
			goto write
		}

		if !quoted && last == r && r == ' ' {
			goto next
		} else if !quoted && r == ' ' {
			dropTail()
			chomp, _ = out.WriteString(PrefixSeparator)
			goto next
		} else if r == '"' {
			if quoted = !quoted; quoted {
				dropTail()
				chomp, _ = out.WriteString(PrefixSeparator)
				goto next
			} else {
				dropTail()
				chomp, _ = out.WriteString(PrefixSeparator)
				goto next
			}
		}

		if quoted && r == '\\' {
			escaped = true
			goto next
		}

	write:
		chomp = 0
		out.WriteRune(r)
	next:
		last = r
	}

	dropTail()
	return out.Bytes()
}

func (p *iniParser) readPrefix() error {
	p.rb = advance(p.rb, 0, anyWhitespace)



@@ 97,8 156,11 @@ func (p *iniParser) readPrefix() error {
	}

	prefix := bytes.Trim(p.rb[:end], whitespaceSansLine)
	prefix = sanitizePrefix(prefix)

	p.rb = p.rb[end+1:]
	prefixStr := string(prefix)

	if strings.ContainsAny(prefixStr, "\n") {
		return fmt.Errorf("Prefixes may not contain newlines (%q)", prefixStr)
	}


@@ 239,6 301,37 @@ var (
	escDQuote    = []byte{byte('"')}  // Double quote
)

func escape(b rune) []byte {
	var storage [4]byte
	var seq []byte
	switch b {
	case '0':
		seq = escNUL
	case 'a':
		seq = escBell
	case 'b':
		seq = escBackspace
	case 'f':
		seq = escFeed
	case 'n':
		seq = escNewline
	case 'r':
		seq = escCR
	case 't':
		seq = escHTab
	case 'v':
		seq = escVTab
	case '\\':
		seq = escSlash
	case '"':
		seq = escDQuote
	default:
		n := utf8.EncodeRune(storage[:], b)
		seq = storage[:n]
	}
	return seq
}

func (p *iniParser) readQuote() (string, error) {

	var (


@@ 246,44 339,21 @@ func (p *iniParser) readQuote() (string, error) {
		idx   int
		ch    byte
	)
	for ch != byte('"') {
	for ch != chQuote {
		idx = bytes.IndexAny(p.rb, `\"`)
		if idx == -1 {
			return ``, io.ErrUnexpectedEOF
		}
		ch = p.rb[idx]

		if ch == byte('\\') && len(p.rb) > idx+1 {
			var escape []byte
			switch p.rb[idx+1] {
			case byte('0'):
				escape = escNUL
			case byte('a'):
				escape = escBell
			case byte('b'):
				escape = escBackspace
			case byte('f'):
				escape = escFeed
			case byte('n'):
				escape = escNewline
			case byte('r'):
				escape = escCR
			case byte('t'):
				escape = escHTab
			case byte('v'):
				escape = escVTab
			case byte('\\'):
				escape = escSlash
			case byte('"'):
				escape = escDQuote
			default:
				escape = p.rb[idx+1 : idx+2]
			}
			parts = append(parts, p.rb[:idx], escape)
		if ch == chEscape && len(p.rb) > idx+1 {
			r, _ := utf8.DecodeRune(p.rb[idx+1:])
			seq := escape(r)
			parts = append(parts, p.rb[:idx], seq)
			idx += 1
		} else if ch == byte('\\') {
		} else if ch == chEscape {
			return ``, io.ErrUnexpectedEOF
		} else if ch == byte('"') && idx != 0 {
		} else if ch == chQuote && idx != 0 {
			parts = append(parts, p.rb[:idx])
		}
		p.rb = p.rb[idx+1:]


@@ 313,14 383,14 @@ func (p *iniParser) readQuote() (string, error) {
// Values enclosed in double quotes can contain newlines and escape characters
// supported by Go (\a, \b, \f, \n, \r, \t, \v, as well as escaped quotes and
// backslashes and \0 for the NUL character).
func ReadINI(b []byte, out map[string]string) (map[string]string, error) {
func ReadINI(b []byte, out map[string][]string) (map[string][]string, error) {
	var l int = len(b)
	if l == 0 {
		return out, nil
	}

	if out == nil {
		out = make(map[string]string, defaultINICapacity)
		out = make(map[string][]string, defaultINICapacity)
	}

	var p iniParser = iniParser{

M ini_test.go => ini_test.go +47 -31
@@ 1,9 1,12 @@
package ini

import "testing"
import (
	"reflect"
	"testing"
)

func TestReadINIEmpty(t *testing.T) {
	testReadINIMatching(t, "\n\t\n;empty\n\t\n\t", map[string]string{})
	testReadINIMatching(t, "\n\t\n;empty\n\t\n\t", map[string][]string{})
}

func TestReadINISectionSpaces(t *testing.T) {


@@ 13,7 16,7 @@ func TestReadINISectionSpaces(t *testing.T) {
	testReadINIError(t, "\n[\nnewline\nsection]\nk = v\n")

	// Good
	expected := map[string]string{`section ok.k`: `v`}
	expected := map[string][]string{`section.ok.k`: []string{`v`}}
	testReadINIMatching(t, `
		[section ok]
		k = v


@@ 21,17 24,30 @@ func TestReadINISectionSpaces(t *testing.T) {
		expected)
}

func TestReadQuotedMulti(t *testing.T) {
	src := `
	[foo "http://git.spiff.io"]
		insteadOf = left
		insteadOf = right
	`
	expected := map[string][]string{
		`foo.http://git.spiff.io.insteadOf`: []string{"left", "right"},
	}

	testReadINIMatching(t, src, expected)
}

func TestReadINISectionValueComment(t *testing.T) {
	testReadINIMatching(t,
		` key = ; `,
		map[string]string{
			`key`: ``,
		map[string][]string{
			`key`: []string{``},
		},
	)
}

func TestReadINIValueNewline(t *testing.T) {
	expected := map[string]string{`key`: ``}
	expected := map[string][]string{`key`: []string{``}}
	testReadINIMatching(t, " key = \n ", expected)
	testReadINIMatching(t, " key =\n ", expected)
	testReadINIMatching(t, " key=\n ", expected)


@@ 43,7 59,7 @@ func TestReadINIValueNewline(t *testing.T) {
}

func TestReadINIValueSimple(t *testing.T) {
	expected := map[string]string{`key`: `value`}
	expected := map[string][]string{`key`: []string{`value`}}
	// In the interest of being possibly unusually thorough.
	testReadINIMatching(t, " key = value ", expected)
	testReadINIMatching(t, " key=value ", expected)


@@ 57,8 73,8 @@ func TestReadINIValueSimple(t *testing.T) {
}

func TestReadINIFlagSimple(t *testing.T) {
	expected := map[string]string{
		`key`: True,
	expected := map[string][]string{
		`key`: []string{True},
	}

	testReadINIMatching(t, "key", expected)


@@ 80,8 96,8 @@ func TestReadINIFlagSimple(t *testing.T) {
}

func TestReadINIUnicode(t *testing.T) {
	expected := map[string]string{
		"-_kŭjəl_-": "käkə-pō",
	expected := map[string][]string{
		"-_kŭjəl_-": []string{"käkə-pō"},
	}
	testReadINIMatching(t, "-_kŭjəl_- = käkə-pō", expected)
	testReadINIMatching(t, "-_kŭjəl_-=käkə-pō", expected)


@@ 97,10 113,10 @@ func TestReadINIUnicode(t *testing.T) {
}

func TestReadMultiline(t *testing.T) {
	expected := map[string]string{
		`foo`: True,
		`bar`: ``,
		`baz`: `value`,
	expected := map[string][]string{
		`foo`: []string{True},
		`bar`: []string{``},
		`baz`: []string{`value`},
	}
	testReadINIMatching(t, "foo\nbar=;\nbaz=value", expected)
	testReadINIMatching(t, "foo;\nbar=\nbaz=value", expected)


@@ 110,9 126,9 @@ func TestReadMultiline(t *testing.T) {
}

func TestReadQuoted(t *testing.T) {
	expected := map[string]string{
		`normal`:  `  a thing  `,
		`escaped`: string([]byte{0}) + "\a\b\f\n\r\t\v\\\"jkl;",
	expected := map[string][]string{
		`normal`:  []string{`  a thing  `},
		`escaped`: []string{string([]byte{0}) + "\a\b\f\n\r\t\v\\\"jkl;"},
	}

	// In the interest of being possibly unusually thorough.


@@ 158,23 174,23 @@ lmn
no_prefix = this has no prefix
`

	keys := map[string]string{
		`a`:              "5\n\n",
		`prefix.foo.a`:   `value of "a"`,
		`prefix.foo.b`:   `unhandled`,
		`prefix.foo.c`:   `1`,
		`prefix.bar.d`:   ``,
		`prefix.bar.efg`: ``,
		`prefix.bar.hij`: `1`,
		`prefix.bar.k`:   `1`,
		`prefix.bar.lmn`: `1`,
		`no_prefix`:      `this has no prefix`,
	keys := map[string][]string{
		`a`:              []string{"5\n\n"},
		`prefix.foo.a`:   []string{`value of "a"`},
		`prefix.foo.b`:   []string{`unhandled`},
		`prefix.foo.c`:   []string{`1`},
		`prefix.bar.d`:   []string{``},
		`prefix.bar.efg`: []string{``},
		`prefix.bar.hij`: []string{`1`},
		`prefix.bar.k`:   []string{`1`},
		`prefix.bar.lmn`: []string{`1`},
		`no_prefix`:      []string{`this has no prefix`},
	}

	testReadINIMatching(t, s, keys)
}

func testReadINIMatching(t *testing.T, b string, expected map[string]string) {
func testReadINIMatching(t *testing.T, b string, expected map[string][]string) {
	actual, err := ReadINI([]byte(b), nil)

	if err != nil {


@@ 193,7 209,7 @@ func testReadINIMatching(t *testing.T, b string, expected map[string]string) {
			t.Errorf("Result map does not contain key %q", k)
		}

		if v != mv {
		if !reflect.DeepEqual(v, mv) {
			t.Errorf("Value of %q in result map %q != (expected) %q", k, mv, v)
		}
	}