test(templater): snapshot the template function surface and behaviour

Golden tests capturing the sorted list of template function names and the
rendered output of a representative expression per function. Generated
against the current slim-sprig implementation so that any change of
templating library produces an auditable diff.
This commit is contained in:
Valentin Maerten
2026-08-10 21:03:56 +02:00
parent 8e89cf879c
commit 1750f4560b
3 changed files with 886 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
package templater
import (
"fmt"
"maps"
"os"
"slices"
"strings"
"testing"
"time"
"github.com/sebdah/goldie/v2"
"github.com/go-task/task/v3/taskfile/ast"
)
// The two tests in this file snapshot the entire template surface: the set of
// function names, and the rendered output of a representative expression per
// function. They exist to make any change of templating library auditable —
// the golden diff is the exhaustive list of what changed for users.
//
// Expressions must stay deterministic and platform independent, so anything
// depending on the clock, the filesystem, the environment or randomness is
// deliberately absent (`now`, `uuid`, `randInt`, `env`, `os*`, `exeExt`, `OS`,
// `ARCH`, `spew`, `getHostByName`). Those are covered by unit tests instead.
// TestMain pins the local timezone so that the date expressions below render
// identically on every machine. A consequence is that this golden cannot show
// the local-vs-UTC difference between sprig's `date`/`toDate` and sprout's —
// that one belongs in the migration documentation.
func TestMain(m *testing.M) {
time.Local = time.UTC
os.Exit(m.Run())
}
func TestFuncNames(t *testing.T) {
t.Parallel()
names := slices.Sorted(maps.Keys(templateFuncs))
g := goldie.New(t)
g.Assert(t, "func_names", []byte(strings.Join(names, "\n")+"\n"))
}
func TestFuncBehaviour(t *testing.T) {
t.Parallel()
var b strings.Builder
for _, group := range funcBehaviourGroups {
fmt.Fprintf(&b, "## %s\n\n", group.name)
for _, expr := range group.exprs {
cache := &Cache{Vars: ast.NewVars()}
got := ReplaceWithExtra(expr, cache, nil)
fmt.Fprintf(&b, "%s\n", expr)
if err := cache.Err(); err != nil {
fmt.Fprintf(&b, "\t! %s\n", normalizeTemplateError(err))
} else {
fmt.Fprintf(&b, "\t= %s\n", strings.ReplaceAll(got, "\n", "\\n"))
}
}
b.WriteString("\n")
}
g := goldie.New(t)
g.Assert(t, "func_behaviour", []byte(b.String()))
}
// normalizeTemplateError strips the position prefix that text/template adds to
// execution errors, which is noise in the golden and shifts whenever an
// expression is added to a group.
func normalizeTemplateError(err error) string {
s := err.Error()
if _, after, found := strings.Cut(s, `at <`); found {
return "at <" + after
}
return s
}
var funcBehaviourGroups = []struct {
name string
exprs []string
}{
{
// The ten functions whose argument order differs between sprig and
// sprout. Written here in sprig order, which is what users' Taskfiles
// contain today.
"argument order — sprig order (target first)",
[]string{
`{{ get (dict "a" "b") "a" }}`,
`{{ set (dict "a" "b") "c" "d" | toJson }}`,
`{{ unset (dict "a" "b" "c" "d") "a" | toJson }}`,
`{{ hasKey (dict "a" "b") "a" }}`,
`{{ pick (dict "a" "1" "b" "2") "a" | toJson }}`,
`{{ omit (dict "a" "1" "b" "2") "a" | toJson }}`,
`{{ append (list 1 2) 3 | toJson }}`,
`{{ push (list 1 2) 3 | toJson }}`,
`{{ prepend (list 2 3) 1 | toJson }}`,
`{{ without (list 1 2 3) 2 | toJson }}`,
`{{ slice (list 1 2 3 4) 1 3 | toJson }}`,
},
},
{
// Same ten functions in sprout order. Today these mostly fail; after
// the migration both forms must work.
"argument order — sprout order (target last)",
[]string{
`{{ dict "a" "b" | get "a" }}`,
`{{ dict "a" "b" | set "c" "d" | toJson }}`,
`{{ dict "a" "b" "c" "d" | unset "a" | toJson }}`,
`{{ dict "a" "b" | hasKey "a" }}`,
`{{ list 1 2 | append 3 | toJson }}`,
`{{ list 2 3 | prepend 1 | toJson }}`,
},
},
{
"maps — unchanged signatures",
[]string{
`{{ dict "a" 1 "b" 2 | toJson }}`,
`{{ keys (dict "b" 1 "a" 2) | sortAlpha | toJson }}`,
`{{ values (dict "a" 1) | toJson }}`,
`{{ pluck "a" (dict "a" 1) (dict "a" 2) | toJson }}`,
`{{ dig "a" "b" "fallback" (dict "a" (dict "b" "found")) }}`,
`{{ dig "a" "missing" "fallback" (dict "a" (dict "b" "found")) }}`,
`{{ dig "a.b" "fallback" (dict "a" (dict "b" "found")) }}`,
`{{ merge (dict "a" 1) (dict "b" 2) | toJson }}`,
`{{ merge (dict "a" 1) (dict "a" 0) | toJson }}`,
},
},
{
"lists",
[]string{
`{{ list 1 2 3 | toJson }}`,
`{{ tuple 1 2 3 | toJson }}`,
`{{ first (list 1 2 3) }}`,
`{{ last (list 1 2 3) }}`,
`{{ rest (list 1 2 3) | toJson }}`,
`{{ initial (list 1 2 3) | toJson }}`,
`{{ reverse (list 1 2 3) | toJson }}`,
`{{ uniq (list 1 1 2) | toJson }}`,
`{{ compact (list 1 "" 2) | toJson }}`,
`{{ concat (list 1) (list 2) | toJson }}`,
`{{ chunk 2 (list 1 2 3) | toJson }}`,
`{{ has 2 (list 1 2 3) }}`,
`{{ sortAlpha (list "b" "a") | toJson }}`,
`{{ splitList "," "a,b,c" | toJson }}`,
`{{ toStrings (list 1 2) | toJson }}`,
`{{ until 3 | toJson }}`,
`{{ untilStep 0 6 2 | toJson }}`,
`{{ seq 1 3 }}`,
`{{ join "," (list "a" "b") }}`,
},
},
{
"strings",
[]string{
`{{ trim " x " }}`,
`{{ trimAll "-" "-x-" }}`,
`{{ trimall "-" "-x-" }}`,
`{{ trimPrefix "a" "ab" }}`,
`{{ trimSuffix "b" "ab" }}`,
`{{ upper "abc" }}`,
`{{ lower "ABC" }}`,
`{{ title "hello world" }}`,
`{{ title "hello wORLD" }}`,
`{{ trunc 3 "foobar" }}`,
`{{ trunc -3 "foobar" }}`,
`{{ substr 0 3 "foobar" }}`,
`{{ substr 0 -3 "foobar" }}`,
`{{ repeat 3 "x" }}`,
`{{ contains "oo" "foobar" }}`,
`{{ hasPrefix "foo" "foobar" }}`,
`{{ hasSuffix "bar" "foobar" }}`,
`{{ quote "x" }}`,
`{{ squote "x" }}`,
`{{ cat "a" "b" }}`,
`{{ indent 2 "x" }}`,
`{{ nindent 2 "x" }}`,
`{{ replace "a" "b" "aa" }}`,
`{{ plural "one" "many" 2 }}`,
`{{ split "," "a,b" | toJson }}`,
`{{ splitn "," 2 "a,b,c" | toJson }}`,
`{{ toString 42 }}`,
},
},
{
"numbers",
[]string{
`{{ add 1 2 }}`,
`{{ add1 1 }}`,
`{{ sub 5 2 }}`,
`{{ mul 2 3 }}`,
`{{ div 6 2 }}`,
`{{ mod 5 3 }}`,
`{{ max 1 5 3 }}`,
`{{ min 1 5 3 }}`,
`{{ biggest 1 5 3 }}`,
`{{ maxf 1.5 2.5 }}`,
`{{ minf 1.5 2.5 }}`,
`{{ ceil 1.1 }}`,
`{{ floor 1.9 }}`,
`{{ round 1.55 1 }}`,
`{{ atoi "42" }}`,
`{{ atoi "abc" }}`,
`{{ int "42" }}`,
`{{ int64 "42" }}`,
`{{ float64 "1.5" }}`,
`{{ toDecimal "0777" }}`,
},
},
{
"defaults and flow",
[]string{
`{{ default "d" "" }}`,
`{{ default "d" "x" }}`,
`{{ empty "" }}`,
`{{ empty 0 }}`,
`{{ coalesce "" "x" }}`,
`{{ all 1 1 }}`,
`{{ any 0 1 }}`,
`{{ ternary "y" "n" true }}`,
`{{ fail "boom" }}`,
},
},
{
"encoding",
[]string{
`{{ toJson (dict "a" 1) }}`,
`{{ toPrettyJson (dict "a" 1) }}`,
`{{ toRawJson (dict "a" "<b>") }}`,
`{{ fromJson "{\"a\":1}" | toJson }}`,
`{{ fromJson "not json" | toJson }}`,
`{{ mustFromJson "not json" | toJson }}`,
`{{ toYaml (dict "a" 1) }}`,
`{{ fromYaml "a: 1" | toJson }}`,
`{{ mustFromYaml "a: :" | toJson }}`,
`{{ b64enc "hello" }}`,
`{{ b64dec "aGVsbG8=" }}`,
`{{ b32enc "hello" }}`,
`{{ b32dec "NBSWY3DP" }}`,
},
},
{
"regex",
[]string{
`{{ regexMatch "^a" "abc" }}`,
`{{ regexFind "[0-9]+" "abc123" }}`,
`{{ regexFindAll "[0-9]" "a1b2" -1 | toJson }}`,
`{{ regexReplaceAll "[0-9]" "abc123" "#" }}`,
`{{ regexReplaceAllLiteral "[0-9]" "abc123" "#" }}`,
`{{ regexSplit "," "a,b" -1 | toJson }}`,
`{{ regexQuoteMeta "a.b" }}`,
`{{ mustRegexFind "[" "abc" }}`,
},
},
{
"reflection",
[]string{
`{{ typeOf 1 }}`,
`{{ typeIs "int" 1 }}`,
`{{ typeIsLike "int" 1 }}`,
`{{ kindOf 1 }}`,
`{{ kindOf (list 1) }}`,
`{{ kindIs "int" 1 }}`,
`{{ deepEqual (list 1) (list 1) }}`,
},
},
{
"checksums",
[]string{
`{{ sha1sum "x" }}`,
`{{ sha256sum "x" }}`,
`{{ adler32sum "x" }}`,
},
},
{
// path.* semantics — slash based, therefore identical on every platform.
"paths",
[]string{
`{{ base "/foo/bar.txt" }}`,
`{{ dir "/foo/bar.txt" }}`,
`{{ ext "/foo/bar.txt" }}`,
`{{ clean "/foo//bar" }}`,
`{{ isAbs "/foo" }}`,
},
},
{
"dates — fixed epoch, explicit zone",
[]string{
`{{ dateInZone "2006-01-02T15:04:05" 0 "UTC" }}`,
`{{ date "2006-01-02T15:04:05" 0 }}`,
`{{ dateModify "1h" (toDate "2006-01-02T15:04:05Z07:00" "2020-01-01T00:00:00Z") }}`,
`{{ date_modify "1h" (toDate "2006-01-02T15:04:05Z07:00" "2020-01-01T00:00:00Z") }}`,
`{{ toDate "2006-01-02" "2020-01-01" }}`,
`{{ unixEpoch (toDate "2006-01-02" "2020-01-01") }}`,
`{{ duration 90 }}`,
`{{ durationRound "1h35m30s" }}`,
`{{ htmlDate 0 }}`,
},
},
{
"urls",
[]string{
`{{ urlParse "http://example.com/a?b=c" | toJson }}`,
`{{ urlJoin (dict "scheme" "http" "host" "example.com" "path" "/a") }}`,
},
},
{
"task's own functions",
[]string{
`{{ numCPU | kindOf }}`,
`{{ catLines "a\nb" }}`,
`{{ splitLines "a\nb" | toJson }}`,
`{{ toSlash "a/b" }}`,
`{{ fromSlash "a/b" | kindOf }}`,
`{{ ToSlash "a/b" }}`,
`{{ shellQuote "a b" }}`,
`{{ q "a b" }}`,
`{{ splitArgs "a b c" | toJson }}`,
`{{ IsSH }}`,
`{{ joinUrl "http://localhost" "a" "b" }}`,
`{{ mustToYaml (dict "a" 1) }}`,
`{{ randIntN 1 }}`,
},
},
}

