Skip to content

Commit 28c1ad9

Browse files
committed
text/template: add variable assignments
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í <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rob Pike <[email protected]>
1 parent 804d032 commit 28c1ad9

File tree

11 files changed

+107
-50
lines changed

11 files changed

+107
-50
lines changed

api/README

-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,3 @@ compatibility.
1111
next.txt is the only file intended to be mutated. It's a list of
1212
features that may be added to the next version. It only affects
1313
warning output from the go api tool.
14-

api/except.txt

+19
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,22 @@ pkg syscall (openbsd-386-cgo), const SYS_KILL = 37
362362
pkg syscall (openbsd-amd64), const SYS_KILL = 37
363363
pkg syscall (openbsd-amd64-cgo), const SYS_KILL = 37
364364
pkg unicode, const Version = "9.0.0"
365+
pkg text/template/parse, method (*AssignNode) Copy() Node
366+
pkg text/template/parse, method (*AssignNode) String() string
367+
pkg text/template/parse, method (*VariableNode) Copy() Node
368+
pkg text/template/parse, method (*VariableNode) String() string
369+
pkg text/template/parse, method (AssignNode) Position() Pos
370+
pkg text/template/parse, method (AssignNode) Type() NodeType
371+
pkg text/template/parse, method (VariableNode) Position() Pos
372+
pkg text/template/parse, method (VariableNode) Type() NodeType
373+
pkg text/template/parse, type AssignNode struct
374+
pkg text/template/parse, type AssignNode struct, Ident []string
375+
pkg text/template/parse, type AssignNode struct, embedded NodeType
376+
pkg text/template/parse, type AssignNode struct, embedded Pos
377+
pkg text/template/parse, type PipeNode struct, Decl []*VariableNode
378+
pkg text/template/parse, type PipeNode struct, Decl bool
379+
pkg text/template/parse, type PipeNode struct, Vars []*AssignNode
380+
pkg text/template/parse, type VariableNode struct
381+
pkg text/template/parse, type VariableNode struct, Ident []string
382+
pkg text/template/parse, type VariableNode struct, embedded NodeType
383+
pkg text/template/parse, type VariableNode struct, embedded Pos

