Repository: twpayne/chezmoi
Pull request #42 (2018-11-30 15:15:16)

Improve handling of template errors


Patch:
diff --git a/cmd/keyring.go b/cmd/keyring.go
index bfb418fab..aeda5ff7f 100644
--- a/cmd/keyring.go
+++ b/cmd/keyring.go
@@ -1,7 +1,10 @@
 package cmd
 
 import (
+	"fmt"
+
 	"github.com/spf13/cobra"
+	"github.com/twpayne/chezmoi/lib/chezmoi"
 	"github.com/zalando/go-keyring"
 )
 
@@ -29,11 +32,13 @@ func init() {
 	persistentFlags.StringVar(&config.keyring.user, "user", "", "user")
 	keyringCommand.MarkPersistentFlagRequired("user")
 
-	config.addFunc("keyring", func(service, user string) string {
-		password, err := keyring.Get(service, user)
-		if err != nil {
-			return err.Error()
-		}
-		return password
-	})
+	config.addFunc("keyring", config.keyringFunc)
+}
+
+func (*Config) keyringFunc(service, user string) string {
+	password, err := keyring.Get(service, user)
+	if err != nil {
+		chezmoi.ReturnTemplateFuncError(fmt.Errorf("keyring %q %q: %v", service, user, err))
+	}
+	return password
 }
diff --git a/cmd/lastpass.go b/cmd/lastpass.go
index c9d707687..84c91f1d8 100644
--- a/cmd/lastpass.go
+++ b/cmd/lastpass.go
@@ -7,7 +7,8 @@ import (
 	"strings"
 
 	"github.com/spf13/cobra"
-	vfs "github.com/twpayne/go-vfs"
+	"github.com/twpayne/chezmoi/lib/chezmoi"
+	"github.com/twpayne/go-vfs"
 )
 
 var lastpassCommand = &cobra.Command{
@@ -31,7 +32,6 @@ func (c *Config) runLastPassCommand(fs vfs.FS, cmd *cobra.Command, args []string
 }
 
 func (c *Config) lastpassFunc(id string) interface{} {
-	// FIXME is there a better way to return errors from template funcs?
 	name := c.LastPass.Lpass
 	args := []string{"show", "-j", id}
 	if c.Verbose {
@@ -39,11 +39,11 @@ func (c *Config) lastpassFunc(id string) interface{} {
 	}
 	output, err := exec.Command(name, args...).CombinedOutput()
 	if err != nil {
-		return err
+		chezmoi.ReturnTemplateFuncError(fmt.Errorf("lastpass %q: %s show -j %q: %v\n%s", id, name, id, err, output))
 	}
 	var data []map[string]interface{}
 	if err := json.Unmarshal(output, &data); err != nil {
-		return err
+		chezmoi.ReturnTemplateFuncError(fmt.Errorf("lastpass %q: %s show -j %q: %v", id, name, id, err))
 	}
 	return data
 }
diff --git a/lib/chezmoi/chezmoi.go b/lib/chezmoi/chezmoi.go
index 4ac68df28..110d82f78 100644
--- a/lib/chezmoi/chezmoi.go
+++ b/lib/chezmoi/chezmoi.go
@@ -27,6 +27,13 @@ const (
 	templateSuffix   = ".tmpl"
 )
 
+// A templateFuncError is an error encountered while executing a template
+// function.
+type templateFuncError struct {
+	name string
+	err  error
+}
+
 // An Entry is either a Dir, a File, or a Symlink.
 type Entry interface {
 	SourceName() string
@@ -623,13 +630,26 @@ func (ts *TargetState) executeTemplate(fs vfs.FS, path string) ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
-	tmpl, err := template.New(path).Funcs(ts.Funcs).Parse(string(data))
+	return ts.executeTemplateData(path, data)
+}
+
+func (ts *TargetState) executeTemplateData(name string, data []byte) (_ []byte, err error) {
+	tmpl, err := template.New(name).Option("missingkey=error").Funcs(ts.Funcs).Parse(string(data))
 	if err != nil {
-		return nil, fmt.Errorf("%s: %v", path, err)
-	}
+		return nil, fmt.Errorf("%s: %v", name, err)
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			if tfe, ok := r.(templateFuncError); ok {
+				err = tfe.err
+			} else {
+				panic(r)
+			}
+		}
+	}()
 	output := &bytes.Buffer{}
-	if err := tmpl.Execute(output, ts.Data); err != nil {
-		return nil, fmt.Errorf("%s: %v", path, err)
+	if err = tmpl.Execute(output, ts.Data); err != nil {
+		return nil, fmt.Errorf("%s: %v", name, err)
 	}
 	return output.Bytes(), nil
 }
@@ -657,6 +677,13 @@ func (ts *TargetState) findEntry(name string) (Entry, error) {
 	return entries[names[len(names)-1]], nil
 }
 
+// ReturnTemplateFuncError causes template execution to return an error.
+func ReturnTemplateFuncError(err error) {
+	panic(templateFuncError{
+		err: err,
+	})
+}
+
 // parseSourceDirName parses a single directory name.
 func parseSourceDirName(dirName string) parsedSourceDirName {
 	perm := os.FileMode(0777)


Test patch:
diff --git a/lib/chezmoi/chezmoi_test.go b/lib/chezmoi/chezmoi_test.go
index 3838a606c..f4407f341 100644
--- a/lib/chezmoi/chezmoi_test.go
+++ b/lib/chezmoi/chezmoi_test.go
@@ -1,6 +1,7 @@
 package chezmoi
 
 import (
+	"errors"
 	"os"
 	"testing"
 	"text/template"
@@ -389,6 +390,28 @@ func TestTargetStatePopulate(t *testing.T) {
 	}
 }
 
+func TestReturnTemplateError(t *testing.T) {
+	funcs := map[string]interface{}{
+		"returnTemplateError": func() string {
+			ReturnTemplateFuncError(errors.New("error"))
+			return "foo"
+		},
+	}
+	for name, dataString := range map[string]string{
+		"syntax_error":         "{{",
+		"unknown_field":        "{{ .Unknown }}",
+		"unknown_func":         "{{ func }}",
+		"func_returning_error": "{{ returnTemplateError }}",
+	} {
+		t.Run(name, func(t *testing.T) {
+			ts := NewTargetState("/home/user", 0, "/home/user/.chezmoi", nil, funcs)
+			if got, err := ts.executeTemplateData(name, []byte(dataString)); err == nil {
+				t.Errorf("ts.executeTemplate(%q, %q) == %q, <nil>, want _, !<nil>", name, dataString, got)
+			}
+		})
+	}
+}
+
 func TestEndToEnd(t *testing.T) {
 	for _, tc := range []struct {
 		name      string