diff --git a/internal/lang/funcs/descriptions.go b/internal/lang/funcs/descriptions.go index 7d606f2e60ea..8c49f45f4f3d 100644 --- a/internal/lang/funcs/descriptions.go +++ b/internal/lang/funcs/descriptions.go @@ -391,6 +391,10 @@ var DescriptionList = map[string]descriptionEntry{ Description: "`startswith` takes two values: a string to check and a prefix string. The function returns true if the string begins with that exact prefix.", ParamDescription: []string{"", ""}, }, + "strcontains": { + Description: "`strcontains` takes two values: a string to check and an expected substring. The function returns true if the string has the substring contained within it.", + ParamDescription: []string{"", ""}, + }, "strrev": { Description: "`strrev` reverses the characters in a string. Note that the characters are treated as _Unicode characters_ (in technical terms, Unicode [grapheme cluster boundaries](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) are respected).", ParamDescription: []string{""}, diff --git a/internal/lang/funcs/string.go b/internal/lang/funcs/string.go index 9ef709c7fb09..3cc78952dce6 100644 --- a/internal/lang/funcs/string.go +++ b/internal/lang/funcs/string.go @@ -103,3 +103,29 @@ var ReplaceFunc = function.New(&function.Spec{ func Replace(str, substr, replace cty.Value) (cty.Value, error) { return ReplaceFunc.Call([]cty.Value{str, substr, replace}) } + +// StrContainsFunc searches a given string for another given substring, +// if found the function returns true, otherwise returns false. +var StrContainsFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + { + Name: "substr", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + str := args[0].AsString() + substr := args[1].AsString() + + if strings.Contains(str, substr) { + return cty.True, nil + } + + return cty.False, nil + }, +}) diff --git a/internal/lang/funcs/string_test.go b/internal/lang/funcs/string_test.go index 7b44a2762402..71df343634b3 100644 --- a/internal/lang/funcs/string_test.go +++ b/internal/lang/funcs/string_test.go @@ -71,3 +71,66 @@ func TestReplace(t *testing.T) { }) } } + +func TestStrContains(t *testing.T) { + tests := []struct { + String cty.Value + Substr cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("hello"), + cty.StringVal("hel"), + cty.BoolVal(true), + false, + }, + { + cty.StringVal("hello"), + cty.StringVal("lo"), + cty.BoolVal(true), + false, + }, + { + cty.StringVal("hello1"), + cty.StringVal("1"), + cty.BoolVal(true), + false, + }, + { + cty.StringVal("hello1"), + cty.StringVal("heo"), + cty.BoolVal(false), + false, + }, + { + cty.StringVal("hello1"), + cty.NumberIntVal(1), + cty.UnknownVal(cty.Bool), + true, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("includes(%#v, %#v)", test.String, test.Substr), func(t *testing.T) { + got, err := StrContains(test.String, test.Substr) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + 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) + } + }) + } +} + +func StrContains(str, substr cty.Value) (cty.Value, error) { + return StrContainsFunc.Call([]cty.Value{str, substr}) +} diff --git a/internal/lang/functions.go b/internal/lang/functions.go index b024e5516481..f27ece88be52 100644 --- a/internal/lang/functions.go +++ b/internal/lang/functions.go @@ -117,6 +117,7 @@ func (s *Scope) Functions() map[string]function.Function { "sort": stdlib.SortFunc, "split": stdlib.SplitFunc, "startswith": funcs.StartsWithFunc, + "strcontains": funcs.StrContainsFunc, "strrev": stdlib.ReverseFunc, "substr": stdlib.SubstrFunc, "sum": funcs.SumFunc, diff --git a/internal/lang/functions_test.go b/internal/lang/functions_test.go index 32a9762d3998..ce1d9586d452 100644 --- a/internal/lang/functions_test.go +++ b/internal/lang/functions_test.go @@ -906,6 +906,17 @@ func TestFunctions(t *testing.T) { }, }, + "strcontains": { + { + `strcontains("hello", "llo")`, + cty.BoolVal(true), + }, + { + `strcontains("hello", "a")`, + cty.BoolVal(false), + }, + }, + "strrev": { { `strrev("hello world")`, diff --git a/website/data/language-nav-data.json b/website/data/language-nav-data.json index e489f8e71cd4..2b8226b133d6 100644 --- a/website/data/language-nav-data.json +++ b/website/data/language-nav-data.json @@ -337,6 +337,10 @@ "title": "startswith", "href": "/language/functions/startswith" }, + { + "title": "strcontains", + "href": "/language/functions/strcontains" + }, { "title": "strrev", "href": "/language/functions/strrev" diff --git a/website/docs/language/functions/strcontains.mdx b/website/docs/language/functions/strcontains.mdx new file mode 100644 index 000000000000..6e684a6f6aa1 --- /dev/null +++ b/website/docs/language/functions/strcontains.mdx @@ -0,0 +1,25 @@ +--- +page_title: strcontains - Functions - Configuration Language +description: |- + The strcontains function checks whether a given string can be found within another string. +--- + +# `strcontains` Function + +`strcontains` function checks whether a substring is within another string. + +```hcl +strcontains(string, substr) +``` + +## Examples + +``` +> strcontains("hello world", "wor") +true +``` + +``` +> strcontains("hello world", "wod") +false +``` \ No newline at end of file diff --git a/website/layouts/language.erb b/website/layouts/language.erb index 4aa951904a21..c93f429f492f 100644 --- a/website/layouts/language.erb +++ b/website/layouts/language.erb @@ -418,6 +418,10 @@ startswith +
  • + strcontains +
  • +
  • strrev