1package stdlib 2 3import ( 4 "fmt" 5 "regexp" 6 "sort" 7 "strings" 8 9 "github.com/apparentlymart/go-textseg/v13/textseg" 10 11 "github.com/zclconf/go-cty/cty" 12 "github.com/zclconf/go-cty/cty/function" 13 "github.com/zclconf/go-cty/cty/gocty" 14) 15 16var UpperFunc = function.New(&function.Spec{ 17 Params: []function.Parameter{ 18 { 19 Name: "str", 20 Type: cty.String, 21 AllowDynamicType: true, 22 }, 23 }, 24 Type: function.StaticReturnType(cty.String), 25 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 26 in := args[0].AsString() 27 out := strings.ToUpper(in) 28 return cty.StringVal(out), nil 29 }, 30}) 31 32var LowerFunc = function.New(&function.Spec{ 33 Params: []function.Parameter{ 34 { 35 Name: "str", 36 Type: cty.String, 37 AllowDynamicType: true, 38 }, 39 }, 40 Type: function.StaticReturnType(cty.String), 41 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 42 in := args[0].AsString() 43 out := strings.ToLower(in) 44 return cty.StringVal(out), nil 45 }, 46}) 47 48var ReverseFunc = function.New(&function.Spec{ 49 Params: []function.Parameter{ 50 { 51 Name: "str", 52 Type: cty.String, 53 AllowDynamicType: true, 54 }, 55 }, 56 Type: function.StaticReturnType(cty.String), 57 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 58 in := []byte(args[0].AsString()) 59 out := make([]byte, len(in)) 60 pos := len(out) 61 62 inB := []byte(in) 63 for i := 0; i < len(in); { 64 d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true) 65 cluster := in[i : i+d] 66 pos -= len(cluster) 67 copy(out[pos:], cluster) 68 i += d 69 } 70 71 return cty.StringVal(string(out)), nil 72 }, 73}) 74 75var StrlenFunc = function.New(&function.Spec{ 76 Params: []function.Parameter{ 77 { 78 Name: "str", 79 Type: cty.String, 80 AllowDynamicType: true, 81 }, 82 }, 83 Type: function.StaticReturnType(cty.Number), 84 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 85 in := args[0].AsString() 86 l := 0 87 88 inB := []byte(in) 89 for i := 0; i < len(in); { 90 d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true) 91 l++ 92 i += d 93 } 94 95 return cty.NumberIntVal(int64(l)), nil 96 }, 97}) 98 99var SubstrFunc = function.New(&function.Spec{ 100 Params: []function.Parameter{ 101 { 102 Name: "str", 103 Type: cty.String, 104 AllowDynamicType: true, 105 }, 106 { 107 Name: "offset", 108 Type: cty.Number, 109 AllowDynamicType: true, 110 }, 111 { 112 Name: "length", 113 Type: cty.Number, 114 AllowDynamicType: true, 115 }, 116 }, 117 Type: function.StaticReturnType(cty.String), 118 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 119 in := []byte(args[0].AsString()) 120 var offset, length int 121 122 var err error 123 err = gocty.FromCtyValue(args[1], &offset) 124 if err != nil { 125 return cty.NilVal, err 126 } 127 err = gocty.FromCtyValue(args[2], &length) 128 if err != nil { 129 return cty.NilVal, err 130 } 131 132 if offset < 0 { 133 totalLenNum, err := Strlen(args[0]) 134 if err != nil { 135 // should never happen 136 panic("Stdlen returned an error") 137 } 138 139 var totalLen int 140 err = gocty.FromCtyValue(totalLenNum, &totalLen) 141 if err != nil { 142 // should never happen 143 panic("Stdlen returned a non-int number") 144 } 145 146 offset += totalLen 147 } else if length == 0 { 148 // Short circuit here, after error checks, because if a 149 // string of length 0 has been requested it will always 150 // be the empty string 151 return cty.StringVal(""), nil 152 } 153 154 sub := in 155 pos := 0 156 var i int 157 158 // First we'll seek forward to our offset 159 if offset > 0 { 160 for i = 0; i < len(sub); { 161 d, _, _ := textseg.ScanGraphemeClusters(sub[i:], true) 162 i += d 163 pos++ 164 if pos == offset { 165 break 166 } 167 if i >= len(in) { 168 return cty.StringVal(""), nil 169 } 170 } 171 172 sub = sub[i:] 173 } 174 175 if length < 0 { 176 // Taking the remainder of the string is a fast path since 177 // we can just return the rest of the buffer verbatim. 178 return cty.StringVal(string(sub)), nil 179 } 180 181 // Otherwise we need to start seeking forward again until we 182 // reach the length we want. 183 pos = 0 184 for i = 0; i < len(sub); { 185 d, _, _ := textseg.ScanGraphemeClusters(sub[i:], true) 186 i += d 187 pos++ 188 if pos == length { 189 break 190 } 191 } 192 193 sub = sub[:i] 194 195 return cty.StringVal(string(sub)), nil 196 }, 197}) 198 199var JoinFunc = function.New(&function.Spec{ 200 Params: []function.Parameter{ 201 { 202 Name: "separator", 203 Type: cty.String, 204 }, 205 }, 206 VarParam: &function.Parameter{ 207 Name: "lists", 208 Type: cty.List(cty.String), 209 }, 210 Type: function.StaticReturnType(cty.String), 211 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 212 sep := args[0].AsString() 213 listVals := args[1:] 214 if len(listVals) < 1 { 215 return cty.UnknownVal(cty.String), fmt.Errorf("at least one list is required") 216 } 217 218 l := 0 219 for _, list := range listVals { 220 if !list.IsWhollyKnown() { 221 return cty.UnknownVal(cty.String), nil 222 } 223 l += list.LengthInt() 224 } 225 226 items := make([]string, 0, l) 227 for ai, list := range listVals { 228 ei := 0 229 for it := list.ElementIterator(); it.Next(); { 230 _, val := it.Element() 231 if val.IsNull() { 232 if len(listVals) > 1 { 233 return cty.UnknownVal(cty.String), function.NewArgErrorf(ai+1, "element %d of list %d is null; cannot concatenate null values", ei, ai+1) 234 } 235 return cty.UnknownVal(cty.String), function.NewArgErrorf(ai+1, "element %d is null; cannot concatenate null values", ei) 236 } 237 items = append(items, val.AsString()) 238 ei++ 239 } 240 } 241 242 return cty.StringVal(strings.Join(items, sep)), nil 243 }, 244}) 245 246var SortFunc = function.New(&function.Spec{ 247 Params: []function.Parameter{ 248 { 249 Name: "list", 250 Type: cty.List(cty.String), 251 }, 252 }, 253 Type: function.StaticReturnType(cty.List(cty.String)), 254 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 255 listVal := args[0] 256 257 if !listVal.IsWhollyKnown() { 258 // If some of the element values aren't known yet then we 259 // can't yet predict the order of the result. 260 return cty.UnknownVal(retType), nil 261 } 262 if listVal.LengthInt() == 0 { // Easy path 263 return listVal, nil 264 } 265 266 list := make([]string, 0, listVal.LengthInt()) 267 for it := listVal.ElementIterator(); it.Next(); { 268 iv, v := it.Element() 269 if v.IsNull() { 270 return cty.UnknownVal(retType), fmt.Errorf("given list element %s is null; a null string cannot be sorted", iv.AsBigFloat().String()) 271 } 272 list = append(list, v.AsString()) 273 } 274 275 sort.Strings(list) 276 retVals := make([]cty.Value, len(list)) 277 for i, s := range list { 278 retVals[i] = cty.StringVal(s) 279 } 280 return cty.ListVal(retVals), nil 281 }, 282}) 283 284var SplitFunc = function.New(&function.Spec{ 285 Params: []function.Parameter{ 286 { 287 Name: "separator", 288 Type: cty.String, 289 }, 290 { 291 Name: "str", 292 Type: cty.String, 293 }, 294 }, 295 Type: function.StaticReturnType(cty.List(cty.String)), 296 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 297 sep := args[0].AsString() 298 str := args[1].AsString() 299 elems := strings.Split(str, sep) 300 elemVals := make([]cty.Value, len(elems)) 301 for i, s := range elems { 302 elemVals[i] = cty.StringVal(s) 303 } 304 if len(elemVals) == 0 { 305 return cty.ListValEmpty(cty.String), nil 306 } 307 return cty.ListVal(elemVals), nil 308 }, 309}) 310 311// ChompFunc is a function that removes newline characters at the end of a 312// string. 313var ChompFunc = function.New(&function.Spec{ 314 Params: []function.Parameter{ 315 { 316 Name: "str", 317 Type: cty.String, 318 }, 319 }, 320 Type: function.StaticReturnType(cty.String), 321 Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 322 newlines := regexp.MustCompile(`(?:\r\n?|\n)*\z`) 323 return cty.StringVal(newlines.ReplaceAllString(args[0].AsString(), "")), nil 324 }, 325}) 326 327// IndentFunc is a function that adds a given number of spaces to the 328// beginnings of all but the first line in a given multi-line string. 329var IndentFunc = function.New(&function.Spec{ 330 Params: []function.Parameter{ 331 { 332 Name: "spaces", 333 Type: cty.Number, 334 }, 335 { 336 Name: "str", 337 Type: cty.String, 338 }, 339 }, 340 Type: function.StaticReturnType(cty.String), 341 Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 342 var spaces int 343 if err := gocty.FromCtyValue(args[0], &spaces); err != nil { 344 return cty.UnknownVal(cty.String), err 345 } 346 data := args[1].AsString() 347 pad := strings.Repeat(" ", spaces) 348 return cty.StringVal(strings.Replace(data, "\n", "\n"+pad, -1)), nil 349 }, 350}) 351 352// TitleFunc is a function that converts the first letter of each word in the 353// given string to uppercase. 354var TitleFunc = function.New(&function.Spec{ 355 Params: []function.Parameter{ 356 { 357 Name: "str", 358 Type: cty.String, 359 }, 360 }, 361 Type: function.StaticReturnType(cty.String), 362 Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 363 return cty.StringVal(strings.Title(args[0].AsString())), nil 364 }, 365}) 366 367// TrimSpaceFunc is a function that removes any space characters from the start 368// and end of the given string. 369var TrimSpaceFunc = function.New(&function.Spec{ 370 Params: []function.Parameter{ 371 { 372 Name: "str", 373 Type: cty.String, 374 }, 375 }, 376 Type: function.StaticReturnType(cty.String), 377 Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 378 return cty.StringVal(strings.TrimSpace(args[0].AsString())), nil 379 }, 380}) 381 382// TrimFunc is a function that removes the specified characters from the start 383// and end of the given string. 384var TrimFunc = function.New(&function.Spec{ 385 Params: []function.Parameter{ 386 { 387 Name: "str", 388 Type: cty.String, 389 }, 390 { 391 Name: "cutset", 392 Type: cty.String, 393 }, 394 }, 395 Type: function.StaticReturnType(cty.String), 396 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 397 str := args[0].AsString() 398 cutset := args[1].AsString() 399 return cty.StringVal(strings.Trim(str, cutset)), nil 400 }, 401}) 402 403// TrimPrefixFunc is a function that removes the specified characters from the 404// start the given string. 405var TrimPrefixFunc = function.New(&function.Spec{ 406 Params: []function.Parameter{ 407 { 408 Name: "str", 409 Type: cty.String, 410 }, 411 { 412 Name: "prefix", 413 Type: cty.String, 414 }, 415 }, 416 Type: function.StaticReturnType(cty.String), 417 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 418 str := args[0].AsString() 419 prefix := args[1].AsString() 420 return cty.StringVal(strings.TrimPrefix(str, prefix)), nil 421 }, 422}) 423 424// TrimSuffixFunc is a function that removes the specified characters from the 425// end of the given string. 426var TrimSuffixFunc = function.New(&function.Spec{ 427 Params: []function.Parameter{ 428 { 429 Name: "str", 430 Type: cty.String, 431 }, 432 { 433 Name: "suffix", 434 Type: cty.String, 435 }, 436 }, 437 Type: function.StaticReturnType(cty.String), 438 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 439 str := args[0].AsString() 440 cutset := args[1].AsString() 441 return cty.StringVal(strings.TrimSuffix(str, cutset)), nil 442 }, 443}) 444 445// Upper is a Function that converts a given string to uppercase. 446func Upper(str cty.Value) (cty.Value, error) { 447 return UpperFunc.Call([]cty.Value{str}) 448} 449 450// Lower is a Function that converts a given string to lowercase. 451func Lower(str cty.Value) (cty.Value, error) { 452 return LowerFunc.Call([]cty.Value{str}) 453} 454 455// Reverse is a Function that reverses the order of the characters in the 456// given string. 457// 458// As usual, "character" for the sake of this function is a grapheme cluster, 459// so combining diacritics (for example) will be considered together as a 460// single character. 461func Reverse(str cty.Value) (cty.Value, error) { 462 return ReverseFunc.Call([]cty.Value{str}) 463} 464 465// Strlen is a Function that returns the length of the given string in 466// characters. 467// 468// As usual, "character" for the sake of this function is a grapheme cluster, 469// so combining diacritics (for example) will be considered together as a 470// single character. 471func Strlen(str cty.Value) (cty.Value, error) { 472 return StrlenFunc.Call([]cty.Value{str}) 473} 474 475// Substr is a Function that extracts a sequence of characters from another 476// string and creates a new string. 477// 478// As usual, "character" for the sake of this function is a grapheme cluster, 479// so combining diacritics (for example) will be considered together as a 480// single character. 481// 482// The "offset" index may be negative, in which case it is relative to the 483// end of the given string. 484// 485// The "length" may be -1, in which case the remainder of the string after 486// the given offset will be returned. 487func Substr(str cty.Value, offset cty.Value, length cty.Value) (cty.Value, error) { 488 return SubstrFunc.Call([]cty.Value{str, offset, length}) 489} 490 491// Join concatenates together the string elements of one or more lists with a 492// given separator. 493func Join(sep cty.Value, lists ...cty.Value) (cty.Value, error) { 494 args := make([]cty.Value, len(lists)+1) 495 args[0] = sep 496 copy(args[1:], lists) 497 return JoinFunc.Call(args) 498} 499 500// Sort re-orders the elements of a given list of strings so that they are 501// in ascending lexicographical order. 502func Sort(list cty.Value) (cty.Value, error) { 503 return SortFunc.Call([]cty.Value{list}) 504} 505 506// Split divides a given string by a given separator, returning a list of 507// strings containing the characters between the separator sequences. 508func Split(sep, str cty.Value) (cty.Value, error) { 509 return SplitFunc.Call([]cty.Value{sep, str}) 510} 511 512// Chomp removes newline characters at the end of a string. 513func Chomp(str cty.Value) (cty.Value, error) { 514 return ChompFunc.Call([]cty.Value{str}) 515} 516 517// Indent adds a given number of spaces to the beginnings of all but the first 518// line in a given multi-line string. 519func Indent(spaces, str cty.Value) (cty.Value, error) { 520 return IndentFunc.Call([]cty.Value{spaces, str}) 521} 522 523// Title converts the first letter of each word in the given string to uppercase. 524func Title(str cty.Value) (cty.Value, error) { 525 return TitleFunc.Call([]cty.Value{str}) 526} 527 528// TrimSpace removes any space characters from the start and end of the given string. 529func TrimSpace(str cty.Value) (cty.Value, error) { 530 return TrimSpaceFunc.Call([]cty.Value{str}) 531} 532 533// Trim removes the specified characters from the start and end of the given string. 534func Trim(str, cutset cty.Value) (cty.Value, error) { 535 return TrimFunc.Call([]cty.Value{str, cutset}) 536} 537 538// TrimPrefix removes the specified prefix from the start of the given string. 539func TrimPrefix(str, prefix cty.Value) (cty.Value, error) { 540 return TrimPrefixFunc.Call([]cty.Value{str, prefix}) 541} 542 543// TrimSuffix removes the specified suffix from the end of the given string. 544func TrimSuffix(str, suffix cty.Value) (cty.Value, error) { 545 return TrimSuffixFunc.Call([]cty.Value{str, suffix}) 546} 547