From df0061041fccc296213e6cc9e853eb144e3b5cd1 Mon Sep 17 00:00:00 2001 From: mappu Date: Sun, 12 Apr 2020 14:40:19 +1200 Subject: [PATCH] node: implement heredocs + string interpolation --- node.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/node.go b/node.go index 8688296..c6011f4 100644 --- a/node.go +++ b/node.go @@ -1465,6 +1465,19 @@ func (this *conversionState) convertNoFreeFloating(n_ node.Node) (string, error) return quoteGoString(rawValue), nil // Go source code quoting format + case *scalar.EncapsedStringPart: + // The n.Value that we receive here does not contain quotes at all + // TODO what happens with embedded escape characters? Did the parser + // remove them for us already or do we just have to assume we are in + // a double-quoted-equivalent situation? + return quoteGoString(n.Value), nil // Go source code quoting format + + case *scalar.Encapsed: + return this.convertEncapsedString(n.Parts) + + case *scalar.Heredoc: + return this.convertEncapsedString(n.Parts) + case *scalar.MagicConstant: // magic constants are case-insensitive switch strings.ToLower(n.Value) { @@ -1599,6 +1612,13 @@ func removeParens(expr string) string { return expr } +// asSingleExpression makes the expr into a single expression that is safe for +// embedding in a larger statement e.g. multiple string concatenation. +func asSingleExpression(expr string) string { + return `(` + removeParens(expr) + `)` + // TODO simplify this in cases where we can tell it is already a single expression e.g. quoted strings, number literals, variable literals, ... +} + func quoteGoString(s string) string { if !strings.Contains(s, "`") && strings.Count(s, "\n") >= 3 { // TODO make the heuristic configurable @@ -1611,6 +1631,27 @@ func quoteGoString(s string) string { } func (this *conversionState) convertEncapsedString(parts []node.Node) (string, error) { + // This is used for string interpolation: `echo "bar $ref";` + // And also for heredocs + // Child nodes are either EncapsedStringPart or expr.** + ret := make([]string, 0, len(parts)) + for _, part := range parts { + pp, err := this.convert(part) + if err != nil { + return "", parseErr{part, err} + } + + if _, ok := part.(*scalar.EncapsedStringPart); !ok { + // Subexpression - should probably parenthesise just in case + pp = asSingleExpression(pp) + } + + ret = append(ret, pp) + } + + return `(` + strings.Join(ret, ` + `) + `)`, nil +} + // resolveName turns a `*name.Name` node into a Go string. func (this *conversionState) resolveName(n node.Node) (string, error) { // TODO support namespace lookups