From bf5e4c56a50a472cf192bbd2e2961cc912a42e53 Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Sat, 18 May 2024 13:45:09 +0100 Subject: [PATCH 1/9] feat: add more list funcs --- docs/templatefuncs.md | 31 +++++++++++++++++++++++++++++++ go.mod | 2 +- templatefuncs.go | 38 ++++++++++++++++++++++++++++++++++++++ templatefuncs_test.go | 17 ++++++++++++++++- 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/docs/templatefuncs.md b/docs/templatefuncs.md index 4d9a362..d3abd1d 100644 --- a/docs/templatefuncs.md +++ b/docs/templatefuncs.md @@ -1,5 +1,15 @@ # Template Functions +## `compact` *list* + +`compact` removes all zero value items from *list*. + +```text +{{ list "one" "" list "three" | compact }} + +[one three] +``` + ## `contains` *substring* *string* `contains` returns whether *substring* is in *string*. @@ -29,6 +39,16 @@ true {{ `{ "foo": "bar" }` | fromJSON }} ``` +## `has` *item* *list* + +`has` returns whether *item* is in *list*. + +```text +{{ list 1 2 3 | has 3 }} + +true +``` + ## `hasPrefix` *prefix* *string* `hasPrefix` returns whether *string* begins with *prefix*. @@ -69,6 +89,17 @@ foobar 666f6f626172 ``` +## `indexOf` *item* *list* + +`indexOf` returns the index of *item* in *list*, or -1 if *item* is not +in *list*. + +```text +{{ list "a" "b" "c" | indexOf "b" }} + +1 +``` + ## `join` *delimiter* *list* `join` returns a string containing each item in *list* joined with *delimiter*. diff --git a/go.mod b/go.mod index 2e9528c..55a7e89 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/chezmoi/templatefuncs -go 1.19 +go 1.21 require github.com/alecthomas/assert/v2 v2.9.0 diff --git a/templatefuncs.go b/templatefuncs.go index de03047..be53de7 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -8,7 +8,9 @@ import ( "io/fs" "os" "os/exec" + "reflect" "regexp" + "slices" "strconv" "strings" "text/template" @@ -29,13 +31,16 @@ var fileModeTypeNames = map[fs.FileMode]string{ // functions. func NewFuncMap() template.FuncMap { return template.FuncMap{ + "compact": compactTemplateFunc, "contains": reverseArgs2(strings.Contains), "eqFold": eqFoldTemplateFunc, "fromJSON": eachByteSliceErr(fromJSONTemplateFunc), + "has": reverseArgs2(slices.Contains[[]any]), "hasPrefix": reverseArgs2(strings.HasPrefix), "hasSuffix": reverseArgs2(strings.HasSuffix), "hexDecode": eachStringErr(hex.DecodeString), "hexEncode": eachByteSlice(hex.EncodeToString), + "indexOf": reverseArgs2(slices.Index[[]any]), "join": reverseArgs2(strings.Join), "list": listTemplateFunc, "lookPath": eachStringErr(lookPathTemplateFunc), @@ -53,6 +58,12 @@ func NewFuncMap() template.FuncMap { } } +// compactTemplateFunc is the core implementation of the `compact` template +// function. +func compactTemplateFunc(list []any) []any { + return slices.DeleteFunc(list, isZeroValue) +} + // eqFoldTemplateFunc is the core implementation of the `eqFold` template // function. func eqFoldTemplateFunc(first, second string, more ...string) bool { @@ -377,6 +388,33 @@ func fileInfoToMap(fileInfo fs.FileInfo) map[string]any { } } +// isZeroValue returns whether a value is the zero value for its type. +// An empty array, map or slice is assumed to be a zero value. +func isZeroValue(v any) bool { + vval := reflect.ValueOf(v) + if !vval.IsValid() { + return true + } + switch vval.Kind() { //nolint:exhaustive + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return vval.Len() == 0 + case reflect.Bool: + return !vval.Bool() + case reflect.Complex64, reflect.Complex128: + return vval.Complex() == 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return vval.Int() == 0 + case reflect.Float32, reflect.Float64: + return vval.Float() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return vval.Uint() == 0 + case reflect.Struct: + return false + default: + return vval.IsNil() + } +} + // reverseArgs2 transforms a function that takes two arguments and returns an // `R` into a function that takes the arguments in reverse order and returns an // `R`. diff --git a/templatefuncs_test.go b/templatefuncs_test.go index 5a13fa6..ec544d9 100644 --- a/templatefuncs_test.go +++ b/templatefuncs_test.go @@ -50,7 +50,10 @@ func TestFuncMap(t *testing.T) { data any expected string }{ - {}, + { + template: `{{ list "one" "" list "three" | compact }}`, + expected: `[one three]`, + }, { template: `{{ "abc" | contains "bc" }}`, expected: "true", @@ -75,6 +78,14 @@ func TestFuncMap(t *testing.T) { template: `{{ fromJSON "0" }}`, expected: "0", }, + { + template: `{{ list 1 2 3 | has 3 }}`, + expected: "true", + }, + { + template: `{{ has 3 (list 1 2 3) }}`, + expected: "true", + }, { template: `{{ "ab" | hasPrefix "a" }}`, expected: "true", @@ -91,6 +102,10 @@ func TestFuncMap(t *testing.T) { template: `{{ "ab" | hasSuffix "b" }}`, expected: "true", }, + { + template: `{{ list "a" "b" "c" | indexOf "b" }}`, + expected: "1", + }, { template: `{{ list "a" "b" "c" | quote | join "," }}`, expected: `"a","b","c"`, From 3fd6298420babd3e4cb1017c6f4846f0b752609e Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Sat, 18 May 2024 14:48:37 +0100 Subject: [PATCH 2/9] reverse --- docs/templatefuncs.md | 10 ++++++++++ templatefuncs.go | 9 +++++++++ templatefuncs_test.go | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/docs/templatefuncs.md b/docs/templatefuncs.md index d3abd1d..8bf448b 100644 --- a/docs/templatefuncs.md +++ b/docs/templatefuncs.md @@ -183,6 +183,16 @@ far adcda ``` +## `reverse` *list* + +`reverse` returns a copy of *list* in reverse order. + +```text +{{ list "a" "b" "c" }} + +[c b a] +``` + ## `stat` *path* `stat` returns a map representation of executing diff --git a/templatefuncs.go b/templatefuncs.go index be53de7..a2eec24 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -49,6 +49,7 @@ func NewFuncMap() template.FuncMap { "quote": eachString(strconv.Quote), "regexpReplaceAll": regexpReplaceAllTemplateFunc, "replaceAll": replaceAllTemplateFunc, + "reverse": reverseTemplateFunc, "stat": eachString(statTemplateFunc), "toJSON": toJSONTemplateFunc, "toLower": eachString(strings.ToLower), @@ -170,6 +171,14 @@ func regexpReplaceAllTemplateFunc(expr, repl, s string) string { return regexp.MustCompile(expr).ReplaceAllString(s, repl) } +// reverseTemplateFunc is the core implementation of the `reverse` +// template function. +func reverseTemplateFunc(list []any) []any { + listcopy := append([]any(nil), list...) + slices.Reverse(listcopy) + return listcopy +} + // statTemplateFunc is the core implementation of the `stat` template function. func statTemplateFunc(name string) any { switch fileInfo, err := os.Stat(name); { diff --git a/templatefuncs_test.go b/templatefuncs_test.go index ec544d9..39331fe 100644 --- a/templatefuncs_test.go +++ b/templatefuncs_test.go @@ -145,6 +145,10 @@ func TestFuncMap(t *testing.T) { template: `{{ quote "a" }}`, expected: `"a"`, }, + { + template: `{{ list "a" "b" "c" | reverse }}`, + expected: "[c b a]", + }, { template: `{{ (stat "testdata/file").type }}`, expected: "file", From b674fe912de30997bf42610159217d839e5dd754 Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Sat, 18 May 2024 20:06:52 +0100 Subject: [PATCH 3/9] terrible sort --- docs/templatefuncs.md | 13 ++++++++++++- templatefuncs.go | 20 ++++++++++++++++++++ templatefuncs_test.go | 16 ++++++++++++---- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/docs/templatefuncs.md b/docs/templatefuncs.md index 8bf448b..8f85a18 100644 --- a/docs/templatefuncs.md +++ b/docs/templatefuncs.md @@ -188,11 +188,22 @@ adcda `reverse` returns a copy of *list* in reverse order. ```text -{{ list "a" "b" "c" }} +{{ list "a" "b" "c" | reverse }} [c b a] ``` +## `sort` *list* + +`sort` returns *list* sorted in ascending order. +If *list* cannot be sorted, it is simply returned. + +```text +{{ list list "c" "a" "b" | sort }} + +[a b c] +``` + ## `stat` *path* `stat` returns a map representation of executing diff --git a/templatefuncs.go b/templatefuncs.go index a2eec24..e31d970 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -50,6 +50,7 @@ func NewFuncMap() template.FuncMap { "regexpReplaceAll": regexpReplaceAllTemplateFunc, "replaceAll": replaceAllTemplateFunc, "reverse": reverseTemplateFunc, + "sort": sortTemplateFunc, "stat": eachString(statTemplateFunc), "toJSON": toJSONTemplateFunc, "toLower": eachString(strings.ToLower), @@ -179,6 +180,25 @@ func reverseTemplateFunc(list []any) []any { return listcopy } +// sortTemplateFunc is the core implementation of the `sort` template function. +func sortTemplateFunc(list []any) []any { + strCopy := make([]string, len(list)) + for i, v := range list { + strCopy[i] = toStringTemplateFunc(v) + } + slices.Sort(strCopy) + for i, newValue := range strCopy { + for j, v := range list { + strv := toStringTemplateFunc(v) + if strv == newValue { + list[i], list[j] = list[j], list[i] + break + } + } + } + return list +} + // statTemplateFunc is the core implementation of the `stat` template function. func statTemplateFunc(name string) any { switch fileInfo, err := os.Stat(name); { diff --git a/templatefuncs_test.go b/templatefuncs_test.go index 39331fe..08c532b 100644 --- a/templatefuncs_test.go +++ b/templatefuncs_test.go @@ -133,6 +133,10 @@ func TestFuncMap(t *testing.T) { "# b", ), }, + { + template: `{{ quote "a" }}`, + expected: `"a"`, + }, { template: `{{ "abcba" | replaceAll "b" "d" }}`, expected: `adcda`, @@ -141,14 +145,18 @@ func TestFuncMap(t *testing.T) { template: `{{ list "abc" "cba" | replaceAll "b" "d" }}`, expected: "[adc cda]", }, - { - template: `{{ quote "a" }}`, - expected: `"a"`, - }, { template: `{{ list "a" "b" "c" | reverse }}`, expected: "[c b a]", }, + { + template: `{{ list "c" "a" "b" | sort }}`, + expected: "[a b c]", + }, + { + template: `{{ list 0 4 5 1 | sort }}`, + expected: "[0 1 4 5]", + }, { template: `{{ (stat "testdata/file").type }}`, expected: "file", From dc69ef8f45b6dbdbcec1ee140063fd3da1be17c1 Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Sun, 19 May 2024 13:57:27 +0100 Subject: [PATCH 4/9] ugly sort --- docs/templatefuncs.md | 2 +- templatefuncs.go | 174 +++++++++++++++++++++++++++++++++--------- 2 files changed, 141 insertions(+), 35 deletions(-) diff --git a/docs/templatefuncs.md b/docs/templatefuncs.md index 8f85a18..60bf095 100644 --- a/docs/templatefuncs.md +++ b/docs/templatefuncs.md @@ -195,7 +195,7 @@ adcda ## `sort` *list* -`sort` returns *list* sorted in ascending order. +`sort` returns a copy of *list* sorted in ascending order. If *list* cannot be sorted, it is simply returned. ```text diff --git a/templatefuncs.go b/templatefuncs.go index e31d970..4b9de83 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -181,22 +181,123 @@ func reverseTemplateFunc(list []any) []any { } // sortTemplateFunc is the core implementation of the `sort` template function. -func sortTemplateFunc(list []any) []any { - strCopy := make([]string, len(list)) - for i, v := range list { - strCopy[i] = toStringTemplateFunc(v) +// +//nolint:exhaustive,forcetypeassert,gocognit,gocyclo +func sortTemplateFunc(list []any) any { + if len(list) < 2 { + return list } - slices.Sort(strCopy) - for i, newValue := range strCopy { - for j, v := range list { - strv := toStringTemplateFunc(v) - if strv == newValue { - list[i], list[j] = list[j], list[i] - break - } + + firstElemType := reflect.TypeOf(list[0]) + + for _, elem := range list[1:] { + if reflect.TypeOf(elem) != firstElemType { + return list } } - return list + + switch firstElemType.Kind() { + case reflect.Int: + l := make([]int, len(list)) + for i, elem := range list { + l[i] = elem.(int) + } + slices.Sort(l) + return l + case reflect.Int8: + l := make([]int8, len(list)) + for i, elem := range list { + l[i] = elem.(int8) + } + slices.Sort(l) + return l + case reflect.Int16: + l := make([]int16, len(list)) + for i, elem := range list { + l[i] = elem.(int16) + } + slices.Sort(l) + return l + case reflect.Int32: + l := make([]int32, len(list)) + for i, elem := range list { + l[i] = elem.(int32) + } + slices.Sort(l) + return l + case reflect.Int64: + l := make([]int64, len(list)) + for i, elem := range list { + l[i] = elem.(int64) + } + slices.Sort(l) + return l + case reflect.Uint: + l := make([]uint, len(list)) + for i, elem := range list { + l[i] = elem.(uint) + } + slices.Sort(l) + return l + case reflect.Uint8: + l := make([]uint8, len(list)) + for i, elem := range list { + l[i] = elem.(uint8) + } + slices.Sort(l) + return l + case reflect.Uint16: + l := make([]uint16, len(list)) + for i, elem := range list { + l[i] = elem.(uint16) + } + slices.Sort(l) + return l + case reflect.Uint32: + l := make([]uint32, len(list)) + for i, elem := range list { + l[i] = elem.(uint32) + } + slices.Sort(l) + return l + case reflect.Uint64: + l := make([]uint64, len(list)) + for i, elem := range list { + l[i] = elem.(uint64) + } + slices.Sort(l) + return l + case reflect.Uintptr: + l := make([]uintptr, len(list)) + for i, elem := range list { + l[i] = elem.(uintptr) + } + slices.Sort(l) + return l + case reflect.Float32: + l := make([]float32, len(list)) + for i, elem := range list { + l[i] = elem.(float32) + } + slices.Sort(l) + return l + case reflect.Float64: + l := make([]float64, len(list)) + for i, elem := range list { + l[i] = elem.(float64) + } + slices.Sort(l) + return l + case reflect.String: + l := make([]string, len(list)) + for i, elem := range list { + l[i] = elem.(string) + } + slices.Sort(l) + return l + default: + return list + } } // statTemplateFunc is the core implementation of the `stat` template function. @@ -420,28 +521,33 @@ func fileInfoToMap(fileInfo fs.FileInfo) map[string]any { // isZeroValue returns whether a value is the zero value for its type. // An empty array, map or slice is assumed to be a zero value. func isZeroValue(v any) bool { - vval := reflect.ValueOf(v) - if !vval.IsValid() { - return true - } - switch vval.Kind() { //nolint:exhaustive - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - return vval.Len() == 0 - case reflect.Bool: - return !vval.Bool() - case reflect.Complex64, reflect.Complex128: - return vval.Complex() == 0 - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return vval.Int() == 0 - case reflect.Float32, reflect.Float64: - return vval.Float() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return vval.Uint() == 0 - case reflect.Struct: - return false - default: - return vval.IsNil() + truth, ok := template.IsTrue(v) + if !ok { + panic(fmt.Sprintf("unable to determine zero value for %v", v)) } + return !truth + // vval := reflect.ValueOf(v) + // if !vval.IsValid() { + // return true + // } + // switch vval.Kind() { //nolint:exhaustive + // case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + // return vval.Len() == 0 + // case reflect.Bool: + // return !vval.Bool() + // case reflect.Complex64, reflect.Complex128: + // return vval.Complex() == 0 + // case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + // return vval.Int() == 0 + // case reflect.Float32, reflect.Float64: + // return vval.Float() == 0 + // case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + // return vval.Uint() == 0 + // case reflect.Struct: + // return false + // default: + // return vval.IsNil() + // } } // reverseArgs2 transforms a function that takes two arguments and returns an From 5d26c4e3af96287556c1b99a6776d024add50675 Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Sun, 19 May 2024 14:18:13 +0100 Subject: [PATCH 5/9] slightly less ugly sort --- templatefuncs.go | 117 ++++++++++++----------------------------------- 1 file changed, 30 insertions(+), 87 deletions(-) diff --git a/templatefuncs.go b/templatefuncs.go index 4b9de83..66937cc 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -1,6 +1,7 @@ package templatefuncs import ( + "cmp" "encoding/hex" "encoding/json" "errors" @@ -181,8 +182,6 @@ func reverseTemplateFunc(list []any) []any { } // sortTemplateFunc is the core implementation of the `sort` template function. -// -//nolint:exhaustive,forcetypeassert,gocognit,gocyclo func sortTemplateFunc(list []any) any { if len(list) < 2 { return list @@ -196,105 +195,35 @@ func sortTemplateFunc(list []any) any { } } - switch firstElemType.Kind() { + switch firstElemType.Kind() { //nolint:exhaustive case reflect.Int: - l := make([]int, len(list)) - for i, elem := range list { - l[i] = elem.(int) - } - slices.Sort(l) - return l + return convertAndSortSlice[int](list) case reflect.Int8: - l := make([]int8, len(list)) - for i, elem := range list { - l[i] = elem.(int8) - } - slices.Sort(l) - return l + return convertAndSortSlice[int8](list) case reflect.Int16: - l := make([]int16, len(list)) - for i, elem := range list { - l[i] = elem.(int16) - } - slices.Sort(l) - return l + return convertAndSortSlice[int16](list) case reflect.Int32: - l := make([]int32, len(list)) - for i, elem := range list { - l[i] = elem.(int32) - } - slices.Sort(l) - return l + return convertAndSortSlice[int32](list) case reflect.Int64: - l := make([]int64, len(list)) - for i, elem := range list { - l[i] = elem.(int64) - } - slices.Sort(l) - return l + return convertAndSortSlice[int64](list) case reflect.Uint: - l := make([]uint, len(list)) - for i, elem := range list { - l[i] = elem.(uint) - } - slices.Sort(l) - return l + return convertAndSortSlice[uint](list) case reflect.Uint8: - l := make([]uint8, len(list)) - for i, elem := range list { - l[i] = elem.(uint8) - } - slices.Sort(l) - return l + return convertAndSortSlice[uint8](list) case reflect.Uint16: - l := make([]uint16, len(list)) - for i, elem := range list { - l[i] = elem.(uint16) - } - slices.Sort(l) - return l + return convertAndSortSlice[uint16](list) case reflect.Uint32: - l := make([]uint32, len(list)) - for i, elem := range list { - l[i] = elem.(uint32) - } - slices.Sort(l) - return l + return convertAndSortSlice[uint32](list) case reflect.Uint64: - l := make([]uint64, len(list)) - for i, elem := range list { - l[i] = elem.(uint64) - } - slices.Sort(l) - return l + return convertAndSortSlice[uint64](list) case reflect.Uintptr: - l := make([]uintptr, len(list)) - for i, elem := range list { - l[i] = elem.(uintptr) - } - slices.Sort(l) - return l + return convertAndSortSlice[uintptr](list) case reflect.Float32: - l := make([]float32, len(list)) - for i, elem := range list { - l[i] = elem.(float32) - } - slices.Sort(l) - return l + return convertAndSortSlice[float32](list) case reflect.Float64: - l := make([]float64, len(list)) - for i, elem := range list { - l[i] = elem.(float64) - } - slices.Sort(l) - return l + return convertAndSortSlice[float64](list) case reflect.String: - l := make([]string, len(list)) - for i, elem := range list { - l[i] = elem.(string) - } - slices.Sort(l) - return l + return convertAndSortSlice[string](list) default: return list } @@ -348,6 +277,20 @@ func toStringTemplateFunc(arg any) string { } } +// convertAndSortSlice converts a `[]any` to a `[]T` and sorts it. +func convertAndSortSlice[T cmp.Ordered](slice []any) []T { + l := make([]T, len(slice)) + for i, elem := range slice { + v, ok := elem.(T) + if !ok { + panic(fmt.Sprintf("unable to convert %v (type %T) to %T", elem, elem, v)) + } + l[i] = v + } + slices.Sort(l) + return l +} + // eachByteSlice transforms a function that takes a single `[]byte` and returns // a `T` to a function that takes zero or more `[]byte`-like arguments and // returns zero or more `T`s. From 0e91528757fe8db268e5787e0b342b40770b0cee Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Sun, 19 May 2024 20:58:54 +0100 Subject: [PATCH 6/9] uniq --- docs/templatefuncs.md | 10 ++++++++++ templatefuncs.go | 18 +++++++++++++++++- templatefuncs_test.go | 4 ++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/templatefuncs.md b/docs/templatefuncs.md index 60bf095..1e7fe85 100644 --- a/docs/templatefuncs.md +++ b/docs/templatefuncs.md @@ -262,3 +262,13 @@ FOOBAR foobar ``` + +## `uniq` *list* + +`uniq` returns a new list containing only unique elements in *list*. + +```text +{{ list 1 2 1 3 3 2 1 2 | uniq }} + +[1 2 3] +``` diff --git a/templatefuncs.go b/templatefuncs.go index 66937cc..e6cc007 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -58,6 +58,7 @@ func NewFuncMap() template.FuncMap { "toString": toStringTemplateFunc, "toUpper": eachString(strings.ToUpper), "trimSpace": eachString(strings.TrimSpace), + "uniq": uniqTemplateFunc, } } @@ -277,7 +278,22 @@ func toStringTemplateFunc(arg any) string { } } -// convertAndSortSlice converts a `[]any` to a `[]T` and sorts it. +// uniqTemplateFunc is the core implementation of the `uniq` template function. +func uniqTemplateFunc(list []any) []any { + seen := make(map[any]struct{}) + result := []any{} + + for _, v := range list { + if _, ok := seen[v]; !ok { + result = append(result, v) + seen[v] = struct{}{} + } + } + + return result +} + +// convertAndSortSlice creates a `[]T` copy of its input and sorts it. func convertAndSortSlice[T cmp.Ordered](slice []any) []T { l := make([]T, len(slice)) for i, elem := range slice { diff --git a/templatefuncs_test.go b/templatefuncs_test.go index 08c532b..3ad4904 100644 --- a/templatefuncs_test.go +++ b/templatefuncs_test.go @@ -169,6 +169,10 @@ func TestFuncMap(t *testing.T) { template: `{{ trimSpace " a " }}`, expected: "a", }, + { + template: `{{ list 1 2 1 3 3 2 1 2 | uniq }}`, + expected: "[1 2 3]", + }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { tmpl, err := template.New("").Funcs(funcMap).Parse(tc.template) From ea3fac2fc13b361832b96a0f0c696686be6e698b Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Sun, 19 May 2024 21:34:51 +0100 Subject: [PATCH 7/9] concat --- .github/workflows/main.yml | 4 ++++ docs/templatefuncs.md | 10 ++++++++++ go.mod | 2 +- templatefuncs.go | 1 + templatefuncs_test.go | 4 ++++ 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea0a4e9..0efe056 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,16 @@ jobs: steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 + with: + go-version: 'stable' - run: go test ./... lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 + with: + go-version: 'stable' - uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 with: version: v1.58.1 diff --git a/docs/templatefuncs.md b/docs/templatefuncs.md index 1e7fe85..25a57da 100644 --- a/docs/templatefuncs.md +++ b/docs/templatefuncs.md @@ -10,6 +10,16 @@ [one three] ``` +## `concat` *list*... + +`concat` concatenates *list*s into a new list. + +```text +{{ concat (list 0 1 2) (list "a" "b" "c") }} + +[0 1 2 a b c] +``` + ## `contains` *substring* *string* `contains` returns whether *substring* is in *string*. diff --git a/go.mod b/go.mod index 55a7e89..1eee1a6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/chezmoi/templatefuncs -go 1.21 +go 1.22 require github.com/alecthomas/assert/v2 v2.9.0 diff --git a/templatefuncs.go b/templatefuncs.go index e6cc007..2477da9 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -33,6 +33,7 @@ var fileModeTypeNames = map[fs.FileMode]string{ func NewFuncMap() template.FuncMap { return template.FuncMap{ "compact": compactTemplateFunc, + "concat": slices.Concat[[]any], "contains": reverseArgs2(strings.Contains), "eqFold": eqFoldTemplateFunc, "fromJSON": eachByteSliceErr(fromJSONTemplateFunc), diff --git a/templatefuncs_test.go b/templatefuncs_test.go index 3ad4904..d4093ee 100644 --- a/templatefuncs_test.go +++ b/templatefuncs_test.go @@ -54,6 +54,10 @@ func TestFuncMap(t *testing.T) { template: `{{ list "one" "" list "three" | compact }}`, expected: `[one three]`, }, + { + template: `{{ concat (list 0 1 2) (list "a" "b" "c") }}`, + expected: "[0 1 2 a b c]", + }, { template: `{{ "abc" | contains "bc" }}`, expected: "true", From f97e5683096010a9d8b1d4a88b08ec9141062a84 Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Mon, 20 May 2024 20:07:40 +0100 Subject: [PATCH 8/9] new compact --- .golangci.yml | 2 + docs/templatefuncs.md | 62 -------------------- templatefuncs.go | 132 +++++------------------------------------- templatefuncs_test.go | 61 ++++++++++--------- 4 files changed, 45 insertions(+), 212 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 9d6ccad..d1bac81 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -104,6 +104,8 @@ linters: - wsl linters-settings: + exhaustive: + default-signifies-exhaustive: true gci: sections: - standard diff --git a/docs/templatefuncs.md b/docs/templatefuncs.md index 25a57da..5a6b042 100644 --- a/docs/templatefuncs.md +++ b/docs/templatefuncs.md @@ -10,16 +10,6 @@ [one three] ``` -## `concat` *list*... - -`concat` concatenates *list*s into a new list. - -```text -{{ concat (list 0 1 2) (list "a" "b" "c") }} - -[0 1 2 a b c] -``` - ## `contains` *substring* *string* `contains` returns whether *substring* is in *string*. @@ -49,16 +39,6 @@ true {{ `{ "foo": "bar" }` | fromJSON }} ``` -## `has` *item* *list* - -`has` returns whether *item* is in *list*. - -```text -{{ list 1 2 3 | has 3 }} - -true -``` - ## `hasPrefix` *prefix* *string* `hasPrefix` returns whether *string* begins with *prefix*. @@ -99,17 +79,6 @@ foobar 666f6f626172 ``` -## `indexOf` *item* *list* - -`indexOf` returns the index of *item* in *list*, or -1 if *item* is not -in *list*. - -```text -{{ list "a" "b" "c" | indexOf "b" }} - -1 -``` - ## `join` *delimiter* *list* `join` returns a string containing each item in *list* joined with *delimiter*. @@ -193,27 +162,6 @@ far adcda ``` -## `reverse` *list* - -`reverse` returns a copy of *list* in reverse order. - -```text -{{ list "a" "b" "c" | reverse }} - -[c b a] -``` - -## `sort` *list* - -`sort` returns a copy of *list* sorted in ascending order. -If *list* cannot be sorted, it is simply returned. - -```text -{{ list list "c" "a" "b" | sort }} - -[a b c] -``` - ## `stat` *path* `stat` returns a map representation of executing @@ -272,13 +220,3 @@ FOOBAR foobar ``` - -## `uniq` *list* - -`uniq` returns a new list containing only unique elements in *list*. - -```text -{{ list 1 2 1 3 3 2 1 2 | uniq }} - -[1 2 3] -``` diff --git a/templatefuncs.go b/templatefuncs.go index 2477da9..4b27b64 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -1,7 +1,6 @@ package templatefuncs import ( - "cmp" "encoding/hex" "encoding/json" "errors" @@ -11,7 +10,6 @@ import ( "os/exec" "reflect" "regexp" - "slices" "strconv" "strings" "text/template" @@ -33,16 +31,13 @@ var fileModeTypeNames = map[fs.FileMode]string{ func NewFuncMap() template.FuncMap { return template.FuncMap{ "compact": compactTemplateFunc, - "concat": slices.Concat[[]any], "contains": reverseArgs2(strings.Contains), "eqFold": eqFoldTemplateFunc, "fromJSON": eachByteSliceErr(fromJSONTemplateFunc), - "has": reverseArgs2(slices.Contains[[]any]), "hasPrefix": reverseArgs2(strings.HasPrefix), "hasSuffix": reverseArgs2(strings.HasSuffix), "hexDecode": eachStringErr(hex.DecodeString), "hexEncode": eachByteSlice(hex.EncodeToString), - "indexOf": reverseArgs2(slices.Index[[]any]), "join": reverseArgs2(strings.Join), "list": listTemplateFunc, "lookPath": eachStringErr(lookPathTemplateFunc), @@ -51,22 +46,30 @@ func NewFuncMap() template.FuncMap { "quote": eachString(strconv.Quote), "regexpReplaceAll": regexpReplaceAllTemplateFunc, "replaceAll": replaceAllTemplateFunc, - "reverse": reverseTemplateFunc, - "sort": sortTemplateFunc, "stat": eachString(statTemplateFunc), "toJSON": toJSONTemplateFunc, "toLower": eachString(strings.ToLower), "toString": toStringTemplateFunc, "toUpper": eachString(strings.ToUpper), "trimSpace": eachString(strings.TrimSpace), - "uniq": uniqTemplateFunc, } } // compactTemplateFunc is the core implementation of the `compact` template // function. -func compactTemplateFunc(list []any) []any { - return slices.DeleteFunc(list, isZeroValue) +func compactTemplateFunc(list any) any { + v := reflect.ValueOf(list) + if v.Kind() != reflect.Slice { + panic(fmt.Sprintf("unable to compact argument of type %T", list)) + } + result := reflect.MakeSlice(v.Type(), 0, v.Len()) + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + if !isZeroValue(elem.Interface()) { + result = reflect.Append(result, elem) + } + } + return result.Interface() } // eqFoldTemplateFunc is the core implementation of the `eqFold` template @@ -175,62 +178,6 @@ func regexpReplaceAllTemplateFunc(expr, repl, s string) string { return regexp.MustCompile(expr).ReplaceAllString(s, repl) } -// reverseTemplateFunc is the core implementation of the `reverse` -// template function. -func reverseTemplateFunc(list []any) []any { - listcopy := append([]any(nil), list...) - slices.Reverse(listcopy) - return listcopy -} - -// sortTemplateFunc is the core implementation of the `sort` template function. -func sortTemplateFunc(list []any) any { - if len(list) < 2 { - return list - } - - firstElemType := reflect.TypeOf(list[0]) - - for _, elem := range list[1:] { - if reflect.TypeOf(elem) != firstElemType { - return list - } - } - - switch firstElemType.Kind() { //nolint:exhaustive - case reflect.Int: - return convertAndSortSlice[int](list) - case reflect.Int8: - return convertAndSortSlice[int8](list) - case reflect.Int16: - return convertAndSortSlice[int16](list) - case reflect.Int32: - return convertAndSortSlice[int32](list) - case reflect.Int64: - return convertAndSortSlice[int64](list) - case reflect.Uint: - return convertAndSortSlice[uint](list) - case reflect.Uint8: - return convertAndSortSlice[uint8](list) - case reflect.Uint16: - return convertAndSortSlice[uint16](list) - case reflect.Uint32: - return convertAndSortSlice[uint32](list) - case reflect.Uint64: - return convertAndSortSlice[uint64](list) - case reflect.Uintptr: - return convertAndSortSlice[uintptr](list) - case reflect.Float32: - return convertAndSortSlice[float32](list) - case reflect.Float64: - return convertAndSortSlice[float64](list) - case reflect.String: - return convertAndSortSlice[string](list) - default: - return list - } -} - // statTemplateFunc is the core implementation of the `stat` template function. func statTemplateFunc(name string) any { switch fileInfo, err := os.Stat(name); { @@ -279,35 +226,6 @@ func toStringTemplateFunc(arg any) string { } } -// uniqTemplateFunc is the core implementation of the `uniq` template function. -func uniqTemplateFunc(list []any) []any { - seen := make(map[any]struct{}) - result := []any{} - - for _, v := range list { - if _, ok := seen[v]; !ok { - result = append(result, v) - seen[v] = struct{}{} - } - } - - return result -} - -// convertAndSortSlice creates a `[]T` copy of its input and sorts it. -func convertAndSortSlice[T cmp.Ordered](slice []any) []T { - l := make([]T, len(slice)) - for i, elem := range slice { - v, ok := elem.(T) - if !ok { - panic(fmt.Sprintf("unable to convert %v (type %T) to %T", elem, elem, v)) - } - l[i] = v - } - slices.Sort(l) - return l -} - // eachByteSlice transforms a function that takes a single `[]byte` and returns // a `T` to a function that takes zero or more `[]byte`-like arguments and // returns zero or more `T`s. @@ -483,31 +401,9 @@ func fileInfoToMap(fileInfo fs.FileInfo) map[string]any { func isZeroValue(v any) bool { truth, ok := template.IsTrue(v) if !ok { - panic(fmt.Sprintf("unable to determine zero value for %v", v)) + panic(fmt.Sprintf("unable to determine zero value for %v (type %T)", v, v)) } return !truth - // vval := reflect.ValueOf(v) - // if !vval.IsValid() { - // return true - // } - // switch vval.Kind() { //nolint:exhaustive - // case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - // return vval.Len() == 0 - // case reflect.Bool: - // return !vval.Bool() - // case reflect.Complex64, reflect.Complex128: - // return vval.Complex() == 0 - // case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - // return vval.Int() == 0 - // case reflect.Float32, reflect.Float64: - // return vval.Float() == 0 - // case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - // return vval.Uint() == 0 - // case reflect.Struct: - // return false - // default: - // return vval.IsNil() - // } } // reverseArgs2 transforms a function that takes two arguments and returns an diff --git a/templatefuncs_test.go b/templatefuncs_test.go index d4093ee..4b24957 100644 --- a/templatefuncs_test.go +++ b/templatefuncs_test.go @@ -9,6 +9,14 @@ import ( "github.com/alecthomas/assert/v2" ) +var ( + strSlice = []string{"", "a", "b", "c"} + intSlice = []int{0, 1, 2, 3} + mixedSlice = []any{map[string]any{}, "a", 1, []string{}, 7.7} + emptySlice = []any{} + zeroValueSlice = []int{0, 0, 0, 0} +) + func TestEachString(t *testing.T) { for i, tc := range []struct { f func(string) string @@ -51,12 +59,29 @@ func TestFuncMap(t *testing.T) { expected string }{ { - template: `{{ list "one" "" list "three" | compact }}`, - expected: `[one three]`, + template: `{{ . | compact }}`, + data: strSlice, + expected: `[a b c]`, + }, + { + template: `{{ . | compact }}`, + data: intSlice, + expected: `[1 2 3]`, + }, + { + template: `{{ . | compact }}`, + data: mixedSlice, + expected: `[a 1 7.7]`, }, { - template: `{{ concat (list 0 1 2) (list "a" "b" "c") }}`, - expected: "[0 1 2 a b c]", + template: `{{ . | compact }}`, + data: emptySlice, + expected: "[]", + }, + { + template: `{{ . | compact }}`, + data: zeroValueSlice, + expected: "[]", }, { template: `{{ "abc" | contains "bc" }}`, @@ -82,14 +107,6 @@ func TestFuncMap(t *testing.T) { template: `{{ fromJSON "0" }}`, expected: "0", }, - { - template: `{{ list 1 2 3 | has 3 }}`, - expected: "true", - }, - { - template: `{{ has 3 (list 1 2 3) }}`, - expected: "true", - }, { template: `{{ "ab" | hasPrefix "a" }}`, expected: "true", @@ -106,10 +123,6 @@ func TestFuncMap(t *testing.T) { template: `{{ "ab" | hasSuffix "b" }}`, expected: "true", }, - { - template: `{{ list "a" "b" "c" | indexOf "b" }}`, - expected: "1", - }, { template: `{{ list "a" "b" "c" | quote | join "," }}`, expected: `"a","b","c"`, @@ -149,18 +162,6 @@ func TestFuncMap(t *testing.T) { template: `{{ list "abc" "cba" | replaceAll "b" "d" }}`, expected: "[adc cda]", }, - { - template: `{{ list "a" "b" "c" | reverse }}`, - expected: "[c b a]", - }, - { - template: `{{ list "c" "a" "b" | sort }}`, - expected: "[a b c]", - }, - { - template: `{{ list 0 4 5 1 | sort }}`, - expected: "[0 1 4 5]", - }, { template: `{{ (stat "testdata/file").type }}`, expected: "file", @@ -173,10 +174,6 @@ func TestFuncMap(t *testing.T) { template: `{{ trimSpace " a " }}`, expected: "a", }, - { - template: `{{ list 1 2 1 3 3 2 1 2 | uniq }}`, - expected: "[1 2 3]", - }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { tmpl, err := template.New("").Funcs(funcMap).Parse(tc.template) From ccbc3a50cf815f697c1161e736887726927c0704 Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Mon, 20 May 2024 21:55:38 +0100 Subject: [PATCH 9/9] new reverse --- docs/templatefuncs.md | 10 ++++++++ templatefuncs.go | 18 ++++++++++++++ templatefuncs_test.go | 55 ++++++++++++++++++++++++++++--------------- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/docs/templatefuncs.md b/docs/templatefuncs.md index 5a6b042..fa50538 100644 --- a/docs/templatefuncs.md +++ b/docs/templatefuncs.md @@ -162,6 +162,16 @@ far adcda ``` +## `reverse` *list* + +`reverse` returns a copy of *list* with its elements in reverse order. + +```text +{{ list 1 2 3 | reverse }} + +[3 2 1] +``` + ## `stat` *path* `stat` returns a map representation of executing diff --git a/templatefuncs.go b/templatefuncs.go index 4b27b64..f47f346 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -46,6 +46,7 @@ func NewFuncMap() template.FuncMap { "quote": eachString(strconv.Quote), "regexpReplaceAll": regexpReplaceAllTemplateFunc, "replaceAll": replaceAllTemplateFunc, + "reverse": reverseTemplateFunc, "stat": eachString(statTemplateFunc), "toJSON": toJSONTemplateFunc, "toLower": eachString(strings.ToLower), @@ -178,6 +179,23 @@ func regexpReplaceAllTemplateFunc(expr, repl, s string) string { return regexp.MustCompile(expr).ReplaceAllString(s, repl) } +// reverseTemplateFunc is the core implementation of the `reverse` +// template function. +func reverseTemplateFunc(list any) any { + v := reflect.ValueOf(list) + if v.Kind() != reflect.Slice { + panic(fmt.Sprintf("unable to reverse argument of type %T", list)) + } + if v.Len() <= 1 { + return list + } + result := reflect.MakeSlice(v.Type(), 0, v.Len()) + for i := v.Len() - 1; i >= 0; i-- { + result = reflect.Append(result, v.Index(i)) + } + return result.Interface() +} + // statTemplateFunc is the core implementation of the `stat` template function. func statTemplateFunc(name string) any { switch fileInfo, err := os.Stat(name); { diff --git a/templatefuncs_test.go b/templatefuncs_test.go index 4b24957..4a148b1 100644 --- a/templatefuncs_test.go +++ b/templatefuncs_test.go @@ -9,14 +9,6 @@ import ( "github.com/alecthomas/assert/v2" ) -var ( - strSlice = []string{"", "a", "b", "c"} - intSlice = []int{0, 1, 2, 3} - mixedSlice = []any{map[string]any{}, "a", 1, []string{}, 7.7} - emptySlice = []any{} - zeroValueSlice = []int{0, 0, 0, 0} -) - func TestEachString(t *testing.T) { for i, tc := range []struct { f func(string) string @@ -59,28 +51,37 @@ func TestFuncMap(t *testing.T) { expected string }{ { - template: `{{ . | compact }}`, - data: strSlice, - expected: `[a b c]`, + template: `{{ . | compact | printf "%[1]v\n%[1]T\n" }}`, + data: []string{"", "a", "", "b", "c"}, + expected: joinLines( + "[a b c]", + "[]string", + ), }, { - template: `{{ . | compact }}`, - data: intSlice, - expected: `[1 2 3]`, + template: `{{ . | compact | printf "%[1]v\n%[1]T\n" }}`, + data: []int{0, 1, 0, 2, 3}, + expected: joinLines( + "[1 2 3]", + "[]int", + ), }, { - template: `{{ . | compact }}`, - data: mixedSlice, - expected: `[a 1 7.7]`, + template: `{{ . | compact | printf "%[1]v\n%[1]T\n" }}`, + data: []any{map[string]any{}, "a", 1, []string{}, 7.7}, + expected: joinLines( + "[a 1 7.7]", + "[]interface {}", + ), }, { template: `{{ . | compact }}`, - data: emptySlice, + data: []any{}, expected: "[]", }, { template: `{{ . | compact }}`, - data: zeroValueSlice, + data: []int{0, 0, 0, 0}, expected: "[]", }, { @@ -162,6 +163,22 @@ func TestFuncMap(t *testing.T) { template: `{{ list "abc" "cba" | replaceAll "b" "d" }}`, expected: "[adc cda]", }, + { + template: `{{ . | reverse | printf "%[1]v\n%[1]T\n" }}`, + data: []string{"a", "b", "c"}, + expected: joinLines( + "[c b a]", + "[]string", + ), + }, + { + template: `{{ . | reverse | printf "%[1]v\n%[1]T\n" }}`, + data: []int{1, 2, 3}, + expected: joinLines( + "[3 2 1]", + "[]int", + ), + }, { template: `{{ (stat "testdata/file").type }}`, expected: "file",