]> Cypherpunks repositories - gostls13.git/commitdiff
text/template: add variable assignments
authorDaniel Martí <mvdan@mvdan.cc>
Sun, 17 Dec 2017 13:23:40 +0000 (13:23 +0000)
committerDaniel Martí <mvdan@mvdan.cc>
Wed, 4 Apr 2018 15:51:56 +0000 (15:51 +0000)
Variables can be declared and shadowing is supported, but modifying
existing variables via assignments was not available.

This meant that modifying a variable from a nested block was not
possible:

{{ $v := "init" }}
{{ if true }}
{{ $v := "changed" }}
{{ end }}
v: {{ $v }} {{/* "init" */}}

Introduce the "=" assignment token, such that one can now do:

{{ $v := "init" }}
{{ if true }}
{{ $v = "changed" }}
{{ end }}
v: {{ $v }} {{/* "changed" */}}

To avoid confusion, rename PipeNode.Decl to PipeNode.Vars, as the
variables may not always be declared after this change. Also change a
few other names to better reflect the added ambiguity of variables in
pipelines.

Modifying the text/template/parse package in a backwards incompatible
manner is acceptable, given that the package godoc clearly states that
it isn't intended for general use. It's the equivalent of an internal
package, back when internal packages didn't exist yet.

To make the changes to the parse package sit well with the cmd/api test,
update except.txt with the changes that we aren't worried about.

Fixes #10608.

Change-Id: I1f83a4297ee093fd45f9993cebb78fc9a9e81295
Reviewed-on: https://go-review.googlesource.com/84480
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
api/README
api/except.txt
src/html/template/escape.go
src/text/template/doc.go
src/text/template/exec.go
src/text/template/exec_test.go
src/text/template/parse/lex.go
src/text/template/parse/lex_test.go
src/text/template/parse/node.go
src/text/template/parse/parse.go
src/text/template/parse/parse_test.go

index d3ad7c1d7473d429102b01afc6f739946ad6e9f4..ce24efcd3126967f72395b842caedae5e5ea4574 100644 (file)
@@ -11,4 +11,3 @@ compatibility.
 next.txt is the only file intended to be mutated. It's a list of
 features that may be added to the next version. It only affects
 warning output from the go api tool.
-
index b3429fe768aa2efc426a5541a27f656e2d9ca379..94043718ba2abba1260aca51cd940ceabd05fc88 100644 (file)
@@ -362,3 +362,22 @@ pkg syscall (openbsd-386-cgo), const SYS_KILL = 37
 pkg syscall (openbsd-amd64), const SYS_KILL = 37
 pkg syscall (openbsd-amd64-cgo), const SYS_KILL = 37
 pkg unicode, const Version = "9.0.0"
