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  }