Skip to content

Commit

Permalink
repl: Display multi-line strings as heredocs
Browse files Browse the repository at this point in the history
The console and output formatter previously displayed multi-line strings
with escaped newlines, e.g. `"hello\nworld\n"`. While this is a valid
way to write the HCL string, it is not as common or as readable as using
the heredoc syntax, e.g.

<<EOF
hello
world
EOF

This commit adds heredoc detection and display to this formatter,
including support for indented heredocs for nested multi-line strings.
This change affects the apply, console, and output sub-commands.
  • Loading branch information
alisdair committed Nov 26, 2020
1 parent 111825d commit 4e7607d
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 3 deletions.
55 changes: 53 additions & 2 deletions repl/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ func FormatValue(v cty.Value, indent int) string {
case ty.IsPrimitiveType():
switch ty {
case cty.String:
// FIXME: If it's a multi-line string, better to render it using
// HEREDOC-style syntax.
if formatted, isMultiline := formatMultilineString(v, indent); isMultiline {
return formatted
}
return strconv.Quote(v.AsString())
case cty.Number:
bf := v.AsBigFloat()
Expand Down Expand Up @@ -75,6 +76,56 @@ func FormatValue(v cty.Value, indent int) string {
return fmt.Sprintf("%#v", v)
}

func formatMultilineString(v cty.Value, indent int) (string, bool) {
str := v.AsString()
lines := strings.Split(str, "\n")
if len(lines) < 2 {
return "", false
}

// If the value is indented, we use the indented form of heredoc for readability.
operator := "<<"
if indent > 0 {
operator = "<<-"
}

// Default delimiter is "End Of Text" by convention
delimiter := "EOT"

OUTER:
for {
// Check if any of the lines are in conflict with the delimiter. The
// parser allows leading and trailing whitespace, so we must remove it
// before comparison.
for _, line := range lines {
// If the delimiter matches a line, extend it and start again
if strings.TrimSpace(line) == delimiter {
delimiter = delimiter + "_"
continue OUTER
}
}

// None of the lines match the delimiter, so we're ready
break
}

// Write the heredoc, with indentation as appropriate.
var buf strings.Builder

buf.WriteString(operator)
buf.WriteString(delimiter)
for _, line := range lines {
buf.WriteByte('\n')
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(line)
}
buf.WriteByte('\n')
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(delimiter)

return buf.String(), true
}

func formatMappingValue(v cty.Value, indent int) string {
var buf strings.Builder
count := 0
Expand Down
23 changes: 22 additions & 1 deletion repl/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,28 @@ func TestFormatValue(t *testing.T) {
},
{
cty.StringVal("hello\nworld"),
`"hello\nworld"`, // Ideally we'd use heredoc syntax here for better readability, but we don't yet
`<<EOT
hello
world
EOT`,
},
{
cty.StringVal("EOR\nEOS\nEOT\nEOU"),
`<<EOT_
EOR
EOS
EOT
EOU
EOT_`,
},
{
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("boop\nbeep")}),
`{
"foo" = <<-EOT
boop
beep
EOT
}`,
},
{
cty.Zero,
Expand Down

0 comments on commit 4e7607d

Please sign in to comment.