Add a function to quote HCL strings

The strings we have in the variables may contain escaped double-quotes,
which have already been parsed and had the `\`s removed. We need to
re-escape these, but only if we are in the outer string and not inside an
interpolation.
This commit is contained in:
James Bardin 2016-07-26 20:37:41 -04:00
parent 648fff9ba1
commit 8038e60a20
3 changed files with 113 additions and 24 deletions

View File

@ -29,10 +29,14 @@ func encodeHCL(i interface{}) ([]byte, error) {
// use the real hcl parser to verify our output, and format it canonically
hcl, err = printer.Format(fakeAssignment)
if err != nil {
return nil, err
}
// now strip that first assignment off
eq := regexp.MustCompile(`=\s+`).FindIndex(hcl)
return hcl[eq[1]:], err
return hcl[eq[1]:], nil
}
type encodeState struct {
@ -98,11 +102,6 @@ func (e *encodeState) encodeMap(m map[string]interface{}) error {
return nil
}
func (e *encodeState) encodeString(s string) error {
_, err := fmt.Fprintf(e, "%q", s)
return err
}
func (e *encodeState) encodeInt(i interface{}) error {
_, err := fmt.Fprintf(e, "%d", i)
return err
@ -112,3 +111,81 @@ func (e *encodeState) encodeFloat(f interface{}) error {
_, err := fmt.Fprintf(e, "%f", f)
return err
}
func (e *encodeState) encodeString(s string) error {
e.Write(quoteHCLString(s))
return nil
}
// Quote an HCL string, which may contain interpolations.
// Since the string was already parsed from HCL, we have to assume the
// required characters are sanely escaped. All we need to do is escape double
// quotes in the string, unless they are in an interpolation block.
func quoteHCLString(s string) []byte {
out := make([]byte, 0, len(s))
out = append(out, '"')
// our parse states
var (
outer = 1 // the starting state for the string
dollar = 2 // look for '{' in the next character
interp = 3 // inside an interpolation block
escape = 4 // take the next character and pop back to prev state
)
// we could have nested interpolations
state := stack{}
state.push(outer)
for i := 0; i < len(s); i++ {
switch state.peek() {
case outer:
switch s[i] {
case '"':
out = append(out, '\\')
case '$':
state.push(dollar)
case '\\':
state.push(escape)
}
case dollar:
state.pop()
switch s[i] {
case '{':
state.push(interp)
case '\\':
state.push(escape)
}
case interp:
switch s[i] {
case '}':
state.pop()
}
case escape:
state.pop()
}
out = append(out, s[i])
}
out = append(out, '"')
return out
}
type stack []int
func (s *stack) push(i int) {
*s = append(*s, i)
}
func (s *stack) pop() int {
last := len(*s) - 1
i := (*s)[last]
*s = (*s)[:last]
return i
}
func (s *stack) peek() int {
return (*s)[len(*s)-1]
}

View File

@ -397,21 +397,29 @@ func TestPush_tfvars(t *testing.T) {
// make sure these dind't go missing for some reason
for k, v := range variables {
if !reflect.DeepEqual(client.UpsertOptions.Variables[k], v) {
t.Fatalf("bad: %#v", client.UpsertOptions.Variables)
t.Fatalf("bad: %#v", client.UpsertOptions.Variables[k])
}
}
//now check TFVVars
//now check TFVars
tfvars := []atlas.TFVar{
{"bar", "foo", false},
{"baz", "{\n A = \"a\"\n B = \"b\"\n}\n", true},
{"fob", "[\"a\", \"b\", \"c\"]\n", true},
{"baz", `{
A = "a"
B = "b"
interp = "${file("t.txt")}"
}
`, true},
{"fob", `["a", "b", "c", "quotes \"in\" quotes"]` + "\n", true},
{"foo", "bar", false},
}
if !reflect.DeepEqual(client.UpsertOptions.TFVars, tfvars) {
t.Fatalf("bad tf_vars: %#v", client.UpsertOptions.TFVars)
for i, expected := range tfvars {
got := client.UpsertOptions.TFVars[i]
if got != expected {
t.Logf("%2d expected: %#v", i, expected)
t.Logf(" got: %#v", got)
}
}
}
@ -582,9 +590,10 @@ func pushTFVars() map[string]interface{} {
"foo": "bar",
"bar": "foo",
"baz": map[string]interface{}{
"A": "a",
"B": "b",
"A": "a",
"B": "b",
"interp": `${file("t.txt")}`,
},
"fob": []interface{}{"a", "b", "c"},
"fob": []interface{}{"a", "b", "c", `quotes "in" quotes`},
}
}

View File

@ -1,21 +1,24 @@
variable "foo" {}
variable "bar" {}
variable "baz" {
type = "map"
default = {
"A" = "a"
"B" = "b"
}
type = "map"
default = {
"A" = "a"
"B" = "b"
interp = "${file("t.txt")}"
}
}
variable "fob" {
type = "list"
default = ["a", "b", "c"]
type = "list"
default = ["a", "b", "c", "quotes \"in\" quotes"]
}
resource "test_instance" "foo" {}
atlas {
name = "foo"
name = "foo"
}