mirror of
https://github.com/go-task/task.git
synced 2026-09-02 04:02:08 +02:00
feat(templater): migrate the template engine from slim-sprig to sprout
slim-sprig is a fork of the unmaintained Masterminds/sprig that Task keeps alive itself. sprout is its maintained successor, and its registry system gives the same trimmed function set without a fork to maintain. Task's own functions move into a sprout registry. Ten functions changed argument order between the two libraries, and dig lost its default-value argument; wrappers accept both forms so existing Taskfiles keep working. merge and fromYaml/toYaml are re-applied after the handler is built, because sprout's namesakes have different semantics. Closes #1638.
This commit is contained in:
6
go.mod
6
go.mod
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
14
go.sum
14
go.sum
@@ -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=
|
||||
@@ -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=
|
||||
@@ -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=
|
||||
@@ -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=
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
// 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 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,
|
||||
handler := sprout.New(
|
||||
sprout.WithLogger(slog.New(slog.DiscardHandler)),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -110,6 +110,10 @@ var funcBehaviourGroups = []struct {
|
||||
`{{ dict "a" "b" | hasKey "a" }}`,
|
||||
`{{ list 1 2 | append 3 | toJson }}`,
|
||||
`{{ list 2 3 | prepend 1 | toJson }}`,
|
||||
`{{ dict "a" "1" "b" "2" | pick "a" | toJson }}`,
|
||||
`{{ dict "a" "1" "b" "2" | omit "a" | toJson }}`,
|
||||
`{{ list 1 2 3 | without 2 | toJson }}`,
|
||||
`{{ list 1 2 3 4 | slice 1 3 | toJson }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
191
internal/templater/sprigcompat.go
Normal file
191
internal/templater/sprigcompat.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package templater
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-sprout/sprout"
|
||||
sproutmaps "github.com/go-sprout/sprout/registry/maps"
|
||||
"github.com/go-sprout/sprout/registry/slices"
|
||||
)
|
||||
|
||||
// sprig passed the map or list to operate on as the *first* argument; sprout
|
||||
// passes it *last* so that the function can be piped into. Taskfiles written
|
||||
// against slim-sprig use the old order, and feeding them to sprout produces an
|
||||
// opaque type error rather than a useful message.
|
||||
//
|
||||
// The wrappers below accept both orders, detecting the old one by the type of
|
||||
// the first argument — the two positions never hold the same kind, so the test
|
||||
// is unambiguous in every realistic case. When the old order is detected the
|
||||
// call still succeeds, and a deprecation warning is logged.
|
||||
//
|
||||
// Only the ten functions whose argument order actually changed are wrapped.
|
||||
// `dig`, `has`, `chunk` and `merge` kept theirs.
|
||||
|
||||
func sprigSignatureShims(handler sprout.Handler) sprout.FunctionMap {
|
||||
m := sproutmaps.NewRegistry()
|
||||
_ = m.LinkHandler(handler)
|
||||
s := slices.NewRegistry()
|
||||
_ = s.LinkHandler(handler)
|
||||
|
||||
warn := func(name, oldSig, newSig string) {
|
||||
handler.Logger().
|
||||
With("function", name, "notice", "deprecated").
|
||||
Warn(fmt.Sprintf("Template function `%s` was called with the deprecated slim-sprig argument order `%s`; please use `%s` instead.", name, oldSig, newSig))
|
||||
}
|
||||
|
||||
return sprout.FunctionMap{
|
||||
"get": func(args ...any) (any, error) {
|
||||
key, dict, err := mapArgs("get", `{{ get $dict "key" }}`, `{{ $dict | get "key" }}`, warn, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Get(key, dict)
|
||||
},
|
||||
"hasKey": func(args ...any) (any, error) {
|
||||
key, dict, err := mapArgs("hasKey", `{{ hasKey $dict "key" }}`, `{{ $dict | hasKey "key" }}`, warn, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.HasKey(key, dict)
|
||||
},
|
||||
"unset": func(args ...any) (any, error) {
|
||||
key, dict, err := mapArgs("unset", `{{ unset $dict "key" }}`, `{{ $dict | unset "key" }}`, warn, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Unset(key, dict)
|
||||
},
|
||||
"set": func(args ...any) (any, error) {
|
||||
if len(args) != 3 {
|
||||
return nil, fmt.Errorf("set requires exactly 3 arguments, got %d", len(args))
|
||||
}
|
||||
if isMap(args[0]) {
|
||||
warn("set", `{{ set $dict "key" "value" }}`, `{{ $dict | set "key" "value" }}`)
|
||||
args = []any{args[1], args[2], args[0]}
|
||||
}
|
||||
key, ok := args[0].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("set: key must be a string, got %T", args[0])
|
||||
}
|
||||
dict, ok := args[2].(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("set: last argument must be a map, got %T", args[2])
|
||||
}
|
||||
return m.Set(key, args[1], dict)
|
||||
},
|
||||
// sprout's `dig` dropped sprig's default-value argument entirely:
|
||||
// it is `dig(keys..., dict)` where sprig had `dig(keys..., default,
|
||||
// dict)`. Both take strings in that position, so the two forms cannot
|
||||
// be told apart by type — Task keeps sprig's meaning, since that is
|
||||
// what every existing Taskfile was written against.
|
||||
"dig": func(args ...any) (any, error) {
|
||||
if len(args) < 3 {
|
||||
return m.Dig(args...)
|
||||
}
|
||||
dict, ok := args[len(args)-1].(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("dig: last argument must be a map, got %T", args[len(args)-1])
|
||||
}
|
||||
fallback := args[len(args)-2]
|
||||
lookup := make([]any, 0, len(args)-1)
|
||||
lookup = append(lookup, args[:len(args)-2]...)
|
||||
out, err := m.Dig(append(lookup, dict)...)
|
||||
if err != nil || out == nil {
|
||||
return fallback, nil
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
"pick": func(args ...any) (any, error) {
|
||||
return m.Pick(rotateFirstToLast("pick", `{{ pick $dict "key" }}`, `{{ $dict | pick "key" }}`, warn, isMap, args)...)
|
||||
},
|
||||
"omit": func(args ...any) (any, error) {
|
||||
return m.Omit(rotateFirstToLast("omit", `{{ omit $dict "key" }}`, `{{ $dict | omit "key" }}`, warn, isMap, args)...)
|
||||
},
|
||||
"append": func(args ...any) (any, error) {
|
||||
v, list, err := listArgs("append", `{{ append $list "value" }}`, `{{ $list | append "value" }}`, warn, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Append(v, list)
|
||||
},
|
||||
"prepend": func(args ...any) (any, error) {
|
||||
v, list, err := listArgs("prepend", `{{ prepend $list "value" }}`, `{{ $list | prepend "value" }}`, warn, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Prepend(v, list)
|
||||
},
|
||||
"without": func(args ...any) (any, error) {
|
||||
return s.Without(rotateFirstToLast("without", `{{ without $list "value" }}`, `{{ $list | without "value" }}`, warn, isList, args)...)
|
||||
},
|
||||
"slice": func(args ...any) (any, error) {
|
||||
return s.Slice(rotateFirstToLast("slice", `{{ slice $list 1 3 }}`, `{{ $list | slice 1 3 }}`, warn, isList, args)...)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type warnFunc func(name, oldSig, newSig string)
|
||||
|
||||
// mapArgs resolves the (key, dict) pair of a two-argument map function written
|
||||
// in either order.
|
||||
func mapArgs(name, oldSig, newSig string, warn warnFunc, args []any) (string, map[string]any, error) {
|
||||
if len(args) != 2 {
|
||||
return "", nil, fmt.Errorf("%s requires exactly 2 arguments, got %d", name, len(args))
|
||||
}
|
||||
if isMap(args[0]) {
|
||||
warn(name, oldSig, newSig)
|
||||
args = []any{args[1], args[0]}
|
||||
}
|
||||
key, ok := args[0].(string)
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf("%s: key must be a string, got %T", name, args[0])
|
||||
}
|
||||
dict, ok := args[1].(map[string]any)
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf("%s: last argument must be a map, got %T", name, args[1])
|
||||
}
|
||||
return key, dict, nil
|
||||
}
|
||||
|
||||
// listArgs resolves the (value, list) pair of a two-argument slice function
|
||||
// written in either order.
|
||||
func listArgs(name, oldSig, newSig string, warn warnFunc, args []any) (any, any, error) {
|
||||
if len(args) != 2 {
|
||||
return nil, nil, fmt.Errorf("%s requires exactly 2 arguments, got %d", name, len(args))
|
||||
}
|
||||
if isList(args[0]) && !isList(args[1]) {
|
||||
warn(name, oldSig, newSig)
|
||||
return args[1], args[0], nil
|
||||
}
|
||||
return args[0], args[1], nil
|
||||
}
|
||||
|
||||
// rotateFirstToLast moves a leading target argument to the end, which is where
|
||||
// sprout's variadic functions expect it.
|
||||
func rotateFirstToLast(name, oldSig, newSig string, warn warnFunc, isTarget func(any) bool, args []any) []any {
|
||||
if len(args) < 2 || !isTarget(args[0]) || isTarget(args[len(args)-1]) {
|
||||
return args
|
||||
}
|
||||
warn(name, oldSig, newSig)
|
||||
rotated := make([]any, 0, len(args))
|
||||
rotated = append(rotated, args[1:]...)
|
||||
return append(rotated, args[0])
|
||||
}
|
||||
|
||||
func isMap(v any) bool {
|
||||
_, ok := v.(map[string]any)
|
||||
return ok
|
||||
}
|
||||
|
||||
func isList(v any) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
switch reflect.TypeOf(v).Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
125
internal/templater/sprigcompat_test.go
Normal file
125
internal/templater/sprigcompat_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package templater
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
// render evaluates a single template expression and returns its output, or the
|
||||
// error the template engine raised.
|
||||
func render(t *testing.T, expr string) (string, error) {
|
||||
t.Helper()
|
||||
cache := &Cache{Vars: ast.NewVars()}
|
||||
got := ReplaceWithExtra(expr, cache, nil)
|
||||
if err := cache.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return got, nil
|
||||
}
|
||||
|
||||
// The ten functions whose argument order changed between slim-sprig and sprout
|
||||
// must accept both, so that existing Taskfiles keep working while new ones can
|
||||
// use the pipe form.
|
||||
func TestSprigSignatureShims(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
sprig string
|
||||
sprout string
|
||||
want string
|
||||
}{
|
||||
{"get", `{{ get (dict "a" "b") "a" }}`, `{{ dict "a" "b" | get "a" }}`, "b"},
|
||||
{"hasKey", `{{ hasKey (dict "a" "b") "a" }}`, `{{ dict "a" "b" | hasKey "a" }}`, "true"},
|
||||
{"unset", `{{ unset (dict "a" "b" "c" "d") "a" | toJson }}`, `{{ dict "a" "b" "c" "d" | unset "a" | toJson }}`, `{"c":"d"}`},
|
||||
{"set", `{{ set (dict "a" "b") "c" "d" | toJson }}`, `{{ dict "a" "b" | set "c" "d" | toJson }}`, `{"a":"b","c":"d"}`},
|
||||
{"pick", `{{ pick (dict "a" "1" "b" "2") "a" | toJson }}`, `{{ dict "a" "1" "b" "2" | pick "a" | toJson }}`, `{"a":"1"}`},
|
||||
{"omit", `{{ omit (dict "a" "1" "b" "2") "a" | toJson }}`, `{{ dict "a" "1" "b" "2" | omit "a" | toJson }}`, `{"b":"2"}`},
|
||||
{"append", `{{ append (list 1 2) 3 | toJson }}`, `{{ list 1 2 | append 3 | toJson }}`, "[1,2,3]"},
|
||||
{"prepend", `{{ prepend (list 2 3) 1 | toJson }}`, `{{ list 2 3 | prepend 1 | toJson }}`, "[1,2,3]"},
|
||||
{"without", `{{ without (list 1 2 3) 2 | toJson }}`, `{{ list 1 2 3 | without 2 | toJson }}`, "[1,3]"},
|
||||
{"slice", `{{ slice (list 1 2 3 4) 1 3 | toJson }}`, `{{ list 1 2 3 4 | slice 1 3 | toJson }}`, "[2,3]"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for order, expr := range map[string]string{"slim-sprig": tt.sprig, "sprout": tt.sprout} {
|
||||
got, err := render(t, expr)
|
||||
if err != nil {
|
||||
t.Fatalf("%s order: %s unexpected error: %v", order, expr, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("%s order: %s = %q; want %q", order, expr, got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// sprout dropped sprig's default-value argument on dig. Task keeps it, since
|
||||
// the two forms are indistinguishable by type and every existing Taskfile was
|
||||
// written against sprig's.
|
||||
func TestDigKeepsSprigDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
expr string
|
||||
want string
|
||||
}{
|
||||
{"path found", `{{ dig "a" "b" "fallback" (dict "a" (dict "b" "found")) }}`, "found"},
|
||||
{"key missing", `{{ dig "a" "missing" "fallback" (dict "a" (dict "b" "found")) }}`, "fallback"},
|
||||
{"root missing", `{{ dig "nope" "fallback" (dict "a" 1) }}`, "fallback"},
|
||||
{"leaf is not a dict", `{{ dig "a" "b" "c" "fallback" (dict "a" (dict "b" "found")) }}`, "fallback"},
|
||||
// sprout splits keys on dots, which sprig did not. Kept: it is an
|
||||
// improvement and no slim-sprig key could contain a dot anyway.
|
||||
{"dotted path", `{{ dig "a.b" "fallback" (dict "a" (dict "b" "found")) }}`, "found"},
|
||||
// Two arguments is unambiguously sprout's no-default form.
|
||||
{"no default", `{{ dig "a" (dict "a" "found") }}`, "found"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := render(t, tt.expr)
|
||||
if err != nil {
|
||||
t.Fatalf("%s unexpected error: %v", tt.expr, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("%s = %q; want %q", tt.expr, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Task's own functions must keep winning over the sprout functions and aliases
|
||||
// that share their name, since their semantics differ.
|
||||
func TestTaskFuncsShadowSprout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
expr string
|
||||
want string
|
||||
}{
|
||||
// sprout's merge is a deep merge that keeps the destination value;
|
||||
// Task's is a shallow merge where the last map wins.
|
||||
{"merge overwrites", `{{ merge (dict "a" 1) (dict "a" 0) | toJson }}`, `{"a":0}`},
|
||||
// sprout aliases fromYaml/toYaml onto fromYAML/toYAML, which raise
|
||||
// errors where Task's swallow them.
|
||||
{"fromYaml swallows errors", `{{ fromYaml "a: :" | toJson }}`, "null"},
|
||||
{"toYaml", `{{ toYaml (dict "a" 1) }}`, "a: 1\n"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := render(t, tt.expr)
|
||||
if err != nil {
|
||||
t.Fatalf("%s unexpected error: %v", tt.expr, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("%s = %q; want %q", tt.expr, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
105
internal/templater/taskfuncs/funcs.go
Normal file
105
internal/templater/taskfuncs/funcs.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Package taskfuncs provides the template functions that Task adds on top of
|
||||
// the generic ones supplied by sprout.
|
||||
package taskfuncs
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"mvdan.cc/sh/v3/shell"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
)
|
||||
|
||||
func OS() string {
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
func Arch() 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)
|
||||
}
|
||||
|
||||
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:]...)
|
||||
}
|
||||
|
||||
// Merge shallow-merges maps, later keys winning. It shadows sprout's `merge`,
|
||||
// which deep-merges and keeps the destination value on conflict.
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package templater
|
||||
package taskfuncs
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -19,12 +19,12 @@ func TestJoinUrl(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := joinUrl(tt.elem...)
|
||||
got, err := JoinURL(tt.elem...)
|
||||
if err != nil {
|
||||
t.Fatalf("joinUrl(%q) unexpected error: %v", tt.elem, err)
|
||||
t.Fatalf("JoinURL(%q) unexpected error: %v", tt.elem, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("joinUrl(%q) = %q; want %q", tt.elem, got, tt.want)
|
||||
t.Errorf("JoinURL(%q) = %q; want %q", tt.elem, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
98
internal/templater/taskfuncs/registry.go
Normal file
98
internal/templater/taskfuncs/registry.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package taskfuncs
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/go-sprout/sprout"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Registry exposes Task's own template functions to a sprout handler.
|
||||
type Registry struct {
|
||||
handler sprout.Handler
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{}
|
||||
}
|
||||
|
||||
func (r *Registry) UID() string {
|
||||
return "go-task/task.taskfuncs"
|
||||
}
|
||||
|
||||
func (r *Registry) LinkHandler(fh sprout.Handler) error {
|
||||
r.handler = fh
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) RegisterFunctions(fnMap sprout.FunctionMap) error {
|
||||
for name, fn := range functions() {
|
||||
sprout.AddFunction(fnMap, name, fn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) RegisterAliases(aliasMap sprout.FunctionAliasMap) error {
|
||||
sprout.AddAlias(aliasMap, "shellQuote", "q")
|
||||
// Deprecated aliases for renamed functions.
|
||||
sprout.AddAlias(aliasMap, "fromSlash", "FromSlash")
|
||||
sprout.AddAlias(aliasMap, "toSlash", "ToSlash")
|
||||
sprout.AddAlias(aliasMap, "exeExt", "ExeExt")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) RegisterNotices(notices *[]sprout.FunctionNotice) error {
|
||||
sprout.AddNotice(notices, sprout.NewDeprecatedNotice("IsSH", "it always returns true and can be removed from your templates"))
|
||||
sprout.AddNotice(notices, sprout.NewDeprecatedNotice("FromSlash", "please use `fromSlash` instead"))
|
||||
sprout.AddNotice(notices, sprout.NewDeprecatedNotice("ToSlash", "please use `toSlash` instead"))
|
||||
sprout.AddNotice(notices, sprout.NewDeprecatedNotice("ExeExt", "please use `exeExt` instead"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Overrides returns the functions that must be re-applied after the handler is
|
||||
// built. sprout owns `merge`, and its encoding registry aliases `fromYAML`
|
||||
// and `toYAML` onto the camelCase names Task already uses — and AssignAliases
|
||||
// overwrites unconditionally, so registration order alone cannot protect them.
|
||||
// Their semantics differ from Task's (deep merge, and errors raised instead of
|
||||
// swallowed), hence Task's implementations win.
|
||||
func Overrides() sprout.FunctionMap {
|
||||
return sprout.FunctionMap{
|
||||
"merge": Merge,
|
||||
"fromYaml": FromYAML,
|
||||
"mustFromYaml": MustFromYAML,
|
||||
"toYaml": ToYAML,
|
||||
"mustToYaml": MustToYAML,
|
||||
}
|
||||
}
|
||||
|
||||
func functions() sprout.FunctionMap {
|
||||
return sprout.FunctionMap{
|
||||
"OS": OS,
|
||||
"ARCH": Arch,
|
||||
"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,
|
||||
}
|
||||
}
|
||||
@@ -26,17 +26,25 @@
|
||||
## argument order — sprout order (target last)
|
||||
|
||||
{{ dict "a" "b" | get "a" }}
|
||||
! at <"a">: can't handle "a" for arg of type map[string]interface {}
|
||||
= b
|
||||
{{ dict "a" "b" | set "c" "d" | toJson }}
|
||||
! at <"c">: can't handle "c" for arg of type map[string]interface {}
|
||||
= {"a":"b","c":"d"}
|
||||
{{ dict "a" "b" "c" "d" | unset "a" | toJson }}
|
||||
! at <"a">: can't handle "a" for arg of type map[string]interface {}
|
||||
= {"c":"d"}
|
||||
{{ dict "a" "b" | hasKey "a" }}
|
||||
! at <"a">: can't handle "a" for arg of type map[string]interface {}
|
||||
= true
|
||||
{{ list 1 2 | append 3 | toJson }}
|
||||
! at <append 3>: error calling append: Cannot push on type int
|
||||
= [1,2,3]
|
||||
{{ list 2 3 | prepend 1 | toJson }}
|
||||
! at <prepend 1>: error calling prepend: Cannot prepend on type int
|
||||
= [1,2,3]
|
||||
{{ dict "a" "1" "b" "2" | pick "a" | toJson }}
|
||||
= {"a":"1"}
|
||||
{{ dict "a" "1" "b" "2" | omit "a" | toJson }}
|
||||
= {"b":"2"}
|
||||
{{ list 1 2 3 | without 2 | toJson }}
|
||||
= [1,3]
|
||||
{{ list 1 2 3 4 | slice 1 3 | toJson }}
|
||||
= [2,3]
|
||||
|
||||
## maps — unchanged signatures
|
||||
|
||||
@@ -53,7 +61,7 @@
|
||||
{{ dig "a" "missing" "fallback" (dict "a" (dict "b" "found")) }}
|
||||
= fallback
|
||||
{{ dig "a.b" "fallback" (dict "a" (dict "b" "found")) }}
|
||||
= fallback
|
||||
= found
|
||||
{{ merge (dict "a" 1) (dict "b" 2) | toJson }}
|
||||
= {"a":1,"b":2}
|
||||
{{ merge (dict "a" 1) (dict "a" 0) | toJson }}
|
||||
@@ -119,7 +127,7 @@
|
||||
{{ title "hello world" }}
|
||||
= Hello World
|
||||
{{ title "hello wORLD" }}
|
||||
= Hello WORLD
|
||||
= Hello World
|
||||
{{ trunc 3 "foobar" }}
|
||||
= foo
|
||||
{{ trunc -3 "foobar" }}
|
||||
@@ -127,7 +135,7 @@
|
||||
{{ substr 0 3 "foobar" }}
|
||||
= foo
|
||||
{{ substr 0 -3 "foobar" }}
|
||||
= foobar
|
||||
= foo
|
||||
{{ repeat 3 "x" }}
|
||||
= xxx
|
||||
{{ contains "oo" "foobar" }}
|
||||
@@ -190,7 +198,7 @@
|
||||
{{ atoi "42" }}
|
||||
= 42
|
||||
{{ atoi "abc" }}
|
||||
= 0
|
||||
! at <atoi "abc">: error calling atoi: unable to cast "abc" of type string to int: strconv.ParseInt: parsing "abc": invalid syntax
|
||||
{{ int "42" }}
|
||||
= 42
|
||||
{{ int64 "42" }}
|
||||
@@ -232,9 +240,9 @@
|
||||
{{ fromJson "{\"a\":1}" | toJson }}
|
||||
= {"a":1}
|
||||
{{ fromJson "not json" | toJson }}
|
||||
= null
|
||||
! at <fromJson "not json">: error calling fromJson: json decode error: invalid character 'o' in literal null (expecting 'u')
|
||||
{{ mustFromJson "not json" | toJson }}
|
||||
! at <mustFromJson "not json">: error calling mustFromJson: invalid character 'o' in literal null (expecting 'u')
|
||||
! at <mustFromJson "not json">: error calling mustFromJson: json decode error: invalid character 'o' in literal null (expecting 'u')
|
||||
{{ toYaml (dict "a" 1) }}
|
||||
= a: 1\n
|
||||
{{ fromYaml "a: 1" | toJson }}
|
||||
@@ -323,7 +331,7 @@
|
||||
{{ unixEpoch (toDate "2006-01-02" "2020-01-01") }}
|
||||
= 1577836800
|
||||
{{ duration 90 }}
|
||||
= 0s
|
||||
= 1m30s
|
||||
{{ durationRound "1h35m30s" }}
|
||||
= 1h
|
||||
{{ htmlDate 0 }}
|
||||
|
||||
78
internal/templater/testdata/func_names.golden
vendored
78
internal/templater/testdata/func_names.golden
vendored
@@ -7,6 +7,9 @@ ToSlash
|
||||
absPath
|
||||
add
|
||||
add1
|
||||
add1f
|
||||
addf
|
||||
adler32Sum
|
||||
adler32sum
|
||||
ago
|
||||
all
|
||||
@@ -18,7 +21,12 @@ b32enc
|
||||
b64dec
|
||||
b64enc
|
||||
base
|
||||
base32Decode
|
||||
base32Encode
|
||||
base64Decode
|
||||
base64Encode
|
||||
biggest
|
||||
capitalize
|
||||
cat
|
||||
catLines
|
||||
ceil
|
||||
@@ -29,33 +37,44 @@ compact
|
||||
concat
|
||||
contains
|
||||
date
|
||||
dateAgo
|
||||
dateInZone
|
||||
dateModify
|
||||
date_in_zone
|
||||
date_modify
|
||||
deepCopy
|
||||
deepEqual
|
||||
default
|
||||
dict
|
||||
dig
|
||||
dir
|
||||
div
|
||||
divf
|
||||
duration
|
||||
durationRound
|
||||
ellipsis
|
||||
ellipsisBoth
|
||||
empty
|
||||
env
|
||||
exeExt
|
||||
expandEnv
|
||||
expandenv
|
||||
ext
|
||||
fail
|
||||
first
|
||||
flatten
|
||||
flattenDepth
|
||||
float64
|
||||
floor
|
||||
fromJSON
|
||||
fromJson
|
||||
fromSlash
|
||||
fromYAML
|
||||
fromYaml
|
||||
get
|
||||
getHostByName
|
||||
has
|
||||
hasField
|
||||
hasKey
|
||||
hasPrefix
|
||||
hasSuffix
|
||||
@@ -64,6 +83,7 @@ htmlDate
|
||||
htmlDateInZone
|
||||
indent
|
||||
initial
|
||||
initials
|
||||
int
|
||||
int64
|
||||
isAbs
|
||||
@@ -79,21 +99,28 @@ list
|
||||
lower
|
||||
max
|
||||
maxf
|
||||
md5Sum
|
||||
md5sum
|
||||
merge
|
||||
mergeOverwrite
|
||||
min
|
||||
minf
|
||||
mod
|
||||
mul
|
||||
mulf
|
||||
mustAppend
|
||||
mustChunk
|
||||
mustCompact
|
||||
mustDateModify
|
||||
mustDeepCopy
|
||||
mustFirst
|
||||
mustFromJson
|
||||
mustFromYaml
|
||||
mustHas
|
||||
mustInitial
|
||||
mustLast
|
||||
mustMerge
|
||||
mustMergeOverwrite
|
||||
mustPrepend
|
||||
mustPush
|
||||
mustRegexFind
|
||||
@@ -114,6 +141,7 @@ mustUniq
|
||||
mustWithout
|
||||
must_date_modify
|
||||
nindent
|
||||
nospace
|
||||
now
|
||||
numCPU
|
||||
omit
|
||||
@@ -122,6 +150,11 @@ osClean
|
||||
osDir
|
||||
osExt
|
||||
osIsAbs
|
||||
pathBase
|
||||
pathClean
|
||||
pathDir
|
||||
pathExt
|
||||
pathIsAbs
|
||||
pick
|
||||
pluck
|
||||
plural
|
||||
@@ -129,10 +162,19 @@ prepend
|
||||
push
|
||||
q
|
||||
quote
|
||||
randAlpha
|
||||
randAlphaNum
|
||||
randAscii
|
||||
randBytes
|
||||
randInt
|
||||
randIntN
|
||||
randNumeric
|
||||
regexFind
|
||||
regexFindAll
|
||||
regexFindAllGroups
|
||||
regexFindAllNamed
|
||||
regexFindGroups
|
||||
regexFindNamed
|
||||
regexMatch
|
||||
regexQuoteMeta
|
||||
regexReplaceAll
|
||||
@@ -146,9 +188,13 @@ reverse
|
||||
round
|
||||
seq
|
||||
set
|
||||
sha1Sum
|
||||
sha1sum
|
||||
sha256Sum
|
||||
sha256sum
|
||||
sha512Sum
|
||||
shellQuote
|
||||
shuffle
|
||||
slice
|
||||
sortAlpha
|
||||
spew
|
||||
@@ -158,18 +204,45 @@ splitLines
|
||||
splitList
|
||||
splitn
|
||||
squote
|
||||
strSlice
|
||||
sub
|
||||
subf
|
||||
substr
|
||||
swapCase
|
||||
ternary
|
||||
title
|
||||
toBool
|
||||
toCamelCase
|
||||
toConstantCase
|
||||
toDate
|
||||
toDecimal
|
||||
toDotCase
|
||||
toDuration
|
||||
toFloat64
|
||||
toIndentYAML
|
||||
toInt
|
||||
toInt64
|
||||
toJSON
|
||||
toJson
|
||||
toKebabCase
|
||||
toLocalDate
|
||||
toLower
|
||||
toOctal
|
||||
toPascalCase
|
||||
toPathCase
|
||||
toPrettyJSON
|
||||
toPrettyJson
|
||||
toRawJSON
|
||||
toRawJson
|
||||
toSlash
|
||||
toSnakeCase
|
||||
toString
|
||||
toStrings
|
||||
toTitleCase
|
||||
toUint
|
||||
toUint64
|
||||
toUpper
|
||||
toYAML
|
||||
toYaml
|
||||
trim
|
||||
trimAll
|
||||
@@ -181,14 +254,19 @@ tuple
|
||||
typeIs
|
||||
typeIsLike
|
||||
typeOf
|
||||
uncapitalize
|
||||
uniq
|
||||
unixEpoch
|
||||
unset
|
||||
until
|
||||
untilStep
|
||||
untitle
|
||||
upper
|
||||
urlJoin
|
||||
urlParse
|
||||
uuid
|
||||
uuidv4
|
||||
values
|
||||
without
|
||||
wrap
|
||||
wrapWith
|
||||
|
||||
Reference in New Issue
Block a user