+pkg text/template/parse, method (*AssignNode) Copy() Node
+pkg text/template/parse, method (*AssignNode) String() string
+pkg text/template/parse, method (*VariableNode) Copy() Node
+pkg text/template/parse, method (*VariableNode) String() string
+pkg text/template/parse, method (AssignNode) Position() Pos
+pkg text/template/parse, method (AssignNode) Type() NodeType
+pkg text/template/parse, method (VariableNode) Position() Pos
+pkg text/template/parse, method (VariableNode) Type() NodeType
+pkg text/template/parse, type AssignNode struct
+pkg text/template/parse, type AssignNode struct, Ident []string
+pkg text/template/parse, type AssignNode struct, embedded NodeType
+pkg text/template/parse, type AssignNode struct, embedded Pos
+pkg text/template/parse, type PipeNode struct, Decl []*VariableNode
+pkg text/template/parse, type PipeNode struct, Decl bool
+pkg text/template/parse, type PipeNode struct, Vars []*AssignNode
+pkg text/template/parse, type VariableNode struct
+pkg text/template/parse, type VariableNode struct, Ident []string
+pkg text/template/parse, type VariableNode struct, embedded NodeType
+pkg text/template/parse, type VariableNode struct, embedded Pos
index aaeb2d6bc1f8d57d3c7d1e12e392825f7fb057b9..f87e9a69ea935633398a9b19b9419478c651ebc4 100644 (file)
@@ -142,7 +142,7 @@ func (e *escaper) escape(c context, n parse.Node) context {
 
 // escapeAction escapes an action template node.
 func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
-       if len(n.Pipe.Decl) != 0 {
+       if len(n.Pipe.Vars) != 0 {
                // A local variable assignment, not an interpolation.
                return c
        }
index e64d27a7577707b9edb8a4d690dcfec621b66a33..69587cd06d217f95808dd8599c68975c7f6c8ff0 100644 (file)
@@ -241,6 +241,10 @@ The initialization has syntax
 where $variable is the name of the variable. An action that declares a
 variable produces no output.
 
+Variables previously declared can also be assigned, using the syntax
+
+       $variable = pipeline
+
 If a "range" action initializes a variable, the variable is set to the
 successive elements of the iteration. Also, a "range" may declare two
 variables, separated by a comma:
index 099726a180773aa3ebab44d8b0eaac4f85e49575..916be46b86921c53f9eb1aa82958604e784b9ace 100644 (file)
@@ -53,8 +53,20 @@ func (s *state) pop(mark int) {
        s.vars = s.vars[0:mark]
 }
 
-// setVar overwrites the top-nth variable on the stack. Used by range iterations.
-func (s *state) setVar(n int, value reflect.Value) {
+// setVar overwrites the last declared variable with the given name.
+// Used by variable assignments.
+func (s *state) setVar(name string, value reflect.Value) {
+       for i := s.mark() - 1; i >= 0; i-- {
+               if s.vars[i].name == name {
+                       s.vars[i].value = value
+                       return
+               }
+       }
+       s.errorf("undefined variable: %s", name)
+}
+
+// setTopVar overwrites the top-nth variable on the stack. Used by range iterations.
+func (s *state) setTopVar(n int, value reflect.Value) {
        s.vars[len(s.vars)-n].value = value
 }
 
@@ -233,7 +245,7 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
                // Do not pop variables so they persist until next end.
                // Also, if the action declares variables, don't print the result.
                val := s.evalPipeline(dot, node.Pipe)
-               if len(node.Pipe.Decl) == 0 {
+               if len(node.Pipe.Vars) == 0 {
                        s.printValue(node, val)
                }
        case *parse.IfNode:
@@ -320,12 +332,12 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
        mark := s.mark()
        oneIteration := func(index, elem reflect.Value) {
                // Set top var (lexically the second if there are two) to the element.
-               if len(r.Pipe.Decl) > 0 {
-                       s.setVar(1, elem)
+               if len(r.Pipe.Vars) > 0 {
+                       s.setTopVar(1, elem)
                }
                // Set next var (lexically the first if there are two) to the index.
-               if len(r.Pipe.Decl) > 1 {
-                       s.setVar(2, index)
+               if len(r.Pipe.Vars) > 1 {
+                       s.setTopVar(2, index)
                }
                s.walk(elem, r.List)
                s.pop(mark)
@@ -413,8 +425,12 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
                        value = reflect.ValueOf(value.Interface()) // lovely!
                }
        }
-       for _, variable := range pipe.Decl {
-               s.push(variable.Ident[0], value)
+       for _, variable := range pipe.Vars {
+               if pipe.Decl {
+                       s.push(variable.Ident[0], value)
+               } else {
+                       s.setVar(variable.Ident[0], value)
+               }
        }
        return value
 }
@@ -438,7 +454,7 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref
        case *parse.PipeNode:
                // Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
                return s.evalPipeline(dot, n)
-       case *parse.VariableNode:
+       case *parse.AssignNode:
                return s.evalVariableNode(dot, n, cmd.Args, final)
        }
        s.at(firstWord)
@@ -507,7 +523,7 @@ func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []
        return s.evalFieldChain(dot, pipe, chain, chain.Field, args, final)
 }
 
-func (s *state) evalVariableNode(dot reflect.Value, variable *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value {
+func (s *state) evalVariableNode(dot reflect.Value, variable *parse.AssignNode, args []parse.Node, final reflect.Value) reflect.Value {
        // $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
        s.at(variable)
        value := s.varValue(variable.Ident[0])
@@ -748,7 +764,7 @@ func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) refle
                s.errorf("cannot assign nil to %s", typ)
        case *parse.FieldNode:
                return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, missingVal), typ)
-       case *parse.VariableNode:
+       case *parse.AssignNode:
                return s.validateType(s.evalVariableNode(dot, arg, nil, missingVal), typ)
        case *parse.PipeNode:
                return s.validateType(s.evalPipeline(dot, arg), typ)
@@ -866,7 +882,7 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu
                return s.idealConstant(n)
        case *parse.StringNode:
                return reflect.ValueOf(n.Text)
-       case *parse.VariableNode:
+       case *parse.AssignNode:
                return s.evalVariableNode(dot, n, nil, missingVal)
        case *parse.PipeNode:
                return s.evalPipeline(dot, n)
index 8fb4749169738b42f9b7fce6c27f5f34c0a5961f..e54a9ca3c792e2409dd22632307c14a574c5ce32 100644 (file)
@@ -304,6 +304,13 @@ var execTests = []execTest{
        {"$.I", "{{$.I}}", "17", tVal, true},
        {"$.U.V", "{{$.U.V}}", "v", tVal, true},
        {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
+       {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
+       {"nested assignment",
+               "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
+               "3", tVal, true},
+       {"nested assignment changes the last declaration",
+               "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
+               "1", tVal, true},
 
        // Type with String method.
        {"V{6666}.String()", "-{{.V0}}-", "-<6666>-", tVal, true},
index e112cb7714a422a58fd824e751a8a569deda9e88..fae8841fb14804a222e01afd72d78efc182dcbed 100644 (file)
@@ -42,7 +42,8 @@ const (
        itemChar                         // printable ASCII character; grab bag for comma etc.
        itemCharConstant                 // character constant
        itemComplex                      // complex constant (1+2i); imaginary is just a number
-       itemColonEquals                  // colon-equals (':=') introducing a declaration
+       itemAssign                       // colon-equals ('=') introducing an assignment
+       itemDeclare                      // colon-equals (':=') introducing a declaration
        itemEOF
        itemField      // alphanumeric identifier starting with '.'
        itemIdentifier // alphanumeric identifier not starting with '.'
@@ -366,11 +367,13 @@ func lexInsideAction(l *lexer) stateFn {
                return l.errorf("unclosed action")
        case isSpace(r):
                return lexSpace
+       case r == '=':
+               l.emit(itemAssign)
        case r == ':':
                if l.next() != '=' {
                        return l.errorf("expected :=")
                }
-               l.emit(itemColonEquals)
+               l.emit(itemDeclare)
        case r == '|':
                l.emit(itemPipe)
        case r == '"':
index cb01cd98b6ab2263618a5feec888e8bfc58807d5..6e7ece9db3e66f1071279b5c5231e6363120f5c4 100644 (file)
@@ -16,7 +16,7 @@ var itemName = map[itemType]string{
        itemChar:         "char",
        itemCharConstant: "charconst",
        itemComplex:      "complex",
-       itemColonEquals:  ":=",
+       itemDeclare:      ":=",
        itemEOF:          "EOF",
        itemField:        "field",
        itemIdentifier:   "identifier",
@@ -210,7 +210,7 @@ var lexTests = []lexTest{
                tLeft,
                mkItem(itemVariable, "$c"),
                tSpace,
-               mkItem(itemColonEquals, ":="),
+               mkItem(itemDeclare, ":="),
                tSpace,
                mkItem(itemIdentifier, "printf"),
                tSpace,
@@ -262,7 +262,7 @@ var lexTests = []lexTest{
                tLeft,
                mkItem(itemVariable, "$v"),
                tSpace,
-               mkItem(itemColonEquals, ":="),
+               mkItem(itemDeclare, ":="),
                tSpace,
                mkItem(itemNumber, "3"),
                tRight,
@@ -276,7 +276,7 @@ var lexTests = []lexTest{
                tSpace,
                mkItem(itemVariable, "$w"),
                tSpace,
-               mkItem(itemColonEquals, ":="),
+               mkItem(itemDeclare, ":="),
                tSpace,
                mkItem(itemNumber, "3"),
                tRight,
index 55ff46c17ab28e84875f70758cc41796651fa53e..737172dfddbd8021dc1954ea37a01bd5e5285637 100644 (file)
@@ -145,13 +145,14 @@ type PipeNode struct {
        NodeType
        Pos
        tr   *Tree
-       Line int             // The line number in the input. Deprecated: Kept for compatibility.
-       Decl []*VariableNode // Variable declarations in lexical order.
-       Cmds []*CommandNode  // The commands in lexical order.
+       Line int            // The line number in the input. Deprecated: Kept for compatibility.
+       Decl bool           // The variables are being declared, not assigned
+       Vars []*AssignNode  // Variables in lexical order.
+       Cmds []*CommandNode // The commands in lexical order.
 }
 
-func (t *Tree) newPipeline(pos Pos, line int, decl []*VariableNode) *PipeNode {
-       return &PipeNode{tr: t, NodeType: NodePipe, Pos: pos, Line: line, Decl: decl}
+func (t *Tree) newPipeline(pos Pos, line int, vars []*AssignNode) *PipeNode {
+       return &PipeNode{tr: t, NodeType: NodePipe, Pos: pos, Line: line, Vars: vars}
 }
 
 func (p *PipeNode) append(command *CommandNode) {
@@ -160,8 +161,8 @@ func (p *PipeNode) append(command *CommandNode) {
 
 func (p *PipeNode) String() string {
        s := ""
-       if len(p.Decl) > 0 {
-               for i, v := range p.Decl {
+       if len(p.Vars) > 0 {
+               for i, v := range p.Vars {
                        if i > 0 {
                                s += ", "
                        }
@@ -186,11 +187,11 @@ func (p *PipeNode) CopyPipe() *PipeNode {
        if p == nil {
                return p
        }
-       var decl []*VariableNode
-       for _, d := range p.Decl {
-               decl = append(decl, d.Copy().(*VariableNode))
+       var vars []*AssignNode
+       for _, d := range p.Vars {
+               vars = append(vars, d.Copy().(*AssignNode))
        }
-       n := p.tr.newPipeline(p.Pos, p.Line, decl)
+       n := p.tr.newPipeline(p.Pos, p.Line, vars)
        for _, c := range p.Cmds {
                n.append(c.Copy().(*CommandNode))
        }
@@ -317,20 +318,20 @@ func (i *IdentifierNode) Copy() Node {
        return NewIdentifier(i.Ident).SetTree(i.tr).SetPos(i.Pos)
 }
 
-// VariableNode holds a list of variable names, possibly with chained field
+// AssignNode holds a list of variable names, possibly with chained field
 // accesses. The dollar sign is part of the (first) name.
-type VariableNode struct {
+type AssignNode struct {
        NodeType
        Pos
        tr    *Tree
        Ident []string // Variable name and fields in lexical order.
 }
 
-func (t *Tree) newVariable(pos Pos, ident string) *VariableNode {
-       return &VariableNode{tr: t, NodeType: NodeVariable, Pos: pos, Ident: strings.Split(ident, ".")}
+func (t *Tree) newVariable(pos Pos, ident string) *AssignNode {
+       return &AssignNode{tr: t, NodeType: NodeVariable, Pos: pos, Ident: strings.Split(ident, ".")}
 }
 
-func (v *VariableNode) String() string {
+func (v *AssignNode) String() string {
        s := ""
        for i, id := range v.Ident {
                if i > 0 {
@@ -341,12 +342,12 @@ func (v *VariableNode) String() string {
        return s
 }
 
-func (v *VariableNode) tree() *Tree {
+func (v *AssignNode) tree() *Tree {
        return v.tr
 }
 
-func (v *VariableNode) Copy() Node {
-       return &VariableNode{tr: v.tr, NodeType: NodeVariable, Pos: v.Pos, Ident: append([]string{}, v.Ident...)}
+func (v *AssignNode) Copy() Node {
+       return &AssignNode{tr: v.tr, NodeType: NodeVariable, Pos: v.Pos, Ident: append([]string{}, v.Ident...)}
 }
 
 // DotNode holds the special identifier '.'.
index a91a544ce016f2c5446c441dcc30a3abf5d39966..34dc41c6202811dc8b78daa7730e5c594071b9d0 100644 (file)
@@ -383,10 +383,11 @@ func (t *Tree) action() (n Node) {
 // Pipeline:
 //     declarations? command ('|' command)*
 func (t *Tree) pipeline(context string) (pipe *PipeNode) {
-       var decl []*VariableNode
+       decl := false
+       var vars []*AssignNode
        token := t.peekNonSpace()
        pos := token.pos
-       // Are there declarations?
+       // Are there declarations or assignments?
        for {
                if v := t.peekNonSpace(); v.typ == itemVariable {
                        t.next()
@@ -395,26 +396,33 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
                        // argument variable rather than a declaration. So remember the token
                        // adjacent to the variable so we can push it back if necessary.
                        tokenAfterVariable := t.peek()
-                       if next := t.peekNonSpace(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") {
+                       next := t.peekNonSpace()
+                       switch {
+                       case next.typ == itemAssign, next.typ == itemDeclare,
+                               next.typ == itemChar && next.val == ",":
                                t.nextNonSpace()
                                variable := t.newVariable(v.pos, v.val)
-                               decl = append(decl, variable)
+                               vars = append(vars, variable)
                                t.vars = append(t.vars, v.val)
+                               if next.typ == itemDeclare {
+                                       decl = true
+                               }
                                if next.typ == itemChar && next.val == "," {
-                                       if context == "range" && len(decl) < 2 {
+                                       if context == "range" && len(vars) < 2 {
                                                continue
                                        }
                                        t.errorf("too many declarations in %s", context)
                                }
-                       } else if tokenAfterVariable.typ == itemSpace {
+                       case tokenAfterVariable.typ == itemSpace:
                                t.backup3(v, tokenAfterVariable)
-                       } else {
+                       default:
                                t.backup2(v)
                        }
                }
                break
        }
-       pipe = t.newPipeline(pos, token.line, decl)
+       pipe = t.newPipeline(pos, token.line, vars)
+       pipe.Decl = decl
        for {
                switch token := t.nextNonSpace(); token.typ {
                case itemRightDelim, itemRightParen:
index 81f14aca986cdcb8774f0c2562cc388bf6a4bb16..c1f80c1326bc317d6993d1497dd4b9c7003b846f 100644 (file)
@@ -259,9 +259,9 @@ var parseTests = []parseTest{
        {"adjacent args", "{{printf 3`x`}}", hasError, ""},
        {"adjacent args with .", "{{printf `x`.}}", hasError, ""},
        {"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""},
-       // Equals (and other chars) do not assignments make (yet).
+       // Other kinds of assignments and operators aren't available yet.
        {"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},
-       {"bug0b", "{{$x = 1}}{{$x}}", hasError, ""},
+       {"bug0b", "{{$x += 1}}{{$x}}", hasError, ""},
        {"bug0c", "{{$x ! 2}}{{$x}}", hasError, ""},
        {"bug0d", "{{$x % 3}}{{$x}}", hasError, ""},
        // Check the parse fails for := rather than comma.