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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
- Fixed the JSON schema rejecting more keys the Taskfile parser accepts:
`ignore_error` on a `task:` call, and `if`, `set` and `shopt` on a command
inside a `for` loop (#2967 by @vmaerten).
- Migrated the templating engine from `slim-sprig` to
[sprout](https://docs.atom.codes/sprout). Every function name Task exposed
still resolves, and the ten functions whose argument order changed accept both
the old and the new form, reporting the old one under `--verbose`. Some
long-standing sprig bugs are fixed as a result, and functions that used to
swallow errors now report them — see
[Migrating from slim-sprig](https://taskfile.dev/docs/reference/templating#migrating-from-slim-sprig)
(#1638, #2006 by @42atomys, by @vmaerten).

## v3.52.0 - 2026-07-02

Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ require (
github.com/elliotchance/orderedmap/v3 v3.1.1
github.com/fatih/color v1.19.0
github.com/fsnotify/fsnotify v1.10.1
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-sprout/sprout v1.0.3
github.com/go-task/template v0.2.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-getter v1.8.6
Expand Down Expand Up @@ -43,6 +43,7 @@ require (
cloud.google.com/go/iam v1.13.0 // indirect
cloud.google.com/go/monitoring v1.30.0 // indirect
cloud.google.com/go/storage v1.64.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0 // indirect
Expand Down Expand Up @@ -97,13 +98,16 @@ require (
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.27 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/u-root/u-root v0.16.0 // indirect
Expand Down
14 changes: 12 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ cloud.google.com/go/storage v1.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSP
cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ=
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 h1:bN1gA3of5bXtbnLsRPrwfmbbe7A5UWFlcTHseujLnpc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0/go.mod h1:Yj5vHEz/aAepZGliRJsA6uvHAVAQyEwajq9ORCHPxzM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0 h1:c/Ivw7FuawPLfrr+zB0LZKeCchO2cAHQpF2qZ6OV7rQ=
Expand Down Expand Up @@ -136,6 +138,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
Expand All @@ -147,8 +151,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-sprout/sprout v1.0.3 h1:LLuz0D3aYazgbVTOwCVuMor3LOUVYinipXRIdjA/D+I=
github.com/go-sprout/sprout v1.0.3/go.mod h1:cFFzpnyGGry3cmN0UNCAM1f7AGok6vPVabeYQzBMBZY=
github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
Expand Down Expand Up @@ -198,10 +202,14 @@ github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsRe
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
Expand All @@ -225,6 +233,8 @@ github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvK
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E=
Expand Down
216 changes: 82 additions & 134 deletions internal/templater/funcs.go
Original file line number Diff line number Diff line change
@@ -1,150 +1,98 @@
package templater

import (
"log/slog"
"maps"
"math/rand/v2"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/davecgh/go-spew/spew"
"github.com/google/uuid"
"go.yaml.in/yaml/v3"
"mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax"
"github.com/go-sprout/sprout"
"github.com/go-sprout/sprout/registry/backward"
"github.com/go-sprout/sprout/registry/checksum"
"github.com/go-sprout/sprout/registry/conversion"
"github.com/go-sprout/sprout/registry/encoding"
"github.com/go-sprout/sprout/registry/env"
"github.com/go-sprout/sprout/registry/filesystem"
sproutmaps "github.com/go-sprout/sprout/registry/maps"
"github.com/go-sprout/sprout/registry/numeric"
"github.com/go-sprout/sprout/registry/random"
"github.com/go-sprout/sprout/registry/reflect"
"github.com/go-sprout/sprout/registry/regexp"
"github.com/go-sprout/sprout/registry/slices"
"github.com/go-sprout/sprout/registry/std"
sproutstrings "github.com/go-sprout/sprout/registry/strings"
sprouttime "github.com/go-sprout/sprout/registry/time"
"github.com/go-sprout/sprout/registry/uniqueid"

sprig "github.com/go-task/slim-sprig/v3"
"github.com/go-task/template"

"github.com/go-task/task/v3/internal/templater/taskfuncs"
)

var templateFuncs template.FuncMap

func init() {
taskFuncs := template.FuncMap{
"OS": goos,
"ARCH": goarch,
"numCPU": runtime.NumCPU,
"catLines": catLines,
"splitLines": splitLines,
"fromSlash": filepath.FromSlash,
"toSlash": filepath.ToSlash,
"exeExt": exeExt,
"shellQuote": shellQuote,
"splitArgs": splitArgs,
"IsSH": IsSH, // Deprecated
"joinPath": filepath.Join,
"joinEnv": joinEnv,
"joinUrl": joinUrl,
"relPath": filepath.Rel,
"absPath": filepath.Abs,
"merge": merge,
"spew": spew.Sdump,
"fromYaml": fromYaml,
"mustFromYaml": mustFromYaml,
"toYaml": toYaml,
"mustToYaml": mustToYaml,
"uuid": uuid.New,
"randIntN": rand.IntN,
}

// aliases
taskFuncs["q"] = taskFuncs["shellQuote"]

// Deprecated aliases for renamed functions.
taskFuncs["FromSlash"] = taskFuncs["fromSlash"]
taskFuncs["ToSlash"] = taskFuncs["toSlash"]
taskFuncs["ExeExt"] = taskFuncs["exeExt"]

templateFuncs = template.FuncMap(sprig.TxtFuncMap())
maps.Copy(templateFuncs, taskFuncs)
}

func goos() string {
return runtime.GOOS
}

func goarch() string {
return runtime.GOARCH
}

func catLines(s string) string {
s = strings.ReplaceAll(s, "\r\n", " ")
return strings.ReplaceAll(s, "\n", " ")
}

func splitLines(s string) []string {
s = strings.ReplaceAll(s, "\r\n", "\n")
return strings.Split(s, "\n")
}

func exeExt() string {
if runtime.GOOS == "windows" {
return ".exe"
}
return ""
}

func shellQuote(str string) (string, error) {
return syntax.Quote(str, syntax.LangBash)
// legacySprigAliases maps the function names Task exposed through slim-sprig
// onto their sprout equivalents. Only names slim-sprig actually shipped are
// listed — sprout carries a wider legacy set, but Task never exposed those and
// should not start now.
var legacySprigAliases = sprout.FunctionAliasMap{
"dateModify": {"date_modify", "must_date_modify"},
"dateInZone": {"date_in_zone"},
"dateAgo": {"ago"},
"trimAll": {"trimall"},
"append": {"push", "mustPush"},
"list": {"tuple"},
"max": {"biggest"},
"toUpper": {"upper"},
"toLower": {"lower"},
"toTitleCase": {"title"},
"base64Encode": {"b64enc"},
"base64Decode": {"b64dec"},
"base32Encode": {"b32enc"},
"base32Decode": {"b32dec"},
"pathBase": {"base"},
"pathDir": {"dir"},
"pathExt": {"ext"},
"pathClean": {"clean"},
"pathIsAbs": {"isAbs"},
"expandEnv": {"expandenv"},
"strSlice": {"toStrings"},
"toInt": {"int", "atoi"},
"toInt64": {"int64"},
"toFloat64": {"float64"},
"toOctal": {"toDecimal"},
}

func splitArgs(s string) ([]string, error) {
return shell.Fields(s, nil)
}

// Deprecated: now always returns true
func IsSH() bool {
return true
}

func joinEnv(elem ...string) string {
return strings.Join(elem, string(os.PathListSeparator))
}

func joinUrl(elem ...string) (string, error) {
if len(elem) == 0 {
return "", nil
}
// Use net/url.JoinPath rather than path.Join: the latter runs path.Clean,
// which collapses the "//" in a URL scheme (e.g. "http://" -> "http:/").
return url.JoinPath(elem[0], elem[1:]...)
}

func merge(base map[string]any, v ...map[string]any) map[string]any {
cap := len(v)
for _, m := range v {
cap += len(m)
}
result := make(map[string]any, cap)
maps.Copy(result, base)
for _, m := range v {
maps.Copy(result, m)
func init() {
handler := sprout.New(
sprout.WithLogger(slog.New(noticeHandler{})),
sprout.WithRegistries(
taskfuncs.NewRegistry(),
backward.NewRegistry(),
checksum.NewRegistry(),
conversion.NewRegistry(),
encoding.NewRegistry(),
env.NewRegistry(),
filesystem.NewRegistry(),
sproutmaps.NewRegistry(),
numeric.NewRegistry(),
random.NewRegistry(),
reflect.NewRegistry(),
regexp.NewRegistry(),
slices.NewRegistry(),
std.NewRegistry(),
sproutstrings.NewRegistry(),
sprouttime.NewRegistry(),
uniqueid.NewRegistry(),
),
)

for original, aliases := range legacySprigAliases {
_ = sprout.WithAlias(original, aliases...)(handler)
for _, alias := range aliases {
_ = sprout.WithNotices(sprout.NewDeprecatedNotice(alias, "please use `"+original+"` instead"))(handler)
}
}
return result
}

func fromYaml(v string) any {
output, _ := mustFromYaml(v)
return output
}

func mustFromYaml(v string) (any, error) {
var output any
err := yaml.Unmarshal([]byte(v), &output)
return output, err
}

func toYaml(v any) string {
output, _ := yaml.Marshal(v)
return string(output)
}

func mustToYaml(v any) (string, error) {
output, err := yaml.Marshal(v)
if err != nil {
return "", err
}
return string(output), nil
templateFuncs = template.FuncMap(handler.Build())
maps.Copy(templateFuncs, taskfuncs.Overrides())
maps.Copy(templateFuncs, sprigSignatureShims(handler))
}
57 changes: 57 additions & 0 deletions internal/templater/notices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package templater

import (
"context"
"log/slog"
"sync"
"sync/atomic"
)

// NoticeFunc receives the deprecation notices that sprout emits when a
// template calls a deprecated function name or uses a deprecated argument
// order.
type NoticeFunc func(format string, args ...any)

// noticeSink is set once the Executor has built its logger. Until then — and
// the function map is built in an init(), long before that — notices are
// dropped rather than written to sprout's default stdout handler, which would
// corrupt the JSON and group output styles.
var noticeSink atomic.Pointer[NoticeFunc]

// SetNoticeSink installs the destination for template deprecation notices.
// Passing nil silences them again.
func SetNoticeSink(fn NoticeFunc) {
noticeSeen.Clear()
if fn == nil {
noticeSink.Store(nil)
return
}
noticeSink.Store(&fn)
}

type noticeHandler struct{}

func (noticeHandler) Enabled(_ context.Context, level slog.Level) bool {
return level >= slog.LevelWarn && noticeSink.Load() != nil
}

// noticeSeen keeps each distinct notice to a single line. Task renders the
// same templates several times per run — once per compilation pass — so
// without this a lone deprecated call would be reported over and over.
var noticeSeen sync.Map

func (noticeHandler) Handle(_ context.Context, record slog.Record) error {
fn := noticeSink.Load()
if fn == nil {
return nil
}
if _, dup := noticeSeen.LoadOrStore(record.Message, struct{}{}); dup {
return nil
}
(*fn)("task: %s\n", record.Message)
return nil
}

func (h noticeHandler) WithAttrs([]slog.Attr) slog.Handler { return h }

func (h noticeHandler) WithGroup(string) slog.Handler { return h }
Loading
Loading