mirror of https://github.com/hashicorp/packer
This commit adds 3 new HCL2 functions: * `sum`: computes the sum of a collection of numerical values * `startswith`: checks if a string has another as prefix * `endswith`: checks if a string has another as suffixpull/13355/head
parent
1d02f14871
commit
51eeadba3d
@ -0,0 +1,31 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
// EndsWithFunc constructs a function that checks if a string ends with
|
||||
// a specific suffix using strings.HasSuffix
|
||||
var EndsWithFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "str",
|
||||
Type: cty.String,
|
||||
},
|
||||
{
|
||||
Name: "suffix",
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
Type: function.StaticReturnType(cty.Bool),
|
||||
RefineResult: refineNotNull,
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
str := args[0].AsString()
|
||||
suffix := args[1].AsString()
|
||||
|
||||
return cty.BoolVal(strings.HasSuffix(str, suffix)), nil
|
||||
},
|
||||
})
|
||||
@ -0,0 +1,83 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestEndsWith(t *testing.T) {
|
||||
tests := []struct {
|
||||
String, Suffix cty.Value
|
||||
Want cty.Value
|
||||
}{
|
||||
{
|
||||
cty.StringVal("hello world"),
|
||||
cty.StringVal("world"),
|
||||
cty.True,
|
||||
},
|
||||
{
|
||||
cty.StringVal("hey world"),
|
||||
cty.StringVal("worldss"),
|
||||
cty.False,
|
||||
},
|
||||
{
|
||||
cty.StringVal(""),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
},
|
||||
{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
},
|
||||
{
|
||||
cty.StringVal("hello world"),
|
||||
cty.StringVal(" "),
|
||||
cty.False,
|
||||
},
|
||||
{
|
||||
cty.StringVal(" "),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
},
|
||||
{
|
||||
cty.StringVal(" "),
|
||||
cty.StringVal("hello"),
|
||||
cty.False,
|
||||
},
|
||||
{
|
||||
cty.StringVal(""),
|
||||
cty.StringVal("a"),
|
||||
cty.False,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.StringVal("a"),
|
||||
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.StringVal(""),
|
||||
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("EndsWith(%#v, %#v)", test.String, test.Suffix), func(t *testing.T) {
|
||||
got, err := EndsWithFunc.Call([]cty.Value{test.String, test.Suffix})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if !got.RawEquals(test.Want) {
|
||||
t.Errorf(
|
||||
"wrong result\nstring: %#v\nsuffix: %#v\ngot: %#v\nwant: %#v",
|
||||
test.String, test.Suffix, got, test.Want,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
// StartsWithFunc constructs a function that checks if a string starts with
|
||||
// a specific prefix using strings.HasPrefix
|
||||
var StartsWithFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "str",
|
||||
Type: cty.String,
|
||||
AllowUnknown: false,
|
||||
},
|
||||
{
|
||||
Name: "prefix",
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
Type: function.StaticReturnType(cty.Bool),
|
||||
RefineResult: refineNotNull,
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
str := args[0].AsString()
|
||||
prefix := args[1].AsString()
|
||||
|
||||
return cty.BoolVal(strings.HasPrefix(str, prefix)), nil
|
||||
},
|
||||
})
|
||||
@ -0,0 +1,98 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestStartsWith(t *testing.T) {
|
||||
tests := []struct {
|
||||
String, Prefix cty.Value
|
||||
Want cty.Value
|
||||
}{
|
||||
{
|
||||
cty.StringVal("hello world"),
|
||||
cty.StringVal("hello"),
|
||||
cty.True,
|
||||
},
|
||||
{
|
||||
cty.StringVal("hey world"),
|
||||
cty.StringVal("hello"),
|
||||
cty.False,
|
||||
},
|
||||
{
|
||||
cty.StringVal(""),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
},
|
||||
{
|
||||
cty.StringVal(""),
|
||||
cty.StringVal(" "),
|
||||
cty.False,
|
||||
},
|
||||
{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
},
|
||||
{
|
||||
cty.StringVal(""),
|
||||
cty.StringVal("a"),
|
||||
cty.False,
|
||||
},
|
||||
|
||||
{
|
||||
// Unicode combining characters edge-case: we match the prefix
|
||||
// in terms of unicode code units rather than grapheme clusters,
|
||||
// which is inconsistent with our string processing elsewhere but
|
||||
// would be a breaking change to fix that bug now.
|
||||
cty.StringVal("\U0001f937\u200d\u2642"), // "Man Shrugging" is encoded as "Person Shrugging" followed by zero-width joiner and then the masculine gender presentation modifier
|
||||
cty.StringVal("\U0001f937"), // Just the "Person Shrugging" character without any modifiers
|
||||
cty.True,
|
||||
},
|
||||
{
|
||||
cty.StringVal("hello world"),
|
||||
cty.StringVal(" "),
|
||||
cty.False,
|
||||
},
|
||||
{
|
||||
cty.StringVal(" "),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
},
|
||||
{
|
||||
cty.StringVal(" "),
|
||||
cty.StringVal("hello"),
|
||||
cty.False,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.StringVal("a"),
|
||||
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.StringVal(""),
|
||||
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("StartsWith(%#v, %#v)", test.String, test.Prefix), func(t *testing.T) {
|
||||
got, err := StartsWithFunc.Call([]cty.Value{test.String, test.Prefix})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if !got.RawEquals(test.Want) {
|
||||
t.Errorf(
|
||||
"wrong result\nstring: %#v\nprefix: %#v\ngot: %#v\nwant: %#v",
|
||||
test.String, test.Prefix, got, test.Want,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
var SumFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "list",
|
||||
Type: cty.DynamicPseudoType,
|
||||
},
|
||||
},
|
||||
Type: function.StaticReturnType(cty.Number),
|
||||
RefineResult: refineNotNull,
|
||||
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
||||
|
||||
if !args[0].CanIterateElements() {
|
||||
return cty.NilVal, function.NewArgErrorf(0, "cannot sum noniterable")
|
||||
}
|
||||
|
||||
if args[0].LengthInt() == 0 { // Easy path
|
||||
return cty.NilVal, function.NewArgErrorf(0, "cannot sum an empty list")
|
||||
}
|
||||
|
||||
arg := args[0].AsValueSlice()
|
||||
ty := args[0].Type()
|
||||
|
||||
if !ty.IsListType() && !ty.IsSetType() && !ty.IsTupleType() {
|
||||
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple. Received %s", ty.FriendlyName())
|
||||
}
|
||||
|
||||
if !args[0].IsWhollyKnown() {
|
||||
return cty.UnknownVal(cty.Number), nil
|
||||
}
|
||||
|
||||
// big.Float.Add can panic if the input values are opposing infinities,
|
||||
// so we must catch that here in order to remain within
|
||||
// the cty Function abstraction.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(big.ErrNaN); ok {
|
||||
ret = cty.NilVal
|
||||
err = fmt.Errorf("can't compute sum of opposing infinities")
|
||||
} else {
|
||||
// not a panic we recognize
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s := arg[0]
|
||||
if s.IsNull() {
|
||||
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
|
||||
}
|
||||
s, err = convert.Convert(s, cty.Number)
|
||||
if err != nil {
|
||||
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
|
||||
}
|
||||
for _, v := range arg[1:] {
|
||||
if v.IsNull() {
|
||||
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
|
||||
}
|
||||
v, err = convert.Convert(v, cty.Number)
|
||||
if err != nil {
|
||||
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
|
||||
}
|
||||
s = s.Add(v)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
},
|
||||
})
|
||||
|
||||
// Sum adds numbers in a list, set, or tuple
|
||||
func Sum(list cty.Value) (cty.Value, error) {
|
||||
return SumFunc.Call([]cty.Value{list})
|
||||
}
|
||||
@ -0,0 +1,226 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestSum(t *testing.T) {
|
||||
tests := []struct {
|
||||
List cty.Value
|
||||
Want cty.Value
|
||||
Err string
|
||||
}{
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
cty.NumberIntVal(2),
|
||||
cty.NumberIntVal(3),
|
||||
}),
|
||||
cty.NumberIntVal(6),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberIntVal(1476),
|
||||
cty.NumberIntVal(2093),
|
||||
cty.NumberIntVal(2092495),
|
||||
cty.NumberIntVal(64589234),
|
||||
cty.NumberIntVal(234),
|
||||
}),
|
||||
cty.NumberIntVal(66685532),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal("b"),
|
||||
cty.StringVal("c"),
|
||||
}),
|
||||
cty.UnknownVal(cty.String),
|
||||
"argument must be list, set, or tuple of number values",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberIntVal(10),
|
||||
cty.NumberIntVal(-19),
|
||||
cty.NumberIntVal(5),
|
||||
}),
|
||||
cty.NumberIntVal(-4),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberFloatVal(10.2),
|
||||
cty.NumberFloatVal(19.4),
|
||||
cty.NumberFloatVal(5.7),
|
||||
}),
|
||||
cty.NumberFloatVal(35.3),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberFloatVal(-10.2),
|
||||
cty.NumberFloatVal(-19.4),
|
||||
cty.NumberFloatVal(-5.7),
|
||||
}),
|
||||
cty.NumberFloatVal(-35.3),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{cty.NullVal(cty.Number)}),
|
||||
cty.NilVal,
|
||||
"argument must be list, set, or tuple of number values",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberIntVal(5),
|
||||
cty.NullVal(cty.Number),
|
||||
}),
|
||||
cty.NilVal,
|
||||
"argument must be list, set, or tuple of number values",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal("b"),
|
||||
cty.StringVal("c"),
|
||||
}),
|
||||
cty.UnknownVal(cty.String).RefineNotNull(),
|
||||
"argument must be list, set, or tuple of number values",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.NumberIntVal(10),
|
||||
cty.NumberIntVal(-19),
|
||||
cty.NumberIntVal(5),
|
||||
}),
|
||||
cty.NumberIntVal(-4),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.NumberIntVal(10),
|
||||
cty.NumberIntVal(25),
|
||||
cty.NumberIntVal(30),
|
||||
}),
|
||||
cty.NumberIntVal(65),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.NumberFloatVal(2340.8),
|
||||
cty.NumberFloatVal(10.2),
|
||||
cty.NumberFloatVal(3),
|
||||
}),
|
||||
cty.NumberFloatVal(2354),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.NumberFloatVal(2),
|
||||
}),
|
||||
cty.NumberFloatVal(2),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.NumberFloatVal(-2),
|
||||
cty.NumberFloatVal(-50),
|
||||
cty.NumberFloatVal(-20),
|
||||
cty.NumberFloatVal(-123),
|
||||
cty.NumberFloatVal(-4),
|
||||
}),
|
||||
cty.NumberFloatVal(-199),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.NumberIntVal(12),
|
||||
cty.StringVal("a"),
|
||||
cty.NumberIntVal(38),
|
||||
}),
|
||||
cty.UnknownVal(cty.String).RefineNotNull(),
|
||||
"argument must be list, set, or tuple of number values",
|
||||
},
|
||||
{
|
||||
cty.NumberIntVal(12),
|
||||
cty.NilVal,
|
||||
"cannot sum noniterable",
|
||||
},
|
||||
{
|
||||
cty.ListValEmpty(cty.Number),
|
||||
cty.NilVal,
|
||||
"cannot sum an empty list",
|
||||
},
|
||||
{
|
||||
cty.MapVal(map[string]cty.Value{"hello": cty.True}),
|
||||
cty.NilVal,
|
||||
"argument must be list, set, or tuple. Received map of bool",
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.Number),
|
||||
cty.UnknownVal(cty.Number).RefineNotNull(),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.List(cty.Number)),
|
||||
cty.UnknownVal(cty.Number).RefineNotNull(),
|
||||
"",
|
||||
},
|
||||
{ // known list containing unknown values
|
||||
cty.ListVal([]cty.Value{cty.UnknownVal(cty.Number)}),
|
||||
cty.UnknownVal(cty.Number).RefineNotNull(),
|
||||
"",
|
||||
},
|
||||
{ // numbers too large to represent as float64
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.MustParseNumberVal("1e+500"),
|
||||
cty.MustParseNumberVal("1e+500"),
|
||||
}),
|
||||
cty.MustParseNumberVal("2e+500"),
|
||||
"",
|
||||
},
|
||||
{ // edge case we have a special error handler for
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberFloatVal(math.Inf(1)),
|
||||
cty.NumberFloatVal(math.Inf(-1)),
|
||||
}),
|
||||
cty.NilVal,
|
||||
"can't compute sum of opposing infinities",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.StringVal("1"),
|
||||
cty.StringVal("2"),
|
||||
cty.StringVal("3"),
|
||||
}),
|
||||
cty.NumberIntVal(6),
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("sum(%#v)", test.List), func(t *testing.T) {
|
||||
got, err := Sum(test.List)
|
||||
|
||||
if test.Err != "" {
|
||||
if err == nil {
|
||||
t.Fatal("succeeded; want error")
|
||||
} else if got, want := err.Error(), test.Err; got != want {
|
||||
t.Fatalf("wrong error\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if !got.RawEquals(test.Want) {
|
||||
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
---
|
||||
page_title: sum - Functions - Configuration Language
|
||||
description: The sum function takes a list or set of numbers and returns the sum of those numbers.
|
||||
---
|
||||
|
||||
# `sum` Function
|
||||
|
||||
`sum` takes a list or set of numbers and returns the sum of those numbers.
|
||||
|
||||
`sum` fails when given an empty list or set.
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
> sum([10, 13, 6, 4.5])
|
||||
33.5
|
||||
```
|
||||
@ -0,0 +1,27 @@
|
||||
---
|
||||
page_title: endswith - Functions - Configuration Language
|
||||
description: |-
|
||||
The endswith function takes two values: a string to check and a suffix string. It returns true if the first string ends with that exact suffix.
|
||||
---
|
||||
|
||||
# `endswith` Function
|
||||
|
||||
`endswith` takes two values: a string to check and a suffix string. The function returns true if the first string ends with that exact suffix.
|
||||
|
||||
```hcl
|
||||
endswith(string, suffix)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
> endswith("hello world", "world")
|
||||
true
|
||||
> endswith("hello world", "hello")
|
||||
false
|
||||
```
|
||||
|
||||
## Related Functions
|
||||
|
||||
- [`startswith`](/packer/docs/templates/hcl_templates/functions/string/startswith) takes two values: a string to check
|
||||
and a prefix string. The function returns true if the string begins with that exact prefix.
|
||||
@ -0,0 +1,27 @@
|
||||
---
|
||||
page_title: startsswith - Functions - Configuration Language
|
||||
description: |-
|
||||
The startswith function takes two values: a string to check and a prefix string. It returns true if the string begins with that exact prefix.
|
||||
---
|
||||
|
||||
# `startswith` Function
|
||||
|
||||
`startswith` takes two values: a string to check and a prefix string. The function returns true if the string begins with that exact prefix.
|
||||
|
||||
```hcl
|
||||
startswith(string, prefix)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
> startswith("hello world", "hello")
|
||||
true
|
||||
> startswith("hello world", "world")
|
||||
false
|
||||
```
|
||||
|
||||
## Related Functions
|
||||
|
||||
- [`endswith`](/packer/docs/templates/hcl_templates/functions/string/endswith) takes two values: a string to check
|
||||
and a suffix string. The function returns true if the first string ends with that exact suffix.
|
||||
Loading…
Reference in new issue