Skip to content

Commit

Permalink
Improve variable substitution, fixes #89
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Jan 10, 2019
1 parent bb05d54 commit 555ea41
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 15 deletions.
28 changes: 23 additions & 5 deletions lib/chezmoi/autotemplate.go
@@ -1,7 +1,6 @@
package chezmoi

import (
"regexp"
"sort"
"strings"
)
Expand Down Expand Up @@ -45,15 +44,34 @@ func extractVariables(variables []templateVariable, parent []string, data map[st
func autoTemplate(contents []byte, data map[string]interface{}) ([]byte, error) {
// FIXME this naive approach will generate incorrect templates if the
// variable names match variable values
// FIXME the algorithm here is probably O(N^2), we can do better
variables := extractVariables(nil, nil, data)
sort.Sort(sort.Reverse(byValueLength(variables)))
contentsStr := string(contents)
for _, variable := range variables {
valueRegexp, err := regexp.Compile(`\b` + regexp.QuoteMeta(variable.value) + `\b`)
if err != nil {
return nil, err
index := strings.Index(contentsStr, variable.value)
for index != -1 && index != len(contentsStr) {
if !inWord(contentsStr, index) && !inWord(contentsStr, index+len(variable.value)) {
// Replace variable.value which is on word boundaries at both
// ends.
replacement := "{{ ." + variable.name + " }}"
contentsStr = contentsStr[:index] + replacement + contentsStr[index+len(variable.value):]
index += len(replacement)
} else {
// Otherwise, keep looking. Consume at least one byte so we
// make progress.
index++
}
// Look for the next occurrence of variable.value.
j := strings.Index(contentsStr[index:], variable.value)
if j == -1 {
// No more occurrences found, so terminate the loop.
break
} else {
// Advance to the next occurrence.
index += j
}
}
contentsStr = valueRegexp.ReplaceAllString(contentsStr, "{{ ."+variable.name+" }}")
}
return []byte(contentsStr), nil
}
Expand Down
65 changes: 55 additions & 10 deletions lib/chezmoi/autotemplate_test.go
Expand Up @@ -54,17 +54,62 @@ func TestAutoTemplate(t *testing.T) {
},
wantStr: "darwinian evolution", // not "{{ .os }}ian evolution"
},
/*
// FIXME this test currently fails because we match on word
// boundaries and ^/ is not a word boundary.
{
contentsStr: "/home/user",
data: map[string]interface{}{
"homedir": "/home/user",
},
wantStr: "{{ .homedir }}",
{
name: "longest_match_first",
contentsStr: "/home/user",
data: map[string]interface{}{
"homedir": "/home/user",
},
wantStr: "{{ .homedir }}",
},
{
name: "longest_match_first_prefix",
contentsStr: "HOME=/home/user",
data: map[string]interface{}{
"homedir": "/home/user",
},
wantStr: "HOME={{ .homedir }}",
},
{
name: "longest_match_first_suffix",
contentsStr: "/home/user/something",
data: map[string]interface{}{
"homedir": "/home/user",
},
wantStr: "{{ .homedir }}/something",
},
{
name: "longest_match_first_prefix_and_suffix",
contentsStr: "HOME=/home/user/something",
data: map[string]interface{}{
"homedir": "/home/user",
},
wantStr: "HOME={{ .homedir }}/something",
},
{
name: "words_only",
contentsStr: "aaa aa a aa aaa aa a aa aaa",
data: map[string]interface{}{
"alpha": "a",
},
wantStr: "aaa aa {{ .alpha }} aa aaa aa {{ .alpha }} aa aaa",
},
{
name: "words_only_2",
contentsStr: "aaa aa a aa aaa aa a aa aaa",
data: map[string]interface{}{
"alpha": "aa",
},
*/
wantStr: "aaa {{ .alpha }} a {{ .alpha }} aaa {{ .alpha }} a {{ .alpha }} aaa",
},
{
name: "words_only_3",
contentsStr: "aaa aa a aa aaa aa a aa aaa",
data: map[string]interface{}{
"alpha": "aaa",
},
wantStr: "{{ .alpha }} aa a aa {{ .alpha }} aa a aa {{ .alpha }}",
},
} {
t.Run(tc.name, func(t *testing.T) {
got, gotErr := autoTemplate([]byte(tc.contentsStr), tc.data)
Expand Down

0 comments on commit 555ea41

Please sign in to comment.