View File

@@ -0,0 +1,367 @@
## argument order — sprig order (target first)
{{ get (dict "a" "b") "a" }}
= b
{{ set (dict "a" "b") "c" "d" | toJson }}
= {"a":"b","c":"d"}
{{ unset (dict "a" "b" "c" "d") "a" | toJson }}
= {"c":"d"}
{{ hasKey (dict "a" "b") "a" }}
= true
{{ pick (dict "a" "1" "b" "2") "a" | toJson }}
= {"a":"1"}
{{ omit (dict "a" "1" "b" "2") "a" | toJson }}
= {"b":"2"}
{{ append (list 1 2) 3 | toJson }}
= [1,2,3]
{{ push (list 1 2) 3 | toJson }}
= [1,2,3]
{{ prepend (list 2 3) 1 | toJson }}
= [1,2,3]
{{ without (list 1 2 3) 2 | toJson }}
= [1,3]
{{ slice (list 1 2 3 4) 1 3 | toJson }}
= [2,3]
## argument order — sprout order (target last)
{{ dict "a" "b" | get "a" }}
! at <"a">: can't handle "a" for arg of type map[string]interface {}
{{ dict "a" "b" | set "c" "d" | toJson }}
! at <"c">: can't handle "c" for arg of type map[string]interface {}
{{ dict "a" "b" "c" "d" | unset "a" | toJson }}
! at <"a">: can't handle "a" for arg of type map[string]interface {}
{{ dict "a" "b" | hasKey "a" }}
! at <"a">: can't handle "a" for arg of type map[string]interface {}
{{ list 1 2 | append 3 | toJson }}
! at <append 3>: error calling append: Cannot push on type int
{{ list 2 3 | prepend 1 | toJson }}
! at <prepend 1>: error calling prepend: Cannot prepend on type int
## maps — unchanged signatures
{{ dict "a" 1 "b" 2 | toJson }}
= {"a":1,"b":2}
{{ keys (dict "b" 1 "a" 2) | sortAlpha | toJson }}
= ["a","b"]
{{ values (dict "a" 1) | toJson }}
= [1]
{{ pluck "a" (dict "a" 1) (dict "a" 2) | toJson }}
= [1,2]
{{ dig "a" "b" "fallback" (dict "a" (dict "b" "found")) }}
= found
{{ dig "a" "missing" "fallback" (dict "a" (dict "b" "found")) }}
= fallback
{{ dig "a.b" "fallback" (dict "a" (dict "b" "found")) }}
= fallback
{{ merge (dict "a" 1) (dict "b" 2) | toJson }}
= {"a":1,"b":2}
{{ merge (dict "a" 1) (dict "a" 0) | toJson }}
= {"a":0}
## lists
{{ list 1 2 3 | toJson }}
= [1,2,3]
{{ tuple 1 2 3 | toJson }}
= [1,2,3]
{{ first (list 1 2 3) }}
= 1
{{ last (list 1 2 3) }}
= 3
{{ rest (list 1 2 3) | toJson }}
= [2,3]
{{ initial (list 1 2 3) | toJson }}
= [1,2]
{{ reverse (list 1 2 3) | toJson }}
= [3,2,1]
{{ uniq (list 1 1 2) | toJson }}
= [1,2]
{{ compact (list 1 "" 2) | toJson }}
= [1,2]
{{ concat (list 1) (list 2) | toJson }}
= [1,2]
{{ chunk 2 (list 1 2 3) | toJson }}
= [[1,2],[3]]
{{ has 2 (list 1 2 3) }}
= true
{{ sortAlpha (list "b" "a") | toJson }}
= ["a","b"]
{{ splitList "," "a,b,c" | toJson }}
= ["a","b","c"]
{{ toStrings (list 1 2) | toJson }}
= ["1","2"]
{{ until 3 | toJson }}
= [0,1,2]
{{ untilStep 0 6 2 | toJson }}
= [0,2,4]
{{ seq 1 3 }}
= 1 2 3
{{ join "," (list "a" "b") }}
= a,b
## strings
{{ trim " x " }}
= x
{{ trimAll "-" "-x-" }}
= x
{{ trimall "-" "-x-" }}
= x
{{ trimPrefix "a" "ab" }}
= b
{{ trimSuffix "b" "ab" }}
= a
{{ upper "abc" }}
= ABC
{{ lower "ABC" }}
= abc
{{ title "hello world" }}
= Hello World
{{ title "hello wORLD" }}
= Hello WORLD
{{ trunc 3 "foobar" }}
= foo
{{ trunc -3 "foobar" }}
= bar
{{ substr 0 3 "foobar" }}
= foo
{{ substr 0 -3 "foobar" }}
= foobar
{{ repeat 3 "x" }}
= xxx
{{ contains "oo" "foobar" }}
= true
{{ hasPrefix "foo" "foobar" }}
= true
{{ hasSuffix "bar" "foobar" }}
= true
{{ quote "x" }}
= "x"
{{ squote "x" }}
= 'x'
{{ cat "a" "b" }}
= a b
{{ indent 2 "x" }}
= x
{{ nindent 2 "x" }}
= \n x
{{ replace "a" "b" "aa" }}
= bb
{{ plural "one" "many" 2 }}
= many
{{ split "," "a,b" | toJson }}
= {"_0":"a","_1":"b"}
{{ splitn "," 2 "a,b,c" | toJson }}
= {"_0":"a","_1":"b,c"}
{{ toString 42 }}
= 42
## numbers
{{ add 1 2 }}
= 3
{{ add1 1 }}
= 2
{{ sub 5 2 }}
= 3
{{ mul 2 3 }}
= 6
{{ div 6 2 }}
= 3
{{ mod 5 3 }}
= 2
{{ max 1 5 3 }}
= 5
{{ min 1 5 3 }}
= 1
{{ biggest 1 5 3 }}
= 5
{{ maxf 1.5 2.5 }}
= 2.5
{{ minf 1.5 2.5 }}
= 1.5
{{ ceil 1.1 }}
= 2
{{ floor 1.9 }}
= 1
{{ round 1.55 1 }}
= 1.6
{{ atoi "42" }}
= 42
{{ atoi "abc" }}
= 0
{{ int "42" }}
= 42
{{ int64 "42" }}
= 42
{{ float64 "1.5" }}
= 1.5
{{ toDecimal "0777" }}
= 511
## defaults and flow
{{ default "d" "" }}
= d
{{ default "d" "x" }}
= x
{{ empty "" }}
= true
{{ empty 0 }}
= true
{{ coalesce "" "x" }}
= x
{{ all 1 1 }}
= true
{{ any 0 1 }}
= true
{{ ternary "y" "n" true }}
= y
{{ fail "boom" }}
! at <fail "boom">: error calling fail: boom
## encoding
{{ toJson (dict "a" 1) }}
= {"a":1}
{{ toPrettyJson (dict "a" 1) }}
= {\n "a": 1\n}
{{ toRawJson (dict "a" "<b>") }}
= {"a":"<b>"}
{{ fromJson "{\"a\":1}" | toJson }}
= {"a":1}
{{ fromJson "not json" | toJson }}
= null
{{ mustFromJson "not json" | toJson }}
! at <mustFromJson "not json">: error calling mustFromJson: invalid character 'o' in literal null (expecting 'u')
{{ toYaml (dict "a" 1) }}
= a: 1\n
{{ fromYaml "a: 1" | toJson }}
= {"a":1}
{{ mustFromYaml "a: :" | toJson }}
! at <mustFromYaml "a: :">: error calling mustFromYaml: yaml: mapping values are not allowed in this context
{{ b64enc "hello" }}
= aGVsbG8=
{{ b64dec "aGVsbG8=" }}
= hello
{{ b32enc "hello" }}
= NBSWY3DP
{{ b32dec "NBSWY3DP" }}
= hello
## regex
{{ regexMatch "^a" "abc" }}
= true
{{ regexFind "[0-9]+" "abc123" }}
= 123
{{ regexFindAll "[0-9]" "a1b2" -1 | toJson }}
= ["1","2"]
{{ regexReplaceAll "[0-9]" "abc123" "#" }}
= abc###
{{ regexReplaceAllLiteral "[0-9]" "abc123" "#" }}
= abc###
{{ regexSplit "," "a,b" -1 | toJson }}
= ["a","b"]
{{ regexQuoteMeta "a.b" }}
= a\.b
{{ mustRegexFind "[" "abc" }}
! at <mustRegexFind "[" "abc">: error calling mustRegexFind: error parsing regexp: missing closing ]: `[`
## reflection
{{ typeOf 1 }}
= int
{{ typeIs "int" 1 }}
= true
{{ typeIsLike "int" 1 }}
= true
{{ kindOf 1 }}
= int
{{ kindOf (list 1) }}
= slice
{{ kindIs "int" 1 }}
= true
{{ deepEqual (list 1) (list 1) }}
= true
## checksums
{{ sha1sum "x" }}
= 11f6ad8ec52a2984abaafd7c3b516503785c2072
{{ sha256sum "x" }}
= 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
{{ adler32sum "x" }}
= 7929977
## paths
{{ base "/foo/bar.txt" }}
= bar.txt
{{ dir "/foo/bar.txt" }}
= /foo
{{ ext "/foo/bar.txt" }}
= .txt
{{ clean "/foo//bar" }}
= /foo/bar
{{ isAbs "/foo" }}
= true
## dates — fixed epoch, explicit zone
{{ dateInZone "2006-01-02T15:04:05" 0 "UTC" }}
= 1970-01-01T00:00:00
{{ date "2006-01-02T15:04:05" 0 }}
= 1970-01-01T00:00:00
{{ dateModify "1h" (toDate "2006-01-02T15:04:05Z07:00" "2020-01-01T00:00:00Z") }}
= 2020-01-01 01:00:00 +0000 UTC
{{ date_modify "1h" (toDate "2006-01-02T15:04:05Z07:00" "2020-01-01T00:00:00Z") }}
= 2020-01-01 01:00:00 +0000 UTC
{{ toDate "2006-01-02" "2020-01-01" }}
= 2020-01-01 00:00:00 +0000 UTC
{{ unixEpoch (toDate "2006-01-02" "2020-01-01") }}
= 1577836800
{{ duration 90 }}
= 0s
{{ durationRound "1h35m30s" }}
= 1h
{{ htmlDate 0 }}
= 1970-01-01
## urls
{{ urlParse "http://example.com/a?b=c" | toJson }}
= {"fragment":"","host":"example.com","hostname":"example.com","opaque":"","path":"/a","query":"b=c","scheme":"http","userinfo":""}
{{ urlJoin (dict "scheme" "http" "host" "example.com" "path" "/a") }}
= http://example.com/a
## task's own functions
{{ numCPU | kindOf }}
= int
{{ catLines "a\nb" }}
= a b
{{ splitLines "a\nb" | toJson }}
= ["a","b"]
{{ toSlash "a/b" }}
= a/b
{{ fromSlash "a/b" | kindOf }}
= string
{{ ToSlash "a/b" }}
= a/b
{{ shellQuote "a b" }}
= 'a b'
{{ q "a b" }}
= 'a b'
{{ splitArgs "a b c" | toJson }}
= ["a","b","c"]
{{ IsSH }}
= true
{{ joinUrl "http://localhost" "a" "b" }}
= http://localhost/a/b
{{ mustToYaml (dict "a" 1) }}
= a: 1\n
{{ randIntN 1 }}
= 0

