Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions internal/chezmoi/sourcestate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
86 changes: 56 additions & 30 deletions internal/chezmoi/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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...).
Expand Down Expand Up @@ -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":
Expand Down
86 changes: 50 additions & 36 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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{
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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{
Expand Down
3 changes: 2 additions & 1 deletion internal/cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/executetemplatecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading