diff --git a/internal/chezmoi/sourcestate.go b/internal/chezmoi/sourcestate.go index 8b67e4ad25e..d8a2323cd64 100644 --- a/internal/chezmoi/sourcestate.go +++ b/internal/chezmoi/sourcestate.go @@ -145,7 +145,8 @@ type SourceState struct { userTemplateData map[string]any priorityTemplateData map[string]any templateData map[string]any - templateFuncs template.FuncMap + sprigTemplateFuncs template.FuncMap + sproutTemplateFuncs template.FuncMap templateOptions []string templates map[string]*Template externals map[RelPath][]*External @@ -268,10 +269,17 @@ func WithTemplateDataOnly(templateDataOnly bool) SourceStateOption { } } -// WithTemplateFuncs sets the template functions. -func WithTemplateFuncs(templateFuncs template.FuncMap) SourceStateOption { +// WithSprigTemplateFuncs sets the Sprig template functions. +func WithSprigTemplateFuncs(sprigTemplateFuncs template.FuncMap) SourceStateOption { return func(s *SourceState) { - s.templateFuncs = templateFuncs + s.sprigTemplateFuncs = sprigTemplateFuncs + } +} + +// WithSproutTemplateFuncs sets the Sprout template functions. +func WithSproutTemplateFuncs(sproutTemplateFuncs template.FuncMap) SourceStateOption { + return func(s *SourceState) { + s.sproutTemplateFuncs = sproutTemplateFuncs } } @@ -846,7 +854,8 @@ type ExecuteTemplateDataOptions struct { // ExecuteTemplateData returns the result of executing template data. func (s *SourceState) ExecuteTemplateData(options ExecuteTemplateDataOptions) ([]byte, error) { templateOptions := options.TemplateOptions - templateOptions.Funcs = s.templateFuncs + templateOptions.SprigFuncs = s.sprigTemplateFuncs + templateOptions.SproutFuncs = s.sproutTemplateFuncs templateOptions.Options = slices.Clone(s.templateOptions) tmpl, err := ParseTemplate(options.NameRelPath.String(), options.Data, templateOptions) @@ -1601,8 +1610,9 @@ func (s *SourceState) addTemplatesDir(ctx context.Context, templatesDirAbsPath A name := templateRelPath.String() tmpl, err := ParseTemplate(name, contents, TemplateOptions{ - Funcs: s.templateFuncs, - Options: slices.Clone(s.templateOptions), + SprigFuncs: s.sprigTemplateFuncs, + SproutFuncs: s.sproutTemplateFuncs, + Options: slices.Clone(s.templateOptions), }) if err != nil { return err @@ -2028,8 +2038,9 @@ func (s *SourceState) newModifyTargetStateEntryFunc( templateContents := removeMatches(modifierContents, matches) var tmpl *Template tmpl, err = ParseTemplate(sourceFile, templateContents, TemplateOptions{ - Funcs: s.templateFuncs, - Options: slices.Clone(s.templateOptions), + SprigFuncs: s.sprigTemplateFuncs, + SproutFuncs: s.sproutTemplateFuncs, + Options: slices.Clone(s.templateOptions), }) if err != nil { return nil, err diff --git a/internal/chezmoi/template.go b/internal/chezmoi/template.go index 87f42025f56..7eaf32ad28c 100644 --- a/internal/chezmoi/template.go +++ b/internal/chezmoi/template.go @@ -17,6 +17,15 @@ import ( "golang.org/x/text/encoding/unicode" ) +// A TemplateFunctions indicates a set of template functions. +type TemplateFunctions int + +// Template functions. +const ( + TemplateFunctionsSprig TemplateFunctions = iota + TemplateFunctionsSprout +) + // A Template extends [text/template.Template] with support for directives. type Template struct { name string @@ -27,52 +36,60 @@ type Template struct { // TemplateOptions are template options that can be set with directives. type TemplateOptions struct { Encoding encoding.Encoding - Funcs template.FuncMap + Functions TemplateFunctions FormatIndent string LeftDelimiter string LineEnding string RightDelimiter string + SprigFuncs template.FuncMap + SproutFuncs template.FuncMap Options []string } -// ParseTemplate parses a template named name from data with the given funcs and -// templateOptions. +// ParseTemplate parses a template named name from data with the given options. func ParseTemplate(name string, data []byte, options TemplateOptions) (*Template, error) { contents, err := options.parseAndRemoveDirectives(data) if err != nil { return nil, err } - funcs := options.Funcs - if options.FormatIndent != "" { - funcs = maps.Clone(funcs) - funcs["toJson"] = func(data any) string { - var builder strings.Builder - encoder := json.NewEncoder(&builder) - encoder.SetIndent("", options.FormatIndent) - if err := encoder.Encode(data); err != nil { - panic(err) + var funcs template.FuncMap + switch options.Functions { + case TemplateFunctionsSprig: + funcs = options.SprigFuncs + if options.FormatIndent != "" { + funcs = maps.Clone(funcs) + funcs["toJson"] = func(data any) string { + var builder strings.Builder + encoder := json.NewEncoder(&builder) + encoder.SetIndent("", options.FormatIndent) + if err := encoder.Encode(data); err != nil { + panic(err) + } + return builder.String() } - return builder.String() - } - funcs["toToml"] = func(data any) string { - var builder strings.Builder - encoder := toml.NewEncoder(&builder) - encoder.Indent = options.FormatIndent - if err := encoder.Encode(data); err != nil { - panic(err) + funcs["toToml"] = func(data any) string { + var builder strings.Builder + encoder := toml.NewEncoder(&builder) + encoder.Indent = options.FormatIndent + if err := encoder.Encode(data); err != nil { + panic(err) + } + return builder.String() } - return builder.String() - } - funcs["toYaml"] = func(data any) string { - var builder strings.Builder - encoder := yaml.NewEncoder(&builder, - yaml.Indent(runewidth.StringWidth(options.FormatIndent)), - ) - if err := encoder.Encode(data); err != nil { - panic(err) + funcs["toYaml"] = func(data any) string { + var builder strings.Builder + encoder := yaml.NewEncoder(&builder, + yaml.Indent(runewidth.StringWidth(options.FormatIndent)), + ) + if err := encoder.Encode(data); err != nil { + panic(err) + } + return builder.String() } - return builder.String() } + case TemplateFunctionsSprout: + funcs = options.SproutFuncs + // FIXME handle FormatIndent } tmpl, err := template.New(name). Option(options.Options...). @@ -169,6 +186,15 @@ func (o *TemplateOptions) parseAndRemoveDirectives(data []byte) ([]byte, error) return nil, err } o.FormatIndent = strings.Repeat(" ", width) + case "functions": + switch value { + case "sprig": + o.Functions = TemplateFunctionsSprig + case "sprout": + o.Functions = TemplateFunctionsSprout + default: + return nil, fmt.Errorf("%s: unknown functions", value) + } case "left-delimiter": o.LeftDelimiter = value case "line-ending", "line-endings": diff --git a/internal/cmd/config.go b/internal/cmd/config.go index dc663a36d6d..3b63c5245c8 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -1,5 +1,7 @@ package cmd +// FIXME add getTemplateOptions function similar to getDiffCmd? need to know how to handle template.functions config var + import ( "bufio" "bytes" @@ -110,7 +112,9 @@ type hookConfig struct { } type templateConfig struct { - Options []string `json:"options" mapstructure:"options" yaml:"options"` + FunctionsStr string `json:"functions" mapstructure:"functions" yaml:"functions"` + Options []string `json:"options" mapstructure:"options" yaml:"options"` + functions chezmoi.TemplateFunctions } type warningsConfig struct { @@ -195,21 +199,22 @@ type Config struct { ConfigFile // Global configuration. - ageRecipient string - ageRecipientFile string - configFormat *choiceFlag - debug bool - dryRun bool - force bool - homeDir string - keepGoing bool - noPager bool - noTTY bool - outputAbsPath chezmoi.AbsPath - refreshExternals chezmoi.RefreshExternals - sourcePath bool - templateFuncs template.FuncMap - useBuiltinDiff bool + ageRecipient string + ageRecipientFile string + configFormat *choiceFlag + debug bool + dryRun bool + force bool + homeDir string + keepGoing bool + noPager bool + noTTY bool + outputAbsPath chezmoi.AbsPath + refreshExternals chezmoi.RefreshExternals + sourcePath bool + sprigTemplateFuncs template.FuncMap + sproutTemplateFuncs template.FuncMap + useBuiltinDiff bool // Password manager data. gitHub gitHubData @@ -371,9 +376,9 @@ func newConfig(options ...configOption) (*Config, error) { ConfigFile: newConfigFile(bds), // Global configuration. - configFormat: newChoiceFlag("", readDataFormatValues), - homeDir: userHomeDir, - templateFuncs: sprigin.TxtFuncMap(), + configFormat: newChoiceFlag("", readDataFormatValues), + homeDir: userHomeDir, + sprigTemplateFuncs: sprigin.TxtFuncMap(), // Command configurations. apply: applyCmdConfig{ @@ -473,10 +478,10 @@ func newConfig(options ...configOption) (*Config, error) { "toStrings", "toYaml", } { - if _, ok := c.templateFuncs[templateFunc]; !ok { + if _, ok := c.sprigTemplateFuncs[templateFunc]; !ok { panic(templateFunc + ": deleting non-existent template function") } - delete(c.templateFuncs, templateFunc) + delete(c.sprigTemplateFuncs, templateFunc) } // The completion template function is added in persistentPreRunRootE as @@ -626,10 +631,10 @@ func (c *Config) Close() error { // to c. It panics if there is already an existing template function with the // same key. func (c *Config) addTemplateFunc(key string, value any) { - if _, ok := c.templateFuncs[key]; ok { + if _, ok := c.sprigTemplateFuncs[key]; ok { panic(key + ": already defined") } - c.templateFuncs[key] = value + c.sprigTemplateFuncs[key] = value } type applyArgsOptions struct { @@ -931,9 +936,9 @@ func (c *Config) createConfigFileContents(filename chezmoi.RelPath, data []byte, // This ensures that the init template functions // are removed before "normal" template parsing. funcMap := make(template.FuncMap) - chezmoi.RecursiveMerge(funcMap, c.templateFuncs) + chezmoi.RecursiveMerge(funcMap, c.sprigTemplateFuncs) defer func() { - c.templateFuncs = funcMap + c.sprigTemplateFuncs = funcMap }() initTemplateFuncs := map[string]any{ @@ -951,11 +956,13 @@ func (c *Config) createConfigFileContents(filename chezmoi.RelPath, data []byte, "stdinIsATTY": c.stdinIsATTYInitTemplateFunc, "writeToStdout": c.writeToStdout, } - chezmoi.RecursiveMerge(c.templateFuncs, initTemplateFuncs) + chezmoi.RecursiveMerge(c.sprigTemplateFuncs, initTemplateFuncs) tmpl, err := chezmoi.ParseTemplate(filename.String(), data, chezmoi.TemplateOptions{ - Funcs: c.templateFuncs, - Options: slices.Clone(c.Template.Options), + Functions: c.Template.functions, + SprigFuncs: c.sprigTemplateFuncs, + SproutFuncs: c.sproutTemplateFuncs, + Options: slices.Clone(c.Template.Options), }) if err != nil { return nil, err @@ -1764,8 +1771,8 @@ func (c *Config) gitAutoPush(status *chezmoigit.Status) error { // gitCommitMessage returns the git commit message for the given status. func (c *Config) gitCommitMessage(cmd *cobra.Command, status *chezmoigit.Status) ([]byte, error) { - templateFuncs := maps.Clone(c.templateFuncs) - maps.Copy(templateFuncs, map[string]any{ + sprigTemplateFuncs := maps.Clone(c.sprigTemplateFuncs) + maps.Copy(sprigTemplateFuncs, map[string]any{ "promptBool": c.promptBoolInteractiveTemplateFunc, "promptChoice": c.promptChoiceInteractiveTemplateFunc, "promptInt": c.promptIntInteractiveTemplateFunc, @@ -1775,6 +1782,8 @@ func (c *Config) gitCommitMessage(cmd *cobra.Command, status *chezmoigit.Status) return mustValue(chezmoi.NewSourceRelPath(source).TargetRelPath(c.encryption.EncryptedSuffix())).String() }, }) + sproutTemplateFuncs := maps.Clone(c.sproutTemplateFuncs) + // FIXME add prompt* and targetRelPath functions var name string var commitMessageTemplateData []byte switch { @@ -1797,8 +1806,10 @@ func (c *Config) gitCommitMessage(cmd *cobra.Command, status *chezmoigit.Status) commitMessageTemplateData = []byte(templates.CommitMessageTmpl) } commitMessageTmpl, err := chezmoi.ParseTemplate(name, commitMessageTemplateData, chezmoi.TemplateOptions{ - Funcs: templateFuncs, - Options: slices.Clone(c.Template.Options), + Functions: c.Template.functions, + SprigFuncs: sprigTemplateFuncs, + SproutFuncs: sproutTemplateFuncs, + Options: slices.Clone(c.Template.Options), }) if err != nil { return nil, err @@ -2016,8 +2027,10 @@ func (c *Config) newExternalDiffSystem(s chezmoi.System) *chezmoi.ExternalDiffSy Reverse: c.Diff.Reverse, ScriptContents: c.Diff.ScriptContents, TemplateOptions: chezmoi.TemplateOptions{ - Funcs: c.templateFuncs, - Options: c.Template.Options, + Functions: c.Template.functions, + SprigFuncs: c.sprigTemplateFuncs, + SproutFuncs: c.sproutTemplateFuncs, + Options: c.Template.Options, }, TextConvFunc: c.TextConv.convert, } @@ -2085,8 +2098,8 @@ func (c *Config) newSourceState( chezmoi.WithPriorityTemplateData(priorityTemplateData), chezmoi.WithScriptTempDir(c.ScriptTempDir), chezmoi.WithSourceDir(c.SourceDirAbsPath), + chezmoi.WithSprigTemplateFuncs(c.sprigTemplateFuncs), chezmoi.WithSystem(c.sourceSystem), - chezmoi.WithTemplateFuncs(c.templateFuncs), chezmoi.WithTemplateOptions(c.Template.Options), chezmoi.WithUmask(c.Umask), chezmoi.WithVersion(c.version), @@ -3140,7 +3153,8 @@ func newConfigFile(bds *xdg.BaseDirectorySpecification) ConfigFile { Safe: true, TempDir: chezmoi.NewAbsPath(os.TempDir()), Template: templateConfig{ - Options: chezmoi.DefaultTemplateOptions, + FunctionsStr: "sprig", + Options: chezmoi.DefaultTemplateOptions, }, Umask: chezmoi.Umask, UseBuiltinAge: autoBool{ diff --git a/internal/cmd/config_test.go b/internal/cmd/config_test.go index a67822c781b..3de66dae855 100644 --- a/internal/cmd/config_test.go +++ b/internal/cmd/config_test.go @@ -88,7 +88,8 @@ func TestConfigFileFormatRoundTrip(t *testing.T) { }, ScriptEnv: map[string]string{}, Template: templateConfig{ - Options: []string{}, + FunctionsStr: defaultSentinel, + Options: []string{}, }, TextConv: []*textConvElement{}, UseBuiltinAge: autoBool{value: false}, diff --git a/internal/cmd/executetemplatecmd.go b/internal/cmd/executetemplatecmd.go index 22a47546acb..a0334e0812a 100644 --- a/internal/cmd/executetemplatecmd.go +++ b/internal/cmd/executetemplatecmd.go @@ -257,7 +257,7 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string) error "writeToStdout": c.writeToStdout, } - chezmoi.RecursiveMerge(c.templateFuncs, initTemplateFuncs) + chezmoi.RecursiveMerge(c.sprigTemplateFuncs, initTemplateFuncs) } if len(args) == 0 { diff --git a/internal/cmd/mergecmd.go b/internal/cmd/mergecmd.go index f5dbac1d8ab..3b0ebd53123 100644 --- a/internal/cmd/mergecmd.go +++ b/internal/cmd/mergecmd.go @@ -148,8 +148,10 @@ func (c *Config) doMerge(targetRelPath chezmoi.RelPath, sourceStateEntry chezmoi anyTemplateArgs := false for i, arg := range c.Merge.Args { tmpl, err := chezmoi.ParseTemplate("merge.args["+strconv.Itoa(i)+"]", []byte(arg), chezmoi.TemplateOptions{ - Funcs: c.templateFuncs, - Options: c.Template.Options, + Functions: c.Template.functions, + SprigFuncs: c.sprigTemplateFuncs, + SproutFuncs: c.sproutTemplateFuncs, + Options: c.Template.Options, }) if err != nil { return err diff --git a/internal/cmd/templatefuncs.go b/internal/cmd/templatefuncs.go index f1fe8526b7c..cc21cfaa732 100644 --- a/internal/cmd/templatefuncs.go +++ b/internal/cmd/templatefuncs.go @@ -264,8 +264,10 @@ func (c *Config) includeTemplateTemplateFunc(filename string, args ...any) strin contents := mustValue(c.readFile(filename, searchDirAbsPaths)) tmpl := mustValue(chezmoi.ParseTemplate(filename, contents, chezmoi.TemplateOptions{ - Funcs: c.templateFuncs, - Options: slices.Clone(c.Template.Options), + Functions: c.Template.functions, + SprigFuncs: c.sprigTemplateFuncs, + SproutFuncs: c.sproutTemplateFuncs, + Options: slices.Clone(c.Template.Options), })) return string(mustValue(tmpl.Execute(data)))