Merge pull request #91 from hashicorp/f-indented-heredocs
Add support for indented HEREDOC terminators
This commit is contained in:
commit
2604f3bda7
@ -91,6 +91,16 @@ func TestDecode_interface(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
map[string]interface{}{"foo": testhelper.Unix2dos("bar\nbaz\n")},
|
map[string]interface{}{"foo": testhelper.Unix2dos("bar\nbaz\n")},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"multiline_indented.hcl",
|
||||||
|
false,
|
||||||
|
map[string]interface{}{"foo": testhelper.Unix2dos(" bar\n baz\n")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiline_no_hanging_indent.hcl",
|
||||||
|
false,
|
||||||
|
map[string]interface{}{"foo": testhelper.Unix2dos(" baz\n bar\n foo\n")},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"multiline_no_eof.hcl",
|
"multiline_no_eof.hcl",
|
||||||
false,
|
false,
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@ -376,7 +377,7 @@ func (s *Scanner) scanExponent(ch rune) rune {
|
|||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanHeredoc scans a heredoc string.
|
// scanHeredoc scans a heredoc string
|
||||||
func (s *Scanner) scanHeredoc() {
|
func (s *Scanner) scanHeredoc() {
|
||||||
// Scan the second '<' in example: '<<EOF'
|
// Scan the second '<' in example: '<<EOF'
|
||||||
if s.next() != '<' {
|
if s.next() != '<' {
|
||||||
@ -389,6 +390,12 @@ func (s *Scanner) scanHeredoc() {
|
|||||||
|
|
||||||
// Scan the identifier
|
// Scan the identifier
|
||||||
ch := s.next()
|
ch := s.next()
|
||||||
|
|
||||||
|
// Indented heredoc syntax
|
||||||
|
if ch == '-' {
|
||||||
|
ch = s.next()
|
||||||
|
}
|
||||||
|
|
||||||
for isLetter(ch) || isDigit(ch) {
|
for isLetter(ch) || isDigit(ch) {
|
||||||
ch = s.next()
|
ch = s.next()
|
||||||
}
|
}
|
||||||
@ -414,6 +421,17 @@ func (s *Scanner) scanHeredoc() {
|
|||||||
|
|
||||||
// Read the identifier
|
// Read the identifier
|
||||||
identBytes := s.src[offs : s.srcPos.Offset-s.lastCharLen]
|
identBytes := s.src[offs : s.srcPos.Offset-s.lastCharLen]
|
||||||
|
if len(identBytes) == 0 {
|
||||||
|
s.err("zero-length heredoc anchor")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var identRegexp *regexp.Regexp
|
||||||
|
if identBytes[0] == '-' {
|
||||||
|
identRegexp = regexp.MustCompile(fmt.Sprintf(`[[:space:]]*%s\z`, identBytes[1:]))
|
||||||
|
} else {
|
||||||
|
identRegexp = regexp.MustCompile(fmt.Sprintf(`[[:space:]]*%s\z`, identBytes))
|
||||||
|
}
|
||||||
|
|
||||||
// Read the actual string value
|
// Read the actual string value
|
||||||
lineStart := s.srcPos.Offset
|
lineStart := s.srcPos.Offset
|
||||||
@ -422,12 +440,11 @@ func (s *Scanner) scanHeredoc() {
|
|||||||
|
|
||||||
// Special newline handling.
|
// Special newline handling.
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
// Math is fast, so we first compare the byte counts to
|
// Math is fast, so we first compare the byte counts to see if we have a chance
|
||||||
// see if we have a chance of seeing the same identifier. If those
|
// of seeing the same identifier - if the length is less than the number of bytes
|
||||||
// match, then we compare the string values directly.
|
// in the identifier, this cannot be a valid terminator.
|
||||||
lineBytesLen := s.srcPos.Offset - s.lastCharLen - lineStart
|
lineBytesLen := s.srcPos.Offset - s.lastCharLen - lineStart
|
||||||
if lineBytesLen == len(identBytes) &&
|
if lineBytesLen >= len(identBytes) && identRegexp.Match(s.src[lineStart:s.srcPos.Offset-s.lastCharLen]) {
|
||||||
bytes.Equal(identBytes, s.src[lineStart:s.srcPos.Offset-s.lastCharLen]) {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/hcl/token"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/hcl/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
var f100 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
var f100 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||||
@ -377,6 +378,14 @@ func TestRealExample(t *testing.T) {
|
|||||||
Main interface
|
Main interface
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
device_index = 1
|
||||||
|
description = <<-EOF
|
||||||
|
Outer text
|
||||||
|
Indented text
|
||||||
|
EOF
|
||||||
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
literals := []struct {
|
literals := []struct {
|
||||||
@ -435,6 +444,15 @@ EOF
|
|||||||
{token.ASSIGN, `=`},
|
{token.ASSIGN, `=`},
|
||||||
{token.HEREDOC, "<<EOF\nMain interface\nEOF\n"},
|
{token.HEREDOC, "<<EOF\nMain interface\nEOF\n"},
|
||||||
{token.RBRACE, `}`},
|
{token.RBRACE, `}`},
|
||||||
|
{token.IDENT, `network_interface`},
|
||||||
|
{token.LBRACE, `{`},
|
||||||
|
{token.IDENT, `device_index`},
|
||||||
|
{token.ASSIGN, `=`},
|
||||||
|
{token.NUMBER, `1`},
|
||||||
|
{token.IDENT, `description`},
|
||||||
|
{token.ASSIGN, `=`},
|
||||||
|
{token.HEREDOC, "<<-EOF\n\t\t\tOuter text\n\t\t\t\tIndented text\n\t\t\tEOF\n"},
|
||||||
|
{token.RBRACE, `}`},
|
||||||
{token.RBRACE, `}`},
|
{token.RBRACE, `}`},
|
||||||
{token.EOF, ``},
|
{token.EOF, ``},
|
||||||
}
|
}
|
||||||
@ -447,7 +465,7 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
if l.literal != tok.Text {
|
if l.literal != tok.Text {
|
||||||
t.Errorf("got: %s want %s\n", tok, l.literal)
|
t.Errorf("got:\n%+v\n%s\n want:\n%+v\n%s\n", []byte(tok.String()), tok, []byte(l.literal), l.literal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,13 +142,7 @@ func (t Token) Value() interface{} {
|
|||||||
case IDENT:
|
case IDENT:
|
||||||
return t.Text
|
return t.Text
|
||||||
case HEREDOC:
|
case HEREDOC:
|
||||||
// We need to find the end of the marker
|
return unindentHeredoc(t.Text)
|
||||||
idx := strings.IndexByte(t.Text, '\n')
|
|
||||||
if idx == -1 {
|
|
||||||
panic("heredoc doesn't contain newline")
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(t.Text[idx+1 : len(t.Text)-idx+1])
|
|
||||||
case STRING:
|
case STRING:
|
||||||
// Determine the Unquote method to use. If it came from JSON,
|
// Determine the Unquote method to use. If it came from JSON,
|
||||||
// then we need to use the built-in unquote since we have to
|
// then we need to use the built-in unquote since we have to
|
||||||
@ -168,3 +162,53 @@ func (t Token) Value() interface{} {
|
|||||||
panic(fmt.Sprintf("unimplemented Value for type: %s", t.Type))
|
panic(fmt.Sprintf("unimplemented Value for type: %s", t.Type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unindentHeredoc returns the string content of a HEREDOC if it is started with <<
|
||||||
|
// and the content of a HEREDOC with the hanging indent removed if it is started with
|
||||||
|
// a <<-, and the terminating line is at least as indented as the least indented line.
|
||||||
|
func unindentHeredoc(heredoc string) string {
|
||||||
|
// We need to find the end of the marker
|
||||||
|
idx := strings.IndexByte(heredoc, '\n')
|
||||||
|
if idx == -1 {
|
||||||
|
panic("heredoc doesn't contain newline")
|
||||||
|
}
|
||||||
|
|
||||||
|
unindent := heredoc[2] == '-'
|
||||||
|
|
||||||
|
// We can optimize if the heredoc isn't marked for indentation
|
||||||
|
if !unindent {
|
||||||
|
return string(heredoc[idx+1 : len(heredoc)-idx+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to unindent each line based on the indentation level of the marker
|
||||||
|
lines := strings.Split(string(heredoc[idx+1:len(heredoc)-idx+2]), "\n")
|
||||||
|
whitespacePrefix := lines[len(lines)-1]
|
||||||
|
|
||||||
|
isIndented := true
|
||||||
|
for _, v := range lines {
|
||||||
|
if strings.HasPrefix(v, whitespacePrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isIndented = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all lines are not at least as indented as the terminating mark, return the
|
||||||
|
// heredoc as is, but trim the leading space from the marker on the final line.
|
||||||
|
if !isIndented {
|
||||||
|
return strings.TrimRight(string(heredoc[idx+1:len(heredoc)-idx+1]), " \t")
|
||||||
|
}
|
||||||
|
|
||||||
|
unindentedLines := make([]string, len(lines))
|
||||||
|
for k, v := range lines {
|
||||||
|
if k == len(lines)-1 {
|
||||||
|
unindentedLines[k] = ""
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
unindentedLines[k] = strings.TrimPrefix(v, whitespacePrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(unindentedLines, "\n")
|
||||||
|
}
|
||||||
|
4
test-fixtures/multiline_indented.hcl
Normal file
4
test-fixtures/multiline_indented.hcl
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
foo = <<-EOF
|
||||||
|
bar
|
||||||
|
baz
|
||||||
|
EOF
|
5
test-fixtures/multiline_no_hanging_indent.hcl
Normal file
5
test-fixtures/multiline_no_hanging_indent.hcl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
foo = <<-EOF
|
||||||
|
baz
|
||||||
|
bar
|
||||||
|
foo
|
||||||
|
EOF
|
Loading…
x
Reference in New Issue
Block a user