View File

@@ -0,0 +1,194 @@
ARCH
ExeExt
FromSlash
IsSH
OS
ToSlash
absPath
add
add1
adler32sum
ago
all
any
append
atoi
b32dec
b32enc
b64dec
b64enc
base
biggest
cat
catLines
ceil
chunk
clean
coalesce
compact
concat
contains
date
dateInZone
dateModify
date_in_zone
date_modify
deepEqual
default
dict
dig
dir
div
duration
durationRound
empty
env
exeExt
expandenv
ext
fail
first
float64
floor
fromJson
fromSlash
fromYaml
get
getHostByName
has
hasKey
hasPrefix
hasSuffix
hello
htmlDate
htmlDateInZone
indent
initial
int
int64
isAbs
join
joinEnv
joinPath
joinUrl
keys
kindIs
kindOf
last
list
lower
max
maxf
merge
min
minf
mod
mul
mustAppend
mustChunk
mustCompact
mustDateModify
mustFirst
mustFromJson
mustFromYaml
mustHas
mustInitial
mustLast
mustPrepend
mustPush
mustRegexFind
mustRegexFindAll
mustRegexMatch
mustRegexReplaceAll
mustRegexReplaceAllLiteral
mustRegexSplit
mustRest
mustReverse
mustSlice
mustToDate
mustToJson
mustToPrettyJson
mustToRawJson
mustToYaml
mustUniq
mustWithout
must_date_modify
nindent
now
numCPU
omit
osBase
osClean
osDir
osExt
osIsAbs
pick
pluck
plural
prepend
push
q
quote
randInt
randIntN
regexFind
regexFindAll
regexMatch
regexQuoteMeta
regexReplaceAll
regexReplaceAllLiteral
regexSplit
relPath
repeat
replace
rest
reverse
round
seq
set
sha1sum
sha256sum
shellQuote
slice
sortAlpha
spew
split
splitArgs
splitLines
splitList
splitn
squote
sub
substr
ternary
title
toDate
toDecimal
toJson
toPrettyJson
toRawJson
toSlash
toString
toStrings
toYaml
trim
trimAll
trimPrefix
trimSuffix
trimall
trunc
tuple
typeIs
typeIsLike
typeOf
uniq
unixEpoch
unset
until
untilStep
upper
urlJoin
urlParse
uuid
values
without