diff --git a/assets/chezmoi.io/docs/reference/target-types.md b/assets/chezmoi.io/docs/reference/target-types.md index 88aa24b7287..a2489373da2 100644 --- a/assets/chezmoi.io/docs/reference/target-types.md +++ b/assets/chezmoi.io/docs/reference/target-types.md @@ -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 diff --git a/assets/chezmoi.io/docs/user-guide/manage-different-types-of-file.md b/assets/chezmoi.io/docs/user-guide/manage-different-types-of-file.md index 6b8f4169571..cb444550d01 100644 --- a/assets/chezmoi.io/docs/user-guide/manage-different-types-of-file.md +++ b/assets/chezmoi.io/docs/user-guide/manage-different-types-of-file.md @@ -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 diff --git a/pkg/chezmoi/sourcestate.go b/pkg/chezmoi/sourcestate.go index 95bd4c4529f..a631c93aee6 100644 --- a/pkg/chezmoi/sourcestate.go +++ b/pkg/chezmoi/sourcestate.go @@ -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+)`) ) @@ -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 } @@ -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 { @@ -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 @@ -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) +} diff --git a/pkg/cmd/testdata/scripts/modify_unix.txtar b/pkg/cmd/testdata/scripts/modify_unix.txtar index b99cc3a6fc1..d100deca2e8 100644 --- a/pkg/cmd/testdata/scripts/modify_unix.txtar +++ b/pkg/cmd/testdata/scripts/modify_unix.txtar @@ -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 @@ -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