text/template: provide a mechanism for options
Add one option, which is the motivating example, a way to control
what happens when a map is indexed with a key that is not in the map.
Rather than do something specific for that case, we provide a simple
general option mechanism to avoid adding API if something else
comes up. This general approach also makes it easy for html/template
to track (and adapt, should that become important).
New method: Option(option string...). The option strings are key=value
pairs or just simple strings (no =).
New option:
missingkey: Control the behavior during execution if a map is
indexed with a key that is not present in the map.
"missingkey=default" or "missingkey=invalid"
The default behavior: Do nothing and continue execution.
If printed, the result of the index operation is the string
"<no value>".
"missingkey=zero"
The operation returns the zero value for the map type's element.
"missingkey=error"
Execution stops immediately with an error.
Fixes #6288.
Change-Id: Id811e2b99dc05aff324d517faac113ef3c25293a
Reviewed-on: https://go-review.googlesource.com/8462
Reviewed-by: Robert Griesemer <gri@golang.org>
diff --git a/src/html/template/template.go b/src/html/template/template.go
index 64c0041..bb9140a 100644
--- a/src/html/template/template.go
+++ b/src/html/template/template.go
@@ -51,6 +51,29 @@
return m
}
+// Option sets options for the template. Options are described by
+// strings, either a simple string or "key=value". There can be at
+// most one equals sign in an option string. If the option string
+// is unrecognized or otherwise invalid, Option panics.
+//
+// Known options:
+//
+// missingkey: Control the behavior during execution if a map is
+// indexed with a key that is not present in the map.
+// "missingkey=default" or "missingkey=invalid"
+// The default behavior: Do nothing and continue execution.
+// If printed, the result of the index operation is the string
+// "<no value>".
+// "missingkey=zero"
+// The operation returns the zero value for the map type's element.
+// "missingkey=error"
+// Execution stops immediately with an error.
+//
+func (t *Template) Option(opt ...string) *Template {
+ t.text.Option(opt...)
+ return t
+}
+
// escape escapes all associated templates.
func (t *Template) escape() error {
t.nameSpace.mu.Lock()
diff --git a/src/text/template/exec.go b/src/text/template/exec.go
index 613a778..e6e1287 100644
--- a/src/text/template/exec.go
+++ b/src/text/template/exec.go
@@ -519,7 +519,18 @@
if hasArgs {
s.errorf("%s is not a method but has arguments", fieldName)
}
- return receiver.MapIndex(nameVal)
+ result := receiver.MapIndex(nameVal)
+ if !result.IsValid() {
+ switch s.tmpl.option.missingKey {
+ case mapInvalid:
+ // Just use the invalid value.
+ case mapZeroValue:
+ result = reflect.Zero(receiver.Type().Elem())
+ case mapError:
+ s.errorf("map has no entry for key %q", fieldName)
+ }
+ }
+ return result
}
}
s.errorf("can't evaluate field %s in type %s", fieldName, typ)
diff --git a/src/text/template/exec_test.go b/src/text/template/exec_test.go
index b1f7787..8c4e165 100644
--- a/src/text/template/exec_test.go
+++ b/src/text/template/exec_test.go
@@ -1044,3 +1044,54 @@
}
}
}
+
+func TestMissingMapKey(t *testing.T) {
+ data := map[string]int{
+ "x": 99,
+ }
+ tmpl, err := New("t1").Parse("{{.x}} {{.y}}")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var b bytes.Buffer
+ // By default, just get "<no value>"
+ err = tmpl.Execute(&b, data)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := "99 <no value>"
+ got := b.String()
+ if got != want {
+ t.Errorf("got %q; expected %q", got, want)
+ }
+ // Same if we set the option explicitly to the default.
+ tmpl.Option("missingkey=default")
+ b.Reset()
+ err = tmpl.Execute(&b, data)
+ if err != nil {
+ t.Fatal("default:", err)
+ }
+ want = "99 <no value>"
+ got = b.String()
+ if got != want {
+ t.Errorf("got %q; expected %q", got, want)
+ }
+ // Next we ask for a zero value
+ tmpl.Option("missingkey=zero")
+ b.Reset()
+ err = tmpl.Execute(&b, data)
+ if err != nil {
+ t.Fatal("zero:", err)
+ }
+ want = "99 0"
+ got = b.String()
+ if got != want {
+ t.Errorf("got %q; expected %q", got, want)
+ }
+ // Now we ask for an error.
+ tmpl.Option("missingkey=error")
+ err = tmpl.Execute(&b, data)
+ if err == nil {
+ t.Errorf("expected error; got none")
+ }
+}
diff --git a/src/text/template/funcs.go b/src/text/template/funcs.go
index 39ee5ed..cdd187b 100644
--- a/src/text/template/funcs.go
+++ b/src/text/template/funcs.go
@@ -590,7 +590,7 @@
a, ok := printableValue(reflect.ValueOf(arg))
if ok {
args[i] = a
- } // else left fmt do its thing
+ } // else let fmt do its thing
}
s = fmt.Sprint(args...)
}
diff --git a/src/text/template/option.go b/src/text/template/option.go
new file mode 100644
index 0000000..fcdd871
--- /dev/null
+++ b/src/text/template/option.go
@@ -0,0 +1,73 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains the code to handle template options.
+
+package template
+
+import "strings"
+
+// missingKeyAction defines how to respond to indexing a map with a key that is not present.
+type missingKeyAction int
+
+const (
+ mapInvalid missingKeyAction = iota // Return an invalid reflect.Value.
+ mapZeroValue // Return the zero value for the map element.
+ mapError // Error out
+)
+
+type option struct {
+ missingKey missingKeyAction
+}
+
+// Option sets options for the template. Options are described by
+// strings, either a simple string or "key=value". There can be at
+// most one equals sign in an option string. If the option string
+// is unrecognized or otherwise invalid, Option panics.
+//
+// Known options:
+//
+// missingkey: Control the behavior during execution if a map is
+// indexed with a key that is not present in the map.
+// "missingkey=default" or "missingkey=invalid"
+// The default behavior: Do nothing and continue execution.
+// If printed, the result of the index operation is the string
+// "<no value>".
+// "missingkey=zero"
+// The operation returns the zero value for the map type's element.
+// "missingkey=error"
+// Execution stops immediately with an error.
+//
+func (t *Template) Option(opt ...string) *Template {
+ for _, s := range opt {
+ t.setOption(s)
+ }
+ return t
+}
+
+func (t *Template) setOption(opt string) {
+ if opt == "" {
+ panic("empty option string")
+ }
+ elems := strings.Split(opt, "=")
+ switch len(elems) {
+ case 2:
+ // key=value
+ switch elems[0] {
+ case "missingkey":
+ switch elems[1] {
+ case "invalid", "default":
+ t.option.missingKey = mapInvalid
+ return
+ case "zero":
+ t.option.missingKey = mapZeroValue
+ return
+ case "error":
+ t.option.missingKey = mapError
+ return
+ }
+ }
+ }
+ panic("unrecognized option: " + opt)
+}
diff --git a/src/text/template/template.go b/src/text/template/template.go
index 249d0cb..8611faa 100644
--- a/src/text/template/template.go
+++ b/src/text/template/template.go
@@ -18,6 +18,7 @@
// expose reflection to the client.
parseFuncs FuncMap
execFuncs map[string]reflect.Value
+ option option
}
// Template is the representation of a parsed template. The *parse.Tree