Modules/businessdev.ALbuild.Pipeline/Private/Expand-AdoExpression.ps1

function Expand-AdoExpression {
    <#
    .SYNOPSIS
        Expands template "${{ }}" expressions and "$(var)" macros in a value.
    .DESCRIPTION
        Internal helper for the local pipeline runner. For a string it replaces every "${{ expr }}"
        with its compile-time value (Get-AdoExpressionValue against -Parameters) and every "$(name)"
        macro with the matching runtime variable from -Variables (unknown macros are left intact, as
        Azure DevOps does). Non-string values are returned unchanged.
    .PARAMETER InputObject
        The value to expand (usually a task input string).
    .PARAMETER Parameters
        Resolved template parameters and loop variables.
    .PARAMETER Variables
        Runtime variables (process environment plus accumulated set-variable outputs).
    .OUTPUTS
        The expanded value.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '',
        Justification = 'Parameters and Variables are used inside the regex MatchEvaluator script blocks, which the analyzer does not track.')]
    [CmdletBinding()]
    param(
        [AllowNull()] [object] $InputObject,
        [hashtable] $Parameters = @{},
        [hashtable] $Variables = @{}
    )

    if ($null -eq $InputObject -or $InputObject -isnot [string]) { return $InputObject }

    $text = [regex]::Replace($InputObject, '\$\{\{\s*(?<expr>.*?)\s*\}\}', {
            param($m)
            $v = Get-AdoExpressionValue -Expr $m.Groups['expr'].Value -Parameters $Parameters
            if ($v -is [bool]) { if ($v) { 'true' } else { 'false' } } else { "$v" }
        })

    $text = [regex]::Replace($text, '\$\((?<name>[A-Za-z_][A-Za-z0-9_.]*)\)', {
            param($m)
            $name = $m.Groups['name'].Value
            if ($Variables.ContainsKey($name)) { "$($Variables[$name])" } else { $m.Value }
        })

    return $text
}