Replace placeholders in string with user-defined Bicep function

When you have a string value in Bicep with multiple placeholders that you want to replace, it can be tricky to find a good way to do this. In this blog post, I will show you how you can replace placeholders in a string with a couple of user-defined functions.
Normally in Bicep, you would use string interpolation to set environment-specific values in a resource. In some cases, I find it useful to store certain data in a separate text file and use one of the file functions to read the content of the file and use it in Bicep. For example, I do this when deploying an Azure workbook, as I explained in one of my previous blog posts: Deploy Azure Workbook and App Insights Function. In such a case, I add placeholders to the input file in a specific format and replace them with the actual values before deploying the resource.
For example, if we have the following input string, where a placeholder is defined with the format $(placeholder)
:
var input = '''
first = $(first)
second = $(second)
third = $(third)
'''
And we have the following placeholders and values:
var placeholders = {
first: 'one'
second: '2'
third: 'III'
}
Then, after replacing the placeholders, the end result should be as follows:
var result = '''
first = one
second = 2
third = III
'''
In for example C#, I would create a dictionary with placeholders as keys and the actual values as values. Then I would loop through the dictionary and replace the placeholders with the actual values. Unfortunately, in Bicep, variables are immutable. So, you can’t just loop through a dictionary and replace the placeholders in the input string.
Up until now, I’ve been using ‘temporary’ variables to store the intermediate results. See the following example:
var input = '''
first = $(first)
second = $(second)
third = $(third)
'''
var temp1 = replace(input, '$(first)', 'one')
var temp2 = replace(temp1, '$(second)', '2')
var result = replace(temp2, '$(third)', 'III')
This can become quite cumbersome when you have a lot of placeholders. It’s also not very flexible, and a mistake is easily made. I’ve had quite a few times when I used the wrong variable as an input for the replace function, resulting in incorrect output.
With the introduction of user-defined functions, I thought I could perhaps use a recursive function to loop over the placeholders and replace them with the actual values. However, Bicep doesn’t allow functions to call themselves directly or indirectly. So, I had to come up with another solution.
Fortunately, I found the reduce function, which has actually been around for a couple of years already. This function reduces an array to a single value. The signature is:
reduce(inputArray, initialValue, lambda expression)
We can pass our placeholders into the inputArray
, the initial string into the initialValue
, and use the lambda expression
to replace the placeholders with the actual values. The lambda expression has the current and next value parameters and an optional index.
The following Bicep code shows how you can use the reduce
function to replace placeholders in a string:
var input = '''
first = $(first)
second = $(second)
third = $(third)
'''
var placeholders = {
first: 'one'
second: '2'
third: 'III'
}
var placeholdersArray = items(placeholders)
var result = reduce(
placeholdersArray,
input, // this is the first 'current'
(current, next) => replace(string(current), '$(${next.key})', next.value)
)
We first convert the placeholders object to an array. Then we use the reduce
function to loop over the placeholders. On the first iteration, current
will have the value of the input
string and next
will be the first item in the placeholders
array. The result of replace
will be the new current
value for the next iteration.
If input
has the value $(first) $(second) $(third)
and we use the placeholders from our previous example, then it would look like this:
index | current | next | result of replace |
---|---|---|---|
1 | '$(first) $(second) $(third)' |
{ first: one } |
'one $(second) $(third)' |
2 | 'one $(second) $(third)' |
{ second: 2 } |
'one 2 $(third)' |
3 | 'one 2 $(third)' |
{ third: III } |
'one 2 III' |
We can create a user-defined function to convert this into reusable logic. See the following code:
@export()
func replacePlaceholders(originalString string, placeholders { *: string }) string =>
replacePlaceholderInternal(originalString, items(placeholders))
func replacePlaceholderInternal(originalString string, placeholders array) string =>
reduce(
placeholders,
originalString, // this is the first 'current'
(current, next) => replacePlaceholder(current, next.key, next.value)
)
@export()
func replacePlaceholder(originalString string, placeholder string, value string) string =>
replace(originalString, '$(${placeholder})', value)
As you can see, I’ve created 3 functions:
- The
replacePlaceholders
function is the main entry point. You call it from your Bicep code, passing the original string and an object with placeholders and their string values. It converts the placeholders object into an array and callsreplacePlaceholderInternal
. - The
replacePlaceholderInternal
function uses thereduce
function to iterate over the array of placeholders. For each placeholder, it callsreplacePlaceholder
to perform the replacement. - The
replacePlaceholder
function handles the actual replacement of a single placeholder with its value. You can also use this function directly if you only need to replace one placeholder in a string.
And here’s a sample of how you can use the replacePlaceholders
function in your Bicep code:
import { replacePlaceholders } from './replace-placeholders.bicep'
var input = '''
first = $(first)
second = $(second)
third = $(third)
'''
var placeholders = {
first: 'one'
second: '2'
third: 'III'
}
var result = replacePlaceholders(input, placeholders)
You can find the final Bicep code here.
Similar to my previous blog post, I’ve included tests to validate the functionality of the replacePlaceholders
function. You can view the test cases and implementation details here. For more insights on the (experimental) Bicep Testing Framework used, refer to my previous blog post.