Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add more list funcs #29

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/templatefuncs.md
Original file line number Diff line number Diff line change
@@ -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*.
Expand Down Expand Up @@ -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*.
Expand Down Expand Up @@ -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*.
Expand Down Expand Up @@ -152,6 +183,27 @@ 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 *list* sorted in ascending order.
If *list* cannot be sorted, it is simply returned.

```text
{{ list list "c" "a" "b" | sort }}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: duplicated list.


[a b c]
```

## `stat` *path*

`stat` returns a map representation of executing
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/chezmoi/templatefuncs

go 1.19
go 1.21

require github.com/alecthomas/assert/v2 v2.9.0

Expand Down
67 changes: 67 additions & 0 deletions templatefuncs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"io/fs"
"os"
"os/exec"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"text/template"
Expand All @@ -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]),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as for concat.

"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),
Expand All @@ -44,6 +49,8 @@ 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),
Expand All @@ -53,6 +60,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 {
Expand Down Expand Up @@ -159,6 +172,33 @@ 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 {
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); {
Expand Down Expand Up @@ -377,6 +417,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()
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be replaced with something like

truth, ok := template.IsTrue(v)
if !ok {
	panic(fmt.Sprintf("unable to determine zero value for %v", v))
}
return !truth

?

// 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`.
Expand Down
33 changes: 30 additions & 3 deletions templatefuncs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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"`,
Expand Down Expand Up @@ -118,6 +133,10 @@ func TestFuncMap(t *testing.T) {
"# b",
),
},
{
template: `{{ quote "a" }}`,
expected: `"a"`,
},
{
template: `{{ "abcba" | replaceAll "b" "d" }}`,
expected: `adcda`,
Expand All @@ -127,8 +146,16 @@ func TestFuncMap(t *testing.T) {
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 }}`,
Expand Down
Loading