src/html/template/escape.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func (e *escaper) escape(c context, n parse.Node) context {
142142

143143
// escapeAction escapes an action template node.
144144
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
145-
if len(n.Pipe.Decl) != 0 {
145+
if len(n.Pipe.Vars) != 0 {
146146
// A local variable assignment, not an interpolation.
147147
return c
148148
}

src/text/template/doc.go

+4
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ The initialization has syntax
241241
where $variable is the name of the variable. An action that declares a
242242
variable produces no output.
243243
244+
Variables previously declared can also be assigned, using the syntax
245+
246+
$variable = pipeline
247+
244248
If a "range" action initializes a variable, the variable is set to the
245249
successive elements of the iteration. Also, a "range" may declare two
246250
variables, separated by a comma:

src/text/template/exec.go

+29-13
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,20 @@ func (s *state) pop(mark int) {
5353
s.vars = s.vars[0:mark]
5454
}
5555

56-
// setVar overwrites the top-nth variable on the stack. Used by range iterations.
57-
func (s *state) setVar(n int, value reflect.Value) {
56+
// setVar overwrites the last declared variable with the given name.
57+
// Used by variable assignments.
58+
func (s *state) setVar(name string, value reflect.Value) {
59+
for i := s.mark() - 1; i >= 0; i-- {
60+
if s.vars[i].name == name {
61+
s.vars[i].value = value
62+
return
63+
}
64+
}
65+
s.errorf("undefined variable: %s", name)
66+
}
67+
68+
// setTopVar overwrites the top-nth variable on the stack. Used by range iterations.
69+
func (s *state) setTopVar(n int, value reflect.Value) {
5870
s.vars[len(s.vars)-n].value = value
5971
}
6072

@@ -233,7 +245,7 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
233245
// Do not pop variables so they persist until next end.
234246
// Also, if the action declares variables, don't print the result.
235247
val := s.evalPipeline(dot, node.Pipe)
236-
if len(node.Pipe.Decl) == 0 {
248+
if len(node.Pipe.Vars) == 0 {
237249
s.printValue(node, val)
238250
}
239251
case *parse.IfNode:
@@ -320,12 +332,12 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
320332
mark := s.mark()
321333
oneIteration := func(index, elem reflect.Value) {
322334
// Set top var (lexically the second if there are two) to the element.
323-
if len(r.Pipe.Decl) > 0 {
324-
s.setVar(1, elem)
335+
if len(r.Pipe.Vars) > 0 {
336+
s.setTopVar(1, elem)
325337
}
326338
// Set next var (lexically the first if there are two) to the index.
327-
if len(r.Pipe.Decl) > 1 {
328-
s.setVar(2, index)
339+
if len(r.Pipe.Vars) > 1 {
340+
s.setTopVar(2, index)
329341
}
330342
s.walk(elem, r.List)
331343
s.pop(mark)
@@ -413,8 +425,12 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
413425
value = reflect.ValueOf(value.Interface()) // lovely!
414426
}
415427
}
416-
for _, variable := range pipe.Decl {
417-
s.push(variable.Ident[0], value)
428+
for _, variable := range pipe.Vars {
429+
if pipe.Decl {
430+
s.push(variable.Ident[0], value)
431+
} else {
432+
s.setVar(variable.Ident[0], value)
433+
}
418434
}
419435
return value
420436
}
@@ -438,7 +454,7 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref
438454
case *parse.PipeNode:
439455
// Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
440456
return s.evalPipeline(dot, n)
441-
case *parse.VariableNode:
457+
case *parse.AssignNode:
442458
return s.evalVariableNode(dot, n, cmd.Args, final)
443459
}
444460
s.at(firstWord)
@@ -507,7 +523,7 @@ func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []
507523
return s.evalFieldChain(dot, pipe, chain, chain.Field, args, final)
508524
}
509525

510-
func (s *state) evalVariableNode(dot reflect.Value, variable *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value {
526+
func (s *state) evalVariableNode(dot reflect.Value, variable *parse.AssignNode, args []parse.Node, final reflect.Value) reflect.Value {
511527
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
512528
s.at(variable)
513529
value := s.varValue(variable.Ident[0])
@@ -748,7 +764,7 @@ func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) refle
748764
s.errorf("cannot assign nil to %s", typ)
749765
case *parse.FieldNode:
750766
return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, missingVal), typ)
751-
case *parse.VariableNode:
767+
case *parse.AssignNode:
752768
return s.validateType(s.evalVariableNode(dot, arg, nil, missingVal), typ)
753769
case *parse.PipeNode:
754770
return s.validateType(s.evalPipeline(dot, arg), typ)
@@ -866,7 +882,7 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu
866882
return s.idealConstant(n)
867883
case *parse.StringNode:
868884
return reflect.ValueOf(n.Text)
869-
case *parse.VariableNode:
885+
case *parse.AssignNode:
870886
return s.evalVariableNode(dot, n, nil, missingVal)
871887
case *parse.PipeNode:
872888
return s.evalPipeline(dot, n)

src/text/template/exec_test.go

+7
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,13 @@ var execTests = []execTest{
304304
{"$.I", "{{$.I}}", "17", tVal, true},
305305
{"$.U.V", "{{$.U.V}}", "v", tVal, true},
306306
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
307+
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
308+
{"nested assignment",
309+
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
310+
"3", tVal, true},
311+
{"nested assignment changes the last declaration",
312+
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
313+
"1", tVal, true},
307314

308315
// Type with String method.
309316
{"V{6666}.String()", "-{{.V0}}-", "-<6666>-", tVal, true},

src/text/template/parse/lex.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ const (
4242
itemChar // printable ASCII character; grab bag for comma etc.
4343
itemCharConstant // character constant
4444
itemComplex // complex constant (1+2i); imaginary is just a number
45-
itemColonEquals // colon-equals (':=') introducing a declaration
45+
itemAssign // colon-equals ('=') introducing an assignment
46+
itemDeclare // colon-equals (':=') introducing a declaration
4647
itemEOF
4748
itemField // alphanumeric identifier starting with '.'
4849
itemIdentifier // alphanumeric identifier not starting with '.'
@@ -366,11 +367,13 @@ func lexInsideAction(l *lexer) stateFn {
366367
return l.errorf("unclosed action")
367368
case isSpace(r):
368369
return lexSpace
370+
case r == '=':
371+
l.emit(itemAssign)
369372
case r == ':':
370373
if l.next() != '=' {
371374
return l.errorf("expected :=")
372375
}
373-
l.emit(itemColonEquals)
376+
l.emit(itemDeclare)
374377
case r == '|':
375378
l.emit(itemPipe)
376379
case r == '"':

src/text/template/parse/lex_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var itemName = map[itemType]string{
1616
itemChar: "char",
1717
itemCharConstant: "charconst",
1818
itemComplex: "complex",
19-
itemColonEquals: ":=",
19+
itemDeclare: ":=",
2020
itemEOF: "EOF",
2121
itemField: "field",
2222
itemIdentifier: "identifier",
@@ -210,7 +210,7 @@ var lexTests = []lexTest{
210210
tLeft,
211211
mkItem(itemVariable, "$c"),
212212
tSpace,
213-
mkItem(itemColonEquals, ":="),
213+
mkItem(itemDeclare, ":="),
214214
tSpace,
215215
mkItem(itemIdentifier, "printf"),
216216
tSpace,
@@ -262,7 +262,7 @@ var lexTests = []lexTest{
262262
tLeft,
263263
mkItem(itemVariable, "$v"),
264264
tSpace,
265-
mkItem(itemColonEquals, ":="),
265+
mkItem(itemDeclare, ":="),
266266
tSpace,
267267
mkItem(itemNumber, "3"),
268268
tRight,
@@ -276,7 +276,7 @@ var lexTests = []lexTest{
276276
tSpace,
277277
mkItem(itemVariable, "$w"),
278278
tSpace,
279-
mkItem(itemColonEquals, ":="),
279+
mkItem(itemDeclare, ":="),
280280
tSpace,
281281
mkItem(itemNumber, "3"),
282282
tRight,

src/text/template/parse/node.go

+20-19
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,14 @@ type PipeNode struct {
145145
NodeType
146146
Pos
147147
tr *Tree
148-
Line int // The line number in the input. Deprecated: Kept for compatibility.
149-
Decl []*VariableNode // Variable declarations in lexical order.
150-
Cmds []*CommandNode // The commands in lexical order.
148+
Line int // The line number in the input. Deprecated: Kept for compatibility.
149+
Decl bool // The variables are being declared, not assigned
150+
Vars []*AssignNode // Variables in lexical order.
151+
Cmds []*CommandNode // The commands in lexical order.
151152
}
152153

153-
func (t *Tree) newPipeline(pos Pos, line int, decl []*VariableNode) *PipeNode {
154-
return &PipeNode{tr: t, NodeType: NodePipe, Pos: pos, Line: line, Decl: decl}
154+
func (t *Tree) newPipeline(pos Pos, line int, vars []*AssignNode) *PipeNode {
155+
return &PipeNode{tr: t, NodeType: NodePipe, Pos: pos, Line: line, Vars: vars}
155156
}
156157

157158
func (p *PipeNode) append(command *CommandNode) {
@@ -160,8 +161,8 @@ func (p *PipeNode) append(command *CommandNode) {
160161

161162
func (p *PipeNode) String() string {
162163
s := ""
163-
if len(p.Decl) > 0 {
164-
for i, v := range p.Decl {
164+
if len(p.Vars) > 0 {
165+
for i, v := range p.Vars {
165166
if i > 0 {
166167
s += ", "
167168
}
@@ -186,11 +187,11 @@ func (p *PipeNode) CopyPipe() *PipeNode {
186187
if p == nil {
187188
return p
188189
}
189-
var decl []*VariableNode
190-
for _, d := range p.Decl {
191-
decl = append(decl, d.Copy().(*VariableNode))
190+
var vars []*AssignNode
191+
for _, d := range p.Vars {
192+
vars = append(vars, d.Copy().(*AssignNode))
192193
}
193-
n := p.tr.newPipeline(p.Pos, p.Line, decl)
194+
n := p.tr.newPipeline(p.Pos, p.Line, vars)
194195
for _, c := range p.Cmds {
195196
n.append(c.Copy().(*CommandNode))
196197
}
@@ -317,20 +318,20 @@ func (i *IdentifierNode) Copy() Node {
317318
return NewIdentifier(i.Ident).SetTree(i.tr).SetPos(i.Pos)
318319
}
319320

320-
// VariableNode holds a list of variable names, possibly with chained field
321+
// AssignNode holds a list of variable names, possibly with chained field
321322
// accesses. The dollar sign is part of the (first) name.
322-
type VariableNode struct {
323+
type AssignNode struct {
323324
NodeType
324325
Pos
325326
tr *Tree
326327
Ident []string // Variable name and fields in lexical order.
327328
}
328329

329-
func (t *Tree) newVariable(pos Pos, ident string) *VariableNode {
330-
return &VariableNode{tr: t, NodeType: NodeVariable, Pos: pos, Ident: strings.Split(ident, ".")}
330+
func (t *Tree) newVariable(pos Pos, ident string) *AssignNode {
331+
return &AssignNode{tr: t, NodeType: NodeVariable, Pos: pos, Ident: strings.Split(ident, ".")}
331332
}
332333

333-
func (v *VariableNode) String() string {
334+
func (v *AssignNode) String() string {
334335
s := ""
335336
for i, id := range v.Ident {
336337
if i > 0 {
@@ -341,12 +342,12 @@ func (v *VariableNode) String() string {
341342
return s
342343
}
343344

344-
func (v *VariableNode) tree() *Tree {
345+
func (v *AssignNode) tree() *Tree {
345346
return v.tr
346347
}
347348

348-
func (v *VariableNode) Copy() Node {
349-
return &VariableNode{tr: v.tr, NodeType: NodeVariable, Pos: v.Pos, Ident: append([]string{}, v.Ident...)}
349+
func (v *AssignNode) Copy() Node {
350+
return &AssignNode{tr: v.tr, NodeType: NodeVariable, Pos: v.Pos, Ident: append([]string{}, v.Ident...)}
350351
}
351352

352353
// DotNode holds the special identifier '.'.

src/text/template/parse/parse.go

+16-8
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,11 @@ func (t *Tree) action() (n Node) {
383383
// Pipeline:
384384
// declarations? command ('|' command)*
385385
func (t *Tree) pipeline(context string) (pipe *PipeNode) {
386-
var decl []*VariableNode
386+
decl := false
387+
var vars []*AssignNode
387388
token := t.peekNonSpace()
388389
pos := token.pos
389-
// Are there declarations?
390+
// Are there declarations or assignments?
390391
for {
391392
if v := t.peekNonSpace(); v.typ == itemVariable {
392393
t.next()
@@ -395,26 +396,33 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
395396
// argument variable rather than a declaration. So remember the token
396397
// adjacent to the variable so we can push it back if necessary.
397398
tokenAfterVariable := t.peek()
398-
if next := t.peekNonSpace(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") {
399+
next := t.peekNonSpace()
400+
switch {
401+
case next.typ == itemAssign, next.typ == itemDeclare,
402+
next.typ == itemChar && next.val == ",":
399403
t.nextNonSpace()
400404
variable := t.newVariable(v.pos, v.val)
401-
decl = append(decl, variable)
405+
vars = append(vars, variable)
402406
t.vars = append(t.vars, v.val)
407+
if next.typ == itemDeclare {
408+
decl = true
409+
}
403410
if next.typ == itemChar && next.val == "," {
404-
if context == "range" && len(decl) < 2 {
411+
if context == "range" && len(vars) < 2 {
405412
continue
406413
}
407414
t.errorf("too many declarations in %s", context)
408415
}
409-
} else if tokenAfterVariable.typ == itemSpace {
416+
case tokenAfterVariable.typ == itemSpace:
410417
t.backup3(v, tokenAfterVariable)
411-
} else {
418+
default:
412419
t.backup2(v)
413420
}
414421
}
415422
break
416423
}
417-
pipe = t.newPipeline(pos, token.line, decl)
424+
pipe = t.newPipeline(pos, token.line, vars)
425+
pipe.Decl = decl
418426
for {
419427
switch token := t.nextNonSpace(); token.typ {
420428
case itemRightDelim, itemRightParen:

src/text/template/parse/parse_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,9 @@ var parseTests = []parseTest{
259259
{"adjacent args", "{{printf 3`x`}}", hasError, ""},
260260
{"adjacent args with .", "{{printf `x`.}}", hasError, ""},
261261
{"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""},
262-
// Equals (and other chars) do not assignments make (yet).
262+
// Other kinds of assignments and operators aren't available yet.
263263
{"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},
264-
{"bug0b", "{{$x = 1}}{{$x}}", hasError, ""},
264+
{"bug0b", "{{$x += 1}}{{$x}}", hasError, ""},
265265
{"bug0c", "{{$x ! 2}}{{$x}}", hasError, ""},
266266
{"bug0d", "{{$x % 3}}{{$x}}", hasError, ""},
267267
// Check the parse fails for := rather than comma.

0 commit comments

Comments
 (0)