github.com/opentofu/opentofu@v1.7.1/internal/lang/funcs/string.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package funcs 7 8 import ( 9 "regexp" 10 "strings" 11 12 "github.com/hashicorp/hcl/v2" 13 "github.com/hashicorp/hcl/v2/hclsyntax" 14 "github.com/zclconf/go-cty/cty" 15 "github.com/zclconf/go-cty/cty/function" 16 ) 17 18 // StartsWithFunc constructs a function that checks if a string starts with 19 // a specific prefix using strings.HasPrefix 20 var StartsWithFunc = function.New(&function.Spec{ 21 Params: []function.Parameter{ 22 { 23 Name: "str", 24 Type: cty.String, 25 AllowUnknown: true, 26 }, 27 { 28 Name: "prefix", 29 Type: cty.String, 30 }, 31 }, 32 Type: function.StaticReturnType(cty.Bool), 33 RefineResult: refineNotNull, 34 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 35 prefix := args[1].AsString() 36 37 if !args[0].IsKnown() { 38 // If the unknown value has a known prefix then we might be 39 // able to still produce a known result. 40 if prefix == "" { 41 // The empty string is a prefix of any string. 42 return cty.True, nil 43 } 44 if knownPrefix := args[0].Range().StringPrefix(); knownPrefix != "" { 45 if strings.HasPrefix(knownPrefix, prefix) { 46 return cty.True, nil 47 } 48 if len(knownPrefix) >= len(prefix) { 49 // If the prefix we're testing is no longer than the known 50 // prefix and it didn't match then the full string with 51 // that same prefix can't match either. 52 return cty.False, nil 53 } 54 } 55 return cty.UnknownVal(cty.Bool), nil 56 } 57 58 str := args[0].AsString() 59 60 if strings.HasPrefix(str, prefix) { 61 return cty.True, nil 62 } 63 64 return cty.False, nil 65 }, 66 }) 67 68 // EndsWithFunc constructs a function that checks if a string ends with 69 // a specific suffix using strings.HasSuffix 70 var EndsWithFunc = function.New(&function.Spec{ 71 Params: []function.Parameter{ 72 { 73 Name: "str", 74 Type: cty.String, 75 }, 76 { 77 Name: "suffix", 78 Type: cty.String, 79 }, 80 }, 81 Type: function.StaticReturnType(cty.Bool), 82 RefineResult: refineNotNull, 83 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 84 str := args[0].AsString() 85 suffix := args[1].AsString() 86 87 if strings.HasSuffix(str, suffix) { 88 return cty.True, nil 89 } 90 91 return cty.False, nil 92 }, 93 }) 94 95 // ReplaceFunc constructs a function that searches a given string for another 96 // given substring, and replaces each occurence with a given replacement string. 97 var ReplaceFunc = function.New(&function.Spec{ 98 Params: []function.Parameter{ 99 { 100 Name: "str", 101 Type: cty.String, 102 }, 103 { 104 Name: "substr", 105 Type: cty.String, 106 }, 107 { 108 Name: "replace", 109 Type: cty.String, 110 }, 111 }, 112 Type: function.StaticReturnType(cty.String), 113 RefineResult: refineNotNull, 114 Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 115 str := args[0].AsString() 116 substr := args[1].AsString() 117 replace := args[2].AsString() 118 119 // We search/replace using a regexp if the string is surrounded 120 // in forward slashes. 121 if len(substr) > 1 && substr[0] == '/' && substr[len(substr)-1] == '/' { 122 re, err := regexp.Compile(substr[1 : len(substr)-1]) 123 if err != nil { 124 return cty.UnknownVal(cty.String), err 125 } 126 127 return cty.StringVal(re.ReplaceAllString(str, replace)), nil 128 } 129 130 return cty.StringVal(strings.Replace(str, substr, replace, -1)), nil 131 }, 132 }) 133 134 // StrContainsFunc searches a given string for another given substring, 135 // if found the function returns true, otherwise returns false. 136 var StrContainsFunc = function.New(&function.Spec{ 137 Params: []function.Parameter{ 138 { 139 Name: "str", 140 Type: cty.String, 141 }, 142 { 143 Name: "substr", 144 Type: cty.String, 145 }, 146 }, 147 Type: function.StaticReturnType(cty.Bool), 148 Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 149 str := args[0].AsString() 150 substr := args[1].AsString() 151 152 if strings.Contains(str, substr) { 153 return cty.True, nil 154 } 155 156 return cty.False, nil 157 }, 158 }) 159 160 // Replace searches a given string for another given substring, 161 // and replaces all occurences with a given replacement string. 162 func Replace(str, substr, replace cty.Value) (cty.Value, error) { 163 return ReplaceFunc.Call([]cty.Value{str, substr, replace}) 164 } 165 166 func StrContains(str, substr cty.Value) (cty.Value, error) { 167 return StrContainsFunc.Call([]cty.Value{str, substr}) 168 } 169 170 // This constant provides a placeholder value for filename indicating 171 // that no file is needed for templatestring. 172 const ( 173 templateStringFilename = "NoFileNeeded" 174 ) 175 176 // MakeTemplateStringFunc constructs a function that takes a string and 177 // an arbitrary object of named values and attempts to render that string 178 // as a template using HCL template syntax. 179 func MakeTemplateStringFunc(content string, funcsCb func() map[string]function.Function) function.Function { 180 181 params := []function.Parameter{ 182 { 183 Name: "data", 184 Type: cty.String, 185 AllowMarked: true, 186 }, 187 { 188 Name: "vars", 189 Type: cty.DynamicPseudoType, 190 AllowMarked: true, 191 }, 192 } 193 loadTmpl := func(content string, marks cty.ValueMarks) (hcl.Expression, error) { 194 195 expr, diags := hclsyntax.ParseTemplate([]byte(content), templateStringFilename, hcl.Pos{Line: 1, Column: 1}) 196 if diags.HasErrors() { 197 return nil, diags 198 } 199 200 return expr, nil 201 } 202 203 return function.New(&function.Spec{ 204 Params: params, 205 Type: func(args []cty.Value) (cty.Type, error) { 206 if !(args[0].IsKnown() && args[1].IsKnown()) { 207 return cty.DynamicPseudoType, nil 208 } 209 210 // We'll render our template now to see what result type it produces. 211 // A template consisting only of a single interpolation can potentially 212 // return any type. 213 dataArg, dataMarks := args[0].Unmark() 214 expr, err := loadTmpl(dataArg.AsString(), dataMarks) 215 if err != nil { 216 return cty.DynamicPseudoType, err 217 } 218 219 // This is safe even if args[1] contains unknowns because the HCL 220 // template renderer itself knows how to short-circuit those. 221 val, err := renderTemplate(expr, args[1], funcsCb()) 222 return val.Type(), err 223 }, 224 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 225 dataArg, dataMarks := args[0].Unmark() 226 expr, err := loadTmpl(dataArg.AsString(), dataMarks) 227 if err != nil { 228 return cty.DynamicVal, err 229 } 230 result, err := renderTemplate(expr, args[1], funcsCb()) 231 return result.WithMarks(dataMarks), err 232 }, 233 }) 234 }