~nilium/go-ini

c62a4716e081bd4efa21a74335f3d78593c4813e — Noel Cower 8 years ago 0f65347
Add raw quotes

These differ from Go's raw quotes in that you can escape the quote mark
by using `` -- this seemed reasonable enough to support.

Should consider adding the same escape sequence to regular quotes.

Ideally, this should be slightly less annoying than dealing with
escaping quotes and backslashes, but if your values have control
characters in them, you'll need to endure the regular quotes.

This breaks existing uses of `s in INI files, since those are currently
considered a regular, unquoted value. I'm personally OK with this
'cause it isn't an issue for me and nobody else is using this package
right now (that I'm aware of).

Change-Id: Ie9f52e645445bc0503c48728d005ac47f539a78d
2 files changed, 52 insertions(+), 3 deletions(-)

M ini.go
M ini_test.go
M ini.go => ini.go +40 -1
@@ 15,6 15,7 @@ const (
	chPrefixEnd   = byte(']')
	chSpace       = byte(' ')
	chQuote       = byte('"')
	chRawQuote    = byte('`')
	chTab         = byte('\t')
	chLine        = byte('\n')
	chFeed        = byte('\r') // ignored outside of strings.


@@ 265,6 266,9 @@ func (p *iniParser) readValue() (string, error) {
	case chQuote:
		p.rb = p.rb[idx+1:]
		return p.readQuote()
	case chRawQuote:
		p.rb = p.rb[idx+1:]
		return p.readRawQuote()
	case chComment:
		fallthrough
	case chLine:


@@ 332,10 336,45 @@ func escape(b rune) []byte {
	return seq
}

func (p *iniParser) readRawQuote() (string, error) {
	var (
		parts = p.quoted[:0]
		idx   int
		ch    byte
	)

	for ch != chRawQuote {
		idx = bytes.IndexByte(p.rb, chRawQuote)
		if idx == -1 {
			return ``, io.ErrUnexpectedEOF
		}
		ch = p.rb[idx]

		if ch == chRawQuote && len(p.rb) > idx+1 && p.rb[idx+1] == chRawQuote {
			parts = append(parts, p.rb[:idx+1])
			idx += 1
			ch = 0 // Reset ch since it was escaped.
		} else if ch == chRawQuote && idx != 0 {
			parts = append(parts, p.rb[:idx])
		}
		p.rb = p.rb[idx+1:]
	}
	p.quoted = parts[:0]

	switch len(parts) {
	case 0:
		return ``, nil
	case 1:
		return string(parts[0]), nil
	default:
		return string(bytes.Join(parts, nil)), nil
	}
}

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

	var (
		parts = p.quoted
		parts = p.quoted[:0]
		idx   int
		ch    byte
	)

M ini_test.go => ini_test.go +12 -2
@@ 129,6 129,8 @@ func TestReadQuoted(t *testing.T) {
	expected := map[string][]string{
		`normal`:  []string{`  a thing  `},
		`escaped`: []string{string([]byte{0}) + "\a\b\f\n\r\t\v\\\"jkl;"},
		`raw`:     []string{"a\n`b`\nc\n"},
		`quote`:   []string{"`", `"`},
	}

	// In the interest of being possibly unusually thorough.


@@ 136,13 138,19 @@ func TestReadQuoted(t *testing.T) {
		; Test a fairly normal string
		normal	= "  a thing  "
		escaped	= "\0\a\b\f\n\r\t\v\\\"\j\k\l\;"
		`, expected)
		raw	= `+"`a\n``b``\nc\n`"+`
		quote   = `+"````"+`
		quote   = `+"`\"`",
		expected)
	testReadINIMatching(t, `
		; Test one with inline characters that could be escaped.
		normal	= "  a thing  "
		escaped	= "\0\a\b\f
\r	\v\\\"\j\k\l\;" ; Tests escaping non-escape characters as themselves
		`, expected)
		raw	= `+"`a\n``b``\nc\n`"+`
		quote   = `+"````"+`
		quote   = `+"`\"`",
		expected)

	testReadINIError(t, `unterminated = "`)
	testReadINIError(t, `unexpected = """`)


@@ 193,6 201,8 @@ no_prefix = this has no prefix
func testReadINIMatching(t *testing.T, b string, expected map[string][]string) {
	actual, err := ReadINI([]byte(b), nil)

	t.Logf("Parsing:\n%s", b)

	if err != nil {
		t.Error("Error reading INI:", err)
	}