mirror of
https://github.com/golang/go
synced 2024-11-26 02:07:57 -07:00
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í <mvdan@mvdan.cc> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rob Pike <r@golang.org>
This commit is contained in:
parent
804d03281c
commit
28c1ad9d35
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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},
|
||||
|
@ -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 == '"':
|
||||
|
@ -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,
|
||||
|
@ -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 '.'.
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user