Skip to content

Commit

Permalink
feat: Allow modify_ scripts to be executed as templates
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Nov 1, 2022
1 parent 4748397 commit 3f3bb87
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 18 deletions.
13 changes: 10 additions & 3 deletions assets/chezmoi.io/docs/reference/target-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@ unchanged.
### Modify file

Files with the `modify_` prefix are treated as scripts that modify an existing
file. The contents of the existing file (which maybe empty if the existing file
does not exist or is empty) are passed to the script's standard input, and the
new contents are read from the script's standard output.
file.

If the file contains a line with the text `chezmoi:modify-template` then that
line is removed and the rest of the script is executed template with the
existing file's contents passed as a string in `.chezmoi.stdin`. The result of
executing the template are the new contents of the file.

Otherwise, the contents of the existing file (which maybe empty if the existing
file does not exist or is empty) are passed to the script's standard input, and
the new contents are read from the script's standard output.

### Remove entry

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ example using `sed`.
will be empty and it is the script's responsibility to write a complete
file to the standard output.

`modify_` scripts that contain the string `chezmoi:modify-template` are
executed as templates with the current contents of the file passed as
`.chezmoi.stdin` and the result of the template execution used as the new
contents of the file.

!!! example

To replace the string `old` with `new` in a file while leaving the rest of
the file unchanged, use the modify script:

```
{{- /* chezmoi:modify-template */ -}}
{{- .chezmoi.stdin | replaceAllRegex "old" "new" }}
```

Secondly, if only a small part of the file changes then consider using a
template to re-generate the full contents of the file from the current state.
For example, Kubernetes configurations include a current context that can be
Expand Down
64 changes: 49 additions & 15 deletions pkg/chezmoi/sourcestate.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const (
)

var (
modifyTemplateRx = regexp.MustCompile(`(?m)^.*chezmoi:modify-template.*$(?:\r?\n)?`)
templateDirectiveRx = regexp.MustCompile(`(?m)^.*?chezmoi:template:(.*)$(?:\r?\n)?`)
templateDirectiveKeyValuePairRx = regexp.MustCompile(`\s*(\S+)=("(?:[^"]|\\")*"|\S+)`)
)
Expand Down Expand Up @@ -1254,12 +1255,9 @@ func (s *SourceState) addTemplatesDir(ctx context.Context, templatesDirAbsPath A
templateRelPath := templateAbsPath.MustTrimDirPrefix(templatesDirAbsPath)
name := templateRelPath.String()

tmpl, err := ParseTemplate(
name,
contents,
s.templateFuncs,
TemplateOptions{Options: append([]string(nil), s.templateOptions...)},
)
tmpl, err := ParseTemplate(name, contents, s.templateFuncs, TemplateOptions{
Options: append([]string(nil), s.templateOptions...),
})
if err != nil {
return err
}
Expand Down Expand Up @@ -1537,6 +1535,39 @@ func (s *SourceState) newModifyTargetStateEntryFunc(
return
}

// If the modifier contains chezmoi:modify-template then execute it
// as a template.
if matches := modifyTemplateRx.FindAllSubmatchIndex(modifierContents, -1); matches != nil {
sourceFile := sourceRelPath.String()
templateContents := removeMatches(modifierContents, matches)
var tmpl *template.Template
tmpl, err = ParseTemplate(sourceFile, templateContents, s.templateFuncs, TemplateOptions{
Options: append([]string(nil), s.templateOptions...),
})
if err != nil {
return
}

// Temporarily set .chezmoi.stdin to the current contents and
// .chezmoi.sourceFile to the name of the template.
templateData := s.TemplateData()
if chezmoiTemplateData, ok := templateData["chezmoi"].(map[string]any); ok {
chezmoiTemplateData["stdin"] = string(currentContents)
chezmoiTemplateData["sourceFile"] = sourceFile
defer func() {
delete(chezmoiTemplateData, "stdin")
delete(chezmoiTemplateData, "sourceFile")
}()
}

var builder strings.Builder
if err = tmpl.Execute(&builder, templateData); err != nil {
return
}
contents = []byte(builder.String())
return
}

// Write the modifier to a temporary file.
var tempFile *os.File
if tempFile, err = os.CreateTemp("", "*."+fileAttr.TargetName); err != nil {
Expand Down Expand Up @@ -2215,15 +2246,7 @@ func (o *TemplateOptions) parseDirectives(data []byte) []byte {
}
}

// Remove lines containing directives.
slices := make([][]byte, 0, len(directiveMatches)+1)
slices = append(slices, data[:directiveMatches[0][0]])
for i, directiveMatch := range directiveMatches[1:] {
slices = append(slices, data[directiveMatches[i][1]:directiveMatch[0]])
}
slices = append(slices, data[directiveMatches[len(directiveMatches)-1][1]:])

return bytes.Join(slices, nil)
return removeMatches(data, directiveMatches)
}

// allEquivalentDirs returns if sourceStateEntries are all equivalent
Expand All @@ -2244,3 +2267,14 @@ func allEquivalentDirs(sourceStateEntries []SourceStateEntry) bool {
}
return true
}

// removeMatches returns data with matchesIndexes removed.
func removeMatches(data []byte, matchesIndexes [][]int) []byte {
slices := make([][]byte, 0, len(matchesIndexes)+1)
slices = append(slices, data[:matchesIndexes[0][0]])
for i, matchIndexes := range matchesIndexes[1:] {
slices = append(slices, data[matchesIndexes[i][1]:matchIndexes[0]])
}
slices = append(slices, data[matchesIndexes[len(matchesIndexes)-1][1]:])
return bytes.Join(slices, nil)
}
13 changes: 13 additions & 0 deletions pkg/cmd/testdata/scripts/modify_unix.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ chhome home4/user
exec chezmoi cat $HOME${/}.modify
cmp stdout golden/.modified

chhome home5/user

# test that modify scripts can be modify-templates
exec chezmoi cat $HOME${/}.modify
cmp stdout golden/.modified

-- golden/.edited-and-modified --
beginning
modified-middle
Expand Down Expand Up @@ -110,3 +116,10 @@ end
beginning
middle
end
-- home5/user/.local/share/chezmoi/modify_dot_modify --
{{- /* chezmoi:modify-template */ -}}
{{- .chezmoi.stdin | replaceAllRegex "middle" "modified-middle" -}}
-- home5/user/.modify --
beginning
middle
end

0 comments on commit 3f3bb87

Please sign in to comment.