Repository: twpayne/chezmoi
Pull request #504 (2019-12-04 16:43:12)

Improve template errors


Patch:
diff --git a/cmd/secretbitwarden.go b/cmd/secretbitwarden.go
index e1f2a36ce..86d4d70d8 100644
--- a/cmd/secretbitwarden.go
+++ b/cmd/secretbitwarden.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 
 	"github.com/spf13/cobra"
+	"github.com/twpayne/chezmoi/internal/chezmoi"
 )
 
 var bitwardenCmd = &cobra.Command{
@@ -46,11 +47,11 @@ func (c *Config) bitwardenFunc(args ...string) interface{} {
 	cmd.Stderr = os.Stderr
 	output, err := c.mutator.IdempotentCmdOutput(cmd)
 	if err != nil {
-		panic(fmt.Errorf("bitwarden: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("bitwarden: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	var data interface{}
 	if err := json.Unmarshal(output, &data); err != nil {
-		panic(fmt.Errorf("bitwarden: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("bitwarden: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	bitwardenCache[key] = data
 	return data
diff --git a/cmd/secretgeneric.go b/cmd/secretgeneric.go
index 7c587b159..67b96b17a 100644
--- a/cmd/secretgeneric.go
+++ b/cmd/secretgeneric.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"github.com/spf13/cobra"
+	"github.com/twpayne/chezmoi/internal/chezmoi"
 )
 
 var genericSecretCmd = &cobra.Command{
@@ -49,7 +50,7 @@ func (c *Config) secretFunc(args ...string) interface{} {
 	cmd.Stderr = os.Stderr
 	output, err := c.mutator.IdempotentCmdOutput(cmd)
 	if err != nil {
-		panic(fmt.Errorf("secret: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("secret: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	value := bytes.TrimSpace(output)
 	secretCache[key] = value
@@ -67,11 +68,11 @@ func (c *Config) secretJSONFunc(args ...string) interface{} {
 	cmd.Stderr = os.Stderr
 	output, err := c.mutator.IdempotentCmdOutput(cmd)
 	if err != nil {
-		panic(fmt.Errorf("secretJSON: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("secretJSON: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	var value interface{}
 	if err := json.Unmarshal(output, &value); err != nil {
-		panic(fmt.Errorf("secretJSON: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("secretJSON: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	secretJSONCache[key] = value
 	return value
diff --git a/cmd/secretgopass.go b/cmd/secretgopass.go
index f07d59854..f459e9efe 100644
--- a/cmd/secretgopass.go
+++ b/cmd/secretgopass.go
@@ -4,9 +4,9 @@ import (
 	"bytes"
 	"fmt"
 	"os/exec"
-	"strings"
 
 	"github.com/spf13/cobra"
+	"github.com/twpayne/chezmoi/internal/chezmoi"
 )
 
 var gopassCmd = &cobra.Command{
@@ -42,7 +42,7 @@ func (c *Config) gopassFunc(id string) string {
 	cmd := exec.Command(name, args...)
 	output, err := c.mutator.IdempotentCmdOutput(cmd)
 	if err != nil {
-		panic(fmt.Errorf("gopass: %s %s: %w", name, strings.Join(args, " "), err))
+		panic(fmt.Errorf("gopass: %s %s: %w", name, chezmoi.ShellQuoteArgs(args), err))
 	}
 	var password string
 	if index := bytes.IndexByte(output, '\n'); index != -1 {
diff --git a/cmd/secretkeepassxc.go b/cmd/secretkeepassxc.go
index 292d38eea..91b4c343e 100644
--- a/cmd/secretkeepassxc.go
+++ b/cmd/secretkeepassxc.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 
 	"github.com/spf13/cobra"
+	"github.com/twpayne/chezmoi/internal/chezmoi"
 	"golang.org/x/crypto/ssh/terminal"
 )
 
@@ -64,11 +65,11 @@ func (c *Config) keePassXCFunc(entry string) map[string]string {
 	args = append(args, c.KeePassXC.Database, entry)
 	output, err := c.runKeePassXCCLICommand(name, args)
 	if err != nil {
-		panic(fmt.Errorf("keepassxc: %s %s: %w", name, strings.Join(args, " "), err))
+		panic(fmt.Errorf("keepassxc: %s %s: %w", name, chezmoi.ShellQuoteArgs(args), err))
 	}
 	data, err := parseKeyPassXCOutput(output)
 	if err != nil {
-		panic(fmt.Errorf("keepassxc: %s %s: %w", name, strings.Join(args, " "), err))
+		panic(fmt.Errorf("keepassxc: %s %s: %w", name, chezmoi.ShellQuoteArgs(args), err))
 	}
 	keePassXCCache[entry] = data
 	return data
@@ -91,7 +92,7 @@ func (c *Config) keePassXCAttributeFunc(entry, attribute string) string {
 	args = append(args, c.KeePassXC.Database, entry)
 	output, err := c.runKeePassXCCLICommand(name, args)
 	if err != nil {
-		panic(fmt.Errorf("keepassxc: %s %s: %w", name, strings.Join(args, " "), err))
+		panic(fmt.Errorf("keepassxc: %s %s: %w", name, chezmoi.ShellQuoteArgs(args), err))
 	}
 	outputStr := strings.TrimSpace(string(output))
 	keePassXCAttributeCache[key] = outputStr
diff --git a/cmd/secretonepassword.go b/cmd/secretonepassword.go
index fd90c1fc8..7b511a875 100644
--- a/cmd/secretonepassword.go
+++ b/cmd/secretonepassword.go
@@ -5,9 +5,9 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
-	"strings"
 
 	"github.com/spf13/cobra"
+	"github.com/twpayne/chezmoi/internal/chezmoi"
 )
 
 var onepasswordCmd = &cobra.Command{
@@ -49,11 +49,11 @@ func (c *Config) onepasswordFunc(item string) interface{} {
 	cmd.Stderr = os.Stderr
 	output, err := c.mutator.IdempotentCmdOutput(cmd)
 	if err != nil {
-		panic(fmt.Errorf("onepassword: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("onepassword: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	var data interface{}
 	if err := json.Unmarshal(output, &data); err != nil {
-		panic(fmt.Errorf("onepassword: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("onepassword: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	onepasswordCache[item] = data
 	return data
@@ -70,7 +70,7 @@ func (c *Config) onepasswordDocumentFunc(item string) interface{} {
 	cmd.Stderr = os.Stderr
 	output, err := c.mutator.IdempotentCmdOutput(cmd)
 	if err != nil {
-		panic(fmt.Errorf("onepassword: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("onepassword: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	onepasswordDocumentCache[item] = string(output)
 	return string(output)
diff --git a/cmd/secretpass.go b/cmd/secretpass.go
index 61e45262e..1a6431820 100644
--- a/cmd/secretpass.go
+++ b/cmd/secretpass.go
@@ -4,9 +4,9 @@ import (
 	"bytes"
 	"fmt"
 	"os/exec"
-	"strings"
 
 	"github.com/spf13/cobra"
+	"github.com/twpayne/chezmoi/internal/chezmoi"
 )
 
 var passCmd = &cobra.Command{
@@ -42,7 +42,7 @@ func (c *Config) passFunc(id string) string {
 	cmd := exec.Command(name, args...)
 	output, err := c.mutator.IdempotentCmdOutput(cmd)
 	if err != nil {
-		panic(fmt.Errorf("pass: %s %s: %w", name, strings.Join(args, " "), err))
+		panic(fmt.Errorf("pass: %s %s: %w", name, chezmoi.ShellQuoteArgs(args), err))
 	}
 	var password string
 	if index := bytes.IndexByte(output, '\n'); index != -1 {
diff --git a/cmd/secretvault.go b/cmd/secretvault.go
index 8e01e4486..d00ed0953 100644
--- a/cmd/secretvault.go
+++ b/cmd/secretvault.go
@@ -5,9 +5,9 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
-	"strings"
 
 	"github.com/spf13/cobra"
+	"github.com/twpayne/chezmoi/internal/chezmoi"
 )
 
 var vaultCmd = &cobra.Command{
@@ -45,11 +45,11 @@ func (c *Config) vaultFunc(key string) interface{} {
 	cmd.Stderr = os.Stderr
 	output, err := c.mutator.IdempotentCmdOutput(cmd)
 	if err != nil {
-		panic(fmt.Errorf("vault: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("vault: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	var data interface{}
 	if err := json.Unmarshal(output, &data); err != nil {
-		panic(fmt.Errorf("vault: %s %s: %w\n%s", name, strings.Join(args, " "), err, output))
+		panic(fmt.Errorf("vault: %s %s: %w\n%s", name, chezmoi.ShellQuoteArgs(args), err, output))
 	}
 	vaultCache[key] = data
 	return data
diff --git a/internal/chezmoi/maybeshellquote.go b/internal/chezmoi/maybeshellquote.go
index 7ba63fa91..2491086f4 100644
--- a/internal/chezmoi/maybeshellquote.go
+++ b/internal/chezmoi/maybeshellquote.go
@@ -2,6 +2,7 @@ package chezmoi
 
 import (
 	"regexp"
+	"strings"
 )
 
 var needShellQuoteRegexp = regexp.MustCompile(`[^+\-./0-9A-Z_a-z]`)
@@ -11,8 +12,8 @@ const (
 	singleQuote = '\''
 )
 
-// maybeShellQuote returns s quoted as a shell argument, if necessary.
-func maybeShellQuote(s string) string {
+// MaybeShellQuote returns s quoted as a shell argument, if necessary.
+func MaybeShellQuote(s string) string {
 	switch {
 	case s == "":
 		return "''"
@@ -49,3 +50,12 @@ func maybeShellQuote(s string) string {
 		return s
 	}
 }
+
+// ShellQuoteArgs returs args shell quoted and joined into a single string.
+func ShellQuoteArgs(args []string) string {
+	shellQuotedArgs := make([]string, 0, len(args))
+	for _, arg := range args {
+		shellQuotedArgs = append(shellQuotedArgs, MaybeShellQuote(arg))
+	}
+	return strings.Join(shellQuotedArgs, " ")
+}
diff --git a/internal/chezmoi/verbosemutator.go b/internal/chezmoi/verbosemutator.go
index 543a1c1bd..f18399002 100644
--- a/internal/chezmoi/verbosemutator.go
+++ b/internal/chezmoi/verbosemutator.go
@@ -34,7 +34,7 @@ func NewVerboseMutator(w io.Writer, m Mutator, colored bool) *VerboseMutator {
 
 // Chmod implements Mutator.Chmod.
 func (m *VerboseMutator) Chmod(name string, mode os.FileMode) error {
-	action := fmt.Sprintf("chmod %o %s", mode, maybeShellQuote(name))
+	action := fmt.Sprintf("chmod %o %s", mode, MaybeShellQuote(name))
 	err := m.m.Chmod(name, mode)
 	if err == nil {
 		_, _ = fmt.Fprintln(m.w, action)
@@ -58,7 +58,7 @@ func (m *VerboseMutator) IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) {
 
 // Mkdir implements Mutator.Mkdir.
 func (m *VerboseMutator) Mkdir(name string, perm os.FileMode) error {
-	action := fmt.Sprintf("mkdir -m %o %s", perm, maybeShellQuote(name))
+	action := fmt.Sprintf("mkdir -m %o %s", perm, MaybeShellQuote(name))
 	err := m.m.Mkdir(name, perm)
 	if err == nil {
 		_, _ = fmt.Fprintln(m.w, action)
@@ -70,7 +70,7 @@ func (m *VerboseMutator) Mkdir(name string, perm os.FileMode) error {
 
 // RemoveAll implements Mutator.RemoveAll.
 func (m *VerboseMutator) RemoveAll(name string) error {
-	action := fmt.Sprintf("rm -rf %s", maybeShellQuote(name))
+	action := fmt.Sprintf("rm -rf %s", MaybeShellQuote(name))
 	err := m.m.RemoveAll(name)
 	if err == nil {
 		_, _ = fmt.Fprintln(m.w, action)
@@ -82,7 +82,7 @@ func (m *VerboseMutator) RemoveAll(name string) error {
 
 // Rename implements Mutator.Rename.
 func (m *VerboseMutator) Rename(oldpath, newpath string) error {
-	action := fmt.Sprintf("mv %s %s", maybeShellQuote(oldpath), maybeShellQuote(newpath))
+	action := fmt.Sprintf("mv %s %s", MaybeShellQuote(oldpath), MaybeShellQuote(newpath))
 	err := m.m.Rename(oldpath, newpath)
 	if err == nil {
 		_, _ = fmt.Fprintln(m.w, action)
@@ -111,7 +111,7 @@ func (m *VerboseMutator) Stat(name string) (os.FileInfo, error) {
 
 // WriteFile implements Mutator.WriteFile.
 func (m *VerboseMutator) WriteFile(name string, data []byte, perm os.FileMode, currData []byte) error {
-	action := fmt.Sprintf("install -m %o /dev/null %s", perm, maybeShellQuote(name))
+	action := fmt.Sprintf("install -m %o /dev/null %s", perm, MaybeShellQuote(name))
 	err := m.m.WriteFile(name, data, perm, currData)
 	if err == nil {
 		_, _ = fmt.Fprintln(m.w, action)
@@ -147,7 +147,7 @@ func (m *VerboseMutator) WriteFile(name string, data []byte, perm os.FileMode, c
 
 // WriteSymlink implements Mutator.WriteSymlink.
 func (m *VerboseMutator) WriteSymlink(oldname, newname string) error {
-	action := fmt.Sprintf("ln -sf %s %s", maybeShellQuote(oldname), maybeShellQuote(newname))
+	action := fmt.Sprintf("ln -sf %s %s", MaybeShellQuote(oldname), MaybeShellQuote(newname))
 	err := m.m.WriteSymlink(oldname, newname)
 	if err == nil {
 		_, _ = fmt.Fprintln(m.w, action)
@@ -159,15 +159,11 @@ func (m *VerboseMutator) WriteSymlink(oldname, newname string) error {
 
 // cmdString returns a string representation of cmd.
 func cmdString(cmd *exec.Cmd) string {
-	components := append([]string{cmd.Path}, cmd.Args[1:]...)
-	for i, component := range components {
-		components[i] = maybeShellQuote(component)
-	}
-	s := strings.Join(components, " ")
+	s := ShellQuoteArgs(append([]string{cmd.Path}, cmd.Args[1:]...))
 	if cmd.Dir == "" {
 		return s
 	}
-	return fmt.Sprintf("( cd %s && %s )", maybeShellQuote(cmd.Dir), s)
+	return fmt.Sprintf("( cd %s && %s )", MaybeShellQuote(cmd.Dir), s)
 }
 
 func isBinary(data []byte) bool {


Test patch:
diff --git a/internal/chezmoi/maybeshellquote_test.go b/internal/chezmoi/maybeshellquote_test.go
index 195ddc8e4..30c2dbc16 100644
--- a/internal/chezmoi/maybeshellquote_test.go
+++ b/internal/chezmoi/maybeshellquote_test.go
@@ -19,6 +19,6 @@ func TestMaybeShellQuote(t *testing.T) {
 		`a/b`: `a/b`,
 		`a b`: `'a b'`,
 	} {
-		assert.Equal(t, expected, maybeShellQuote(s), "quoting %q", s)
+		assert.Equal(t, expected, MaybeShellQuote(s), "quoting %q", s)
 	}
 }