Modules/businessdev.ALbuild.Pipeline/Private/Get-AdoExpressionValue.ps1

function Split-AdoArgument {
    <#
    .SYNOPSIS
        Splits a comma-separated Azure DevOps expression argument list at the top level.
    .DESCRIPTION
        Internal helper: respects nested parentheses and single-quoted literals so commas inside
        them do not split the list.
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param([Parameter(Mandatory)] [AllowEmptyString()] [string] $Text)

    $argList = [System.Collections.Generic.List[string]]::new()
    $depth = 0; $inQuote = $false; $current = [System.Text.StringBuilder]::new()
    foreach ($ch in $Text.ToCharArray()) {
        switch ($ch) {
            "'" { $inQuote = -not $inQuote; [void]$current.Append($ch) }
            '(' { if (-not $inQuote) { $depth++ }; [void]$current.Append($ch) }
            ')' { if (-not $inQuote) { $depth-- }; [void]$current.Append($ch) }
            ',' {
                if (-not $inQuote -and $depth -eq 0) { [void]$argList.Add($current.ToString()); [void]$current.Clear() }
                else { [void]$current.Append($ch) }
            }
            default { [void]$current.Append($ch) }
        }
    }
    if ($current.Length -gt 0 -or $argList.Count -gt 0) { [void]$argList.Add($current.ToString()) }
    return @($argList | ForEach-Object { $_.Trim() })
}

function Get-AdoExpressionValue {
    <#
    .SYNOPSIS
        Evaluates the supported subset of an Azure DevOps template expression to a value.
    .DESCRIPTION
        Internal, pure evaluator for the local pipeline runner. Supports the functions used by the
        ALbuild templates - eq, ne, not, and, or, coalesce - and operands: parameters.<name>,
        variables.<name>, a bare loop/parameter name, 'single-quoted literals', true / false, and
        numbers. The full Azure DevOps expression grammar is intentionally NOT implemented.
    .PARAMETER Expr
        The expression text (the inside of a "${{ }}" or an "if" condition).
    .PARAMETER Parameters
        Resolved template parameters and loop variables (name -> value).
    .OUTPUTS
        The evaluated value (bool, string or int).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [AllowEmptyString()] [string] $Expr,
        [hashtable] $Parameters = @{}
    )

    $e = $Expr.Trim()
    if ($e -eq '') { return '' }

    # Function call: name( args ) spanning the whole expression.
    $fn = [regex]::Match($e, '^(?<fn>[A-Za-z][A-Za-z0-9]*)\((?<args>.*)\)$')
    if ($fn.Success) {
        $name = $fn.Groups['fn'].Value.ToLowerInvariant()
        $parts = @(Split-AdoArgument -Text $fn.Groups['args'].Value)
        $values = @($parts | ForEach-Object { Get-AdoExpressionValue -Expr $_ -Parameters $Parameters })

        $toString = { param($v) if ($v -is [bool]) { if ($v) { 'true' } else { 'false' } } else { "$v" } }
        $toBool = {
            param($v)
            if ($v -is [bool]) { return $v }
            $s = "$v"
            return ($s -and $s -ine 'false' -and $s -ne '0')
        }

        switch ($name) {
            'eq' { return ((& $toString $values[0]) -ieq (& $toString $values[1])) }
            'ne' { return -not ((& $toString $values[0]) -ieq (& $toString $values[1])) }
            'not' { return -not (& $toBool $values[0]) }
            'and' { foreach ($v in $values) { if (-not (& $toBool $v)) { return $false } } return $true }
            'or' { foreach ($v in $values) { if (& $toBool $v) { return $true } } return $false }
            'coalesce' { foreach ($v in $values) { if ("$v" -ne '') { return $v } } return '' }
            default { throw "Unsupported expression function '$name' in '$Expr'." }
        }
    }

    # Operands.
    if ($e -ieq 'true') { return $true }
    if ($e -ieq 'false') { return $false }
    if ($e -match "^'(.*)'$") { return $matches[1] }
    if ($e -match '^-?\d+$') { return [int]$e }

    $key = $e
    if ($e -match '^(parameters|variables)\.(.+)$') { $key = $matches[2] }
    if ($Parameters.ContainsKey($key)) { return $Parameters[$key] }
    return $e
}