Skip to content

Commit 3788d1b

Browse files
authored
Merge pull request #549 from twpayne/improve-template-docs
Validate variable names in config data
2 parents cf6b4a7 + 16e1fa9 commit 3788d1b

File tree

5 files changed

+91
-4
lines changed

5 files changed

+91
-4
lines changed

cmd/config.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os/exec"
1212
"os/user"
1313
"path/filepath"
14+
"regexp"
1415
"runtime"
1516
"strings"
1617
"text/template"
@@ -105,6 +106,8 @@ var (
105106
}
106107

107108
templatesBox = packr.New("templates", "../templates")
109+
110+
identifierRegexp = regexp.MustCompile(`\A[\pL_][\pL\p{Nd}_]*\z`)
108111
)
109112

110113
// Stderr returns c's stderr.
@@ -479,6 +482,10 @@ func (c *Config) runEditor(argv ...string) error {
479482
return c.run("", c.getEditor(), argv...)
480483
}
481484

485+
func (c *Config) validateData() error {
486+
return validateKeys(config.Data, identifierRegexp)
487+
}
488+
482489
func (c *Config) warn(s string) {
483490
fmt.Fprintf(c.Stderr(), "warning: %s\n", s)
484491
}
@@ -552,3 +559,25 @@ func upperSnakeCaseToCamelCaseMap(m map[string]string) map[string]string {
552559
}
553560
return result
554561
}
562+
563+
// validateKeys ensures that all keys in data match re.
564+
func validateKeys(data interface{}, re *regexp.Regexp) error {
565+
switch data := data.(type) {
566+
case map[string]interface{}:
567+
for key, value := range data {
568+
if !re.MatchString(key) {
569+
return fmt.Errorf("invalid key: %q", key)
570+
}
571+
if err := validateKeys(value, re); err != nil {
572+
return err
573+
}
574+
}
575+
case []interface{}:
576+
for _, value := range data {
577+
if err := validateKeys(value, re); err != nil {
578+
return err
579+
}
580+
}
581+
}
582+
return nil
583+
}

cmd/config_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,58 @@ func TestUpperSnakeCaseToCamelCase(t *testing.T) {
7777
}
7878
}
7979

80+
func TestValidateKeys(t *testing.T) {
81+
for _, tc := range []struct {
82+
data interface{}
83+
wantErr bool
84+
}{
85+
{
86+
data: nil,
87+
wantErr: false,
88+
},
89+
{
90+
data: map[string]interface{}{
91+
"foo": "bar",
92+
"a": 0,
93+
"_x9": false,
94+
"ThisVariableIsExported": nil,
95+
"αβ": "",
96+
},
97+
wantErr: false,
98+
},
99+
{
100+
data: map[string]interface{}{
101+
"foo-foo": "bar",
102+
},
103+
wantErr: true,
104+
},
105+
{
106+
data: map[string]interface{}{
107+
"foo": map[string]interface{}{
108+
"bar-bar": "baz",
109+
},
110+
},
111+
wantErr: true,
112+
},
113+
{
114+
data: map[string]interface{}{
115+
"foo": []interface{}{
116+
map[string]interface{}{
117+
"bar-bar": "baz",
118+
},
119+
},
120+
},
121+
wantErr: true,
122+
},
123+
} {
124+
if tc.wantErr {
125+
assert.Error(t, validateKeys(tc.data, identifierRegexp))
126+
} else {
127+
assert.NoError(t, validateKeys(tc.data, identifierRegexp))
128+
}
129+
}
130+
}
131+
80132
//nolint:unparam
81133
func newTestBaseDirectorySpecification(homeDir string) *xdg.BaseDirectorySpecification {
82134
return &xdg.BaseDirectorySpecification{

cmd/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ func init() {
111111
if config.err == nil {
112112
config.err = viper.Unmarshal(&config)
113113
}
114+
if config.err == nil {
115+
config.err = config.validateData()
116+
}
114117
if config.err != nil {
115118
config.warn(fmt.Sprintf("%s: %v", config.configFile, config.err))
116119
}

docs/HOWTO.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ Whereas at work it might be:
106106
email = "john.smith@company.com"
107107

108108
To handle this, on each machine create a configuration file called
109-
`~/.config/chezmoi/chezmoi.toml` defining what might change. For your home
110-
machine:
109+
`~/.config/chezmoi/chezmoi.toml` defining variables that might vary from machine
110+
to machine. For example, for your home machine:
111111

112112
[data]
113113
email = "john@home.org"
@@ -117,7 +117,8 @@ If you intend to store private data (e.g. access tokens) in
117117

118118
If you prefer, you can use any format supported by
119119
[Viper](https://github.com/spf13/viper) for your configuration file. This
120-
includes JSON, YAML, and TOML.
120+
includes JSON, YAML, and TOML. Variable names must start with a letter and be
121+
followed by zero or more letters or digits.
121122

122123
Then, add `~/.gitconfig` to chezmoi using the `-T` flag to automatically turn
123124
it in to a template:
@@ -160,7 +161,7 @@ chezmoi includes all of the hermetic text functions from
160161
If, after executing the template, the file contents are empty, the target file
161162
will be removed. This can be used to ensure that files are only present on
162163
certain machines. If you want an empty file to be created anyway, you will need
163-
to give it an `empty_` prefix. See "Under the hood" below.
164+
to give it an `empty_` prefix.
164165

165166
For coarser-grained control of files and entire directories are managed on
166167
different machines, or to exclude certain files completely, you can create

docs/REFERENCE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,8 @@ chezmoi provides the following automatically populated variables:
727727
| `.chezmoi.username` | The username of the user running chezmoi. |
728728

729729
Additional variables can be defined in the config file in the `data` section.
730+
Variable names must consist of a letter and be followed by zero or more letters
731+
and/or digits.
730732

731733
## Template functions
732734

0 commit comments

Comments
 (0)