Skip to content

Commit c188776

Browse files
authored
Merge pull request #42 from twpayne/template-errors
Improve handling of template errors
2 parents d305307 + 841d2e7 commit c188776

File tree

4 files changed

+71
-16
lines changed

4 files changed

+71
-16
lines changed

cmd/keyring.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package cmd
22

33
import (
4+
"fmt"
5+
46
"github.com/spf13/cobra"
7+
"github.com/twpayne/chezmoi/lib/chezmoi"
58
"github.com/zalando/go-keyring"
69
)
710

@@ -29,11 +32,13 @@ func init() {
2932
persistentFlags.StringVar(&config.keyring.user, "user", "", "user")
3033
keyringCommand.MarkPersistentFlagRequired("user")
3134

32-
config.addFunc("keyring", func(service, user string) string {
33-
password, err := keyring.Get(service, user)
34-
if err != nil {
35-
return err.Error()
36-
}
37-
return password
38-
})
35+
config.addFunc("keyring", config.keyringFunc)
36+
}
37+
38+
func (*Config) keyringFunc(service, user string) string {
39+
password, err := keyring.Get(service, user)
40+
if err != nil {
41+
chezmoi.ReturnTemplateFuncError(fmt.Errorf("keyring %q %q: %v", service, user, err))
42+
}
43+
return password
3944
}

cmd/lastpass.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import (
77
"strings"
88

99
"github.com/spf13/cobra"
10-
vfs "github.com/twpayne/go-vfs"
10+
"github.com/twpayne/chezmoi/lib/chezmoi"
11+
"github.com/twpayne/go-vfs"
1112
)
1213

1314
var lastpassCommand = &cobra.Command{
@@ -31,19 +32,18 @@ func (c *Config) runLastPassCommand(fs vfs.FS, cmd *cobra.Command, args []string
3132
}
3233

3334
func (c *Config) lastpassFunc(id string) interface{} {
34-
// FIXME is there a better way to return errors from template funcs?
3535
name := c.LastPass.Lpass
3636
args := []string{"show", "-j", id}
3737
if c.Verbose {
3838
fmt.Printf("%s %s\n", name, strings.Join(args, " "))
3939
}
4040
output, err := exec.Command(name, args...).CombinedOutput()
4141
if err != nil {
42-
return err
42+
chezmoi.ReturnTemplateFuncError(fmt.Errorf("lastpass %q: %s show -j %q: %v\n%s", id, name, id, err, output))
4343
}
4444
var data []map[string]interface{}
4545
if err := json.Unmarshal(output, &data); err != nil {
46-
return err
46+
chezmoi.ReturnTemplateFuncError(fmt.Errorf("lastpass %q: %s show -j %q: %v", id, name, id, err))
4747
}
4848
return data
4949
}

lib/chezmoi/chezmoi.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ const (
2727
templateSuffix = ".tmpl"
2828
)
2929

30+
// A templateFuncError is an error encountered while executing a template
31+
// function.
32+
type templateFuncError struct {
33+
name string
34+
err error
35+
}
36+
3037
// An Entry is either a Dir, a File, or a Symlink.
3138
type Entry interface {
3239
SourceName() string
@@ -623,13 +630,26 @@ func (ts *TargetState) executeTemplate(fs vfs.FS, path string) ([]byte, error) {
623630
if err != nil {
624631
return nil, err
625632
}
626-
tmpl, err := template.New(path).Funcs(ts.Funcs).Parse(string(data))
633+
return ts.executeTemplateData(path, data)
634+
}
635+
636+
func (ts *TargetState) executeTemplateData(name string, data []byte) (_ []byte, err error) {
637+
tmpl, err := template.New(name).Option("missingkey=error").Funcs(ts.Funcs).Parse(string(data))
627638
if err != nil {
628-
return nil, fmt.Errorf("%s: %v", path, err)
629-
}
639+
return nil, fmt.Errorf("%s: %v", name, err)
640+
}
641+
defer func() {
642+
if r := recover(); r != nil {
643+
if tfe, ok := r.(templateFuncError); ok {
644+
err = tfe.err
645+
} else {
646+
panic(r)
647+
}
648+
}
649+
}()
630650
output := &bytes.Buffer{}
631-
if err := tmpl.Execute(output, ts.Data); err != nil {
632-
return nil, fmt.Errorf("%s: %v", path, err)
651+
if err = tmpl.Execute(output, ts.Data); err != nil {
652+
return nil, fmt.Errorf("%s: %v", name, err)
633653
}
634654
return output.Bytes(), nil
635655
}
@@ -657,6 +677,13 @@ func (ts *TargetState) findEntry(name string) (Entry, error) {
657677
return entries[names[len(names)-1]], nil
658678
}
659679

680+
// ReturnTemplateFuncError causes template execution to return an error.
681+
func ReturnTemplateFuncError(err error) {
682+
panic(templateFuncError{
683+
err: err,
684+
})
685+
}
686+
660687
// parseSourceDirName parses a single directory name.
661688
func parseSourceDirName(dirName string) parsedSourceDirName {
662689
perm := os.FileMode(0777)

lib/chezmoi/chezmoi_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package chezmoi
22

33
import (
4+
"errors"
45
"os"
56
"testing"
67
"text/template"
@@ -389,6 +390,28 @@ func TestTargetStatePopulate(t *testing.T) {
389390
}
390391
}
391392

393+
func TestReturnTemplateError(t *testing.T) {
394+
funcs := map[string]interface{}{
395+
"returnTemplateError": func() string {
396+
ReturnTemplateFuncError(errors.New("error"))
397+
return "foo"
398+
},
399+
}
400+
for name, dataString := range map[string]string{
401+
"syntax_error": "{{",
402+
"unknown_field": "{{ .Unknown }}",
403+
"unknown_func": "{{ func }}",
404+
"func_returning_error": "{{ returnTemplateError }}",
405+
} {
406+
t.Run(name, func(t *testing.T) {
407+
ts := NewTargetState("/home/user", 0, "/home/user/.chezmoi", nil, funcs)
408+
if got, err := ts.executeTemplateData(name, []byte(dataString)); err == nil {
409+
t.Errorf("ts.executeTemplate(%q, %q) == %q, <nil>, want _, !<nil>", name, dataString, got)
410+
}
411+
})
412+
}
413+
}
414+
392415
func TestEndToEnd(t *testing.T) {
393416
for _, tc := range []struct {
394417
name string

0 commit comments

Comments
 (0)