exp/template: fields and methods on variables.
Not strictly necessary (you could achieve the same, clumsily,
via with blocks) but great to have: $x.Field, $y.Method.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/4678047
diff --git a/src/pkg/exp/template/doc.go b/src/pkg/exp/template/doc.go
index 736b1a3..a0fdd0a 100644
--- a/src/pkg/exp/template/doc.go
+++ b/src/pkg/exp/template/doc.go
@@ -79,7 +79,7 @@
Arguments
-An argument is a simple value, denoted by one of the following:
+An argument is a simple value, denoted by one of the following.
- A boolean, string, character, integer, floating-point, imaginary
or complex constant in Go syntax. These behave like Go's untyped
@@ -100,6 +100,8 @@
The result is the value of the field. Field invocations may be
chained:
.Field1.Field2
+ Fields can also be evaluated on variables, including chaining:
+ $x.Field1.Field2
- The name of a niladic method of the data, preceded by a period,
such as
.Method
@@ -111,6 +113,8 @@
Method invocations may be chained, but only the last element of
the chain may be a method; other others must be struct fields:
.Field1.Field2.Method
+ Methods can also be evaluated on variables, including chaining:
+ $x.Field1.Method
- The name of a niladic function, such as
fun
The result is the value of invoking the function, fun(). The return
diff --git a/src/pkg/exp/template/exec.go b/src/pkg/exp/template/exec.go
index 21e8e81..d24e8b9 100644
--- a/src/pkg/exp/template/exec.go
+++ b/src/pkg/exp/template/exec.go
@@ -271,7 +271,7 @@
}
}
if pipe.decl != nil {
- s.push(pipe.decl.ident, value)
+ s.push(pipe.decl.ident[0], value)
}
return value
}
@@ -290,6 +290,8 @@
case *identifierNode:
// Must be a function.
return s.evalFunction(data, n.ident, cmd.args, final)
+ case *variableNode:
+ return s.evalVariableNode(n, cmd.args, final)
}
s.notAFunction(cmd.args, final)
switch word := firstWord.(type) {
@@ -313,21 +315,32 @@
}
case *stringNode:
return reflect.ValueOf(word.text)
- case *variableNode:
- return s.varValue(word.ident)
}
s.errorf("can't handle command %q", firstWord)
panic("not reached")
}
func (s *state) evalFieldNode(data reflect.Value, field *fieldNode, args []node, final reflect.Value) reflect.Value {
+ return s.evalFieldChain(data, field.ident, args, final)
+}
+
+func (s *state) evalVariableNode(v *variableNode, args []node, final reflect.Value) reflect.Value {
+ // $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
+ data := s.varValue(v.ident[0])
+ if len(v.ident) == 1 {
+ return data
+ }
+ return s.evalFieldChain(data, v.ident[1:], args, final)
+}
+
+func (s *state) evalFieldChain(data reflect.Value, ident []string, args []node, final reflect.Value) reflect.Value {
// Up to the last entry, it must be a field.
- n := len(field.ident)
+ n := len(ident)
for i := 0; i < n-1; i++ {
- data = s.evalField(data, field.ident[i], nil, zero, false)
+ data = s.evalField(data, ident[i], nil, zero, false)
}
// Now it can be a field or method and if a method, gets arguments.
- return s.evalField(data, field.ident[n-1], args, final, true)
+ return s.evalField(data, ident[n-1], args, final, true)
}
func (s *state) evalFunction(data reflect.Value, name string, args []node, final reflect.Value) reflect.Value {
@@ -468,7 +481,7 @@
case *fieldNode:
return s.validateType(s.evalFieldNode(data, arg, []node{n}, zero), typ)
case *variableNode:
- return s.validateType(s.varValue(arg.ident), typ)
+ return s.validateType(s.evalVariableNode(arg, nil, zero), typ)
}
switch typ.Kind() {
case reflect.Bool:
@@ -578,7 +591,7 @@
case *stringNode:
return reflect.ValueOf(n.text)
case *variableNode:
- return s.varValue(n.ident)
+ return s.evalVariableNode(n, nil, zero)
}
s.errorf("can't handle assignment of %s to empty interface argument", n)
panic("not reached")
diff --git a/src/pkg/exp/template/exec_test.go b/src/pkg/exp/template/exec_test.go
index 55cb681..e4fd4e6 100644
--- a/src/pkg/exp/template/exec_test.go
+++ b/src/pkg/exp/template/exec_test.go
@@ -165,6 +165,9 @@
{"range $x PSI", "{{range $x := .PSI}}<{{$x}}>{{end}}", "<21><22><23>", tVal, true},
{"if $x with $y int", "{{if $x := true}}{{with $y := .I}}{{$x}},{{$y}}{{end}}{{end}}", "true,17", tVal, true},
{"if $x with $x int", "{{if $x := true}}{{with $x := .I}}{{$x}},{{end}}{{$x}}{{end}}", "17,true", tVal, true},
+ {"$.I", "{{$.I}}", "17", tVal, true},
+ {"$.U.V", "{{$.U.V}}", "v", tVal, true},
+ {"with $x struct.U.V", "{{with $x := $}}{{$.U.V}}{{end}}", "v", tVal, true},
// Pointers.
{"*int", "{{.PI}}", "23", tVal, true},
@@ -186,6 +189,7 @@
{".Method2(3, .X)", "-{{.Method2 3 .X}}-", "-Method2: 3 x-", tVal, true},
{".Method2(.U16, `str`)", "-{{.Method2 .U16 `str`}}-", "-Method2: 16 str-", tVal, true},
{".Method2(.U16, $x)", "{{if $x := .X}}-{{.Method2 .U16 $x}}{{end}}-", "-Method2: 16 x-", tVal, true},
+ {"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
// Pipelines.
{"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 M0-", tVal, true},
diff --git a/src/pkg/exp/template/lex.go b/src/pkg/exp/template/lex.go
index 7aebe02..72eff10 100644
--- a/src/pkg/exp/template/lex.go
+++ b/src/pkg/exp/template/lex.go
@@ -329,7 +329,7 @@
switch r := l.next(); {
case isAlphaNumeric(r):
// absorb.
- case r == '.' && l.input[l.start] == '.':
+ case r == '.' && (l.input[l.start] == '.' || l.input[l.start] == '$'):
// field chaining; absorb into one token.
default:
l.backup()
diff --git a/src/pkg/exp/template/lex_test.go b/src/pkg/exp/template/lex_test.go
index e88fd36..36079e2 100644
--- a/src/pkg/exp/template/lex_test.go
+++ b/src/pkg/exp/template/lex_test.go
@@ -97,7 +97,7 @@
tRight,
tEOF,
}},
- {"variables", "{{$c := printf $ $hello $23 $.Method}}", []item{
+ {"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
tLeft,
{itemVariable, "$c"},
{itemColonEquals, ":="},
@@ -106,6 +106,7 @@
{itemVariable, "$hello"},
{itemVariable, "$23"},
{itemVariable, "$"},
+ {itemVariable, "$var.Field"},
{itemField, ".Method"},
tRight,
tEOF,
diff --git a/src/pkg/exp/template/parse.go b/src/pkg/exp/template/parse.go
index 38a415d..774a7dd 100644
--- a/src/pkg/exp/template/parse.go
+++ b/src/pkg/exp/template/parse.go
@@ -214,11 +214,11 @@
// variableNode holds a variable.
type variableNode struct {
nodeType
- ident string
+ ident []string
}
func newVariable(ident string) *variableNode {
- return &variableNode{nodeType: nodeVariable, ident: ident}
+ return &variableNode{nodeType: nodeVariable, ident: strings.Split(ident, ".")}
}
func (v *variableNode) String() string {
@@ -716,6 +716,9 @@
if ce := t.peek(); ce.typ == itemColonEquals {
t.next()
decl = newVariable(v.val)
+ if len(decl.ident) != 1 {
+ t.errorf("illegal variable in declaration: %s", v.val)
+ }
t.vars = append(t.vars, v.val)
} else {
t.backup2(v)
@@ -854,9 +857,10 @@
case itemDot:
cmd.append(newDot())
case itemVariable:
+ v := newVariable(token.val)
found := false
for _, varName := range t.vars {
- if varName == token.val {
+ if varName == v.ident[0] {
found = true
break
}
@@ -864,7 +868,7 @@
if !found {
t.errorf("undefined variable %q", token.val)
}
- cmd.append(newVariable(token.val))
+ cmd.append(v)
case itemField:
cmd.append(newField(token.val))
case itemBool:
diff --git a/src/pkg/exp/template/parse_test.go b/src/pkg/exp/template/parse_test.go
index 7439ec8..2a2fa64 100644
--- a/src/pkg/exp/template/parse_test.go
+++ b/src/pkg/exp/template/parse_test.go
@@ -172,15 +172,17 @@
{"simple command", "{{printf}}", noError,
`[(action: [(command: [I=printf])])]`},
{"$ invocation", "{{$}}", noError,
- "[(action: [(command: [V=$])])]"},
+ "[(action: [(command: [V=[$]])])]"},
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
- "[({{with $x := [(command: [N=3])]}} [(action: [(command: [V=$x N=23])])])]"},
+ "[({{with [$x] := [(command: [N=3])]}} [(action: [(command: [V=[$x] N=23])])])]"},
+ {"variable with fields", "{{$.I}}", noError,
+ "[(action: [(command: [V=[$ I]])])]"},
{"multi-word command", "{{printf `%d` 23}}", noError,
"[(action: [(command: [I=printf S=`%d` N=23])])]"},
{"pipeline", "{{.X|.Y}}", noError,
`[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`},
{"pipeline with decl", "{{$x := .X|.Y}}", noError,
- `[(action: $x := [(command: [F=[X]]) (command: [F=[Y]])])]`},
+ `[(action: [$x] := [(command: [F=[X]]) (command: [F=[Y]])])]`},
{"declaration", "{{.X|.Y}}", noError,
`[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`},
{"simple if", "{{if .X}}hello{{end}}", noError,
@@ -217,6 +219,7 @@
{"undefined function", "hello{{undefined}}", hasError, ""},
{"undefined variable", "{{$x}}", hasError, ""},
{"variable undefined after end", "{{with $x := 4}}{{end}}{{$x}}", hasError, ""},
+ {"declare with field", "{{with $x.Y := 4}}{{end}}", hasError, ""},
}
func TestParse(t *testing.T) {