Private/ConvertFrom-HDTStepCondition.ps1

function ConvertFrom-HDTStepCondition {
    <#
        .SYNOPSIS
            Parses a step condition into Left, Operator and Right.
 
        .DESCRIPTION
            HDT deliberately has no condition language, and an authored
            sequence uses exactly one shape - '"%_HDTPhase%" == "FullOS"' - so
            the grammar is CLOSED and tiny:
 
              <condition> := <operand> <operator> <operand>
              <operand> := '"' anything-but-a-double-quote '"'
                           | a token with no whitespace and no double quote
              <operator> := == | != | -eq | -ne | -like | -notlike
 
            Surrounding double quotes are stripped from each operand, so the
            evaluator compares values rather than quoting. The raw text is kept on
            the result for the message a failure prints.
 
            IT IS CALLED AT IMPORT TIME. Assert-HDTSequenceDocument runs it over
            every condition in the document, so a malformed one fails authoring
            rather than a deployment at 3 a.m. Nothing about a condition is
            deferred to the moment it matters.
 
            The refusals are deliberate, not accidental. A boolean expression
            ('%A% == "1" -and %B% == "2"') has no match in this grammar and is
            rejected: a half-working condition language is worse than none,
            because it runs the wrong branch silently. So is an unterminated
            quote, which is why an unquoted operand may not contain a double
            quote at all - otherwise '"%A% == "1"' would parse as the two
            operands '"%A%' and '1' and mean something nobody wrote.
 
            YAML NOTE. Because a condition carries double quotes as part of its
            own grammar, the whole condition must be a SINGLE-quoted YAML scalar:
 
              condition: '"%_HDTPhase%" == "FullOS"'
 
            The unquoted form is not parseable YAML at all.
 
        .PARAMETER Condition
            The condition text, exactly as the document carried it.
 
        .PARAMETER Path
            The file the condition came from, for the error message. Optional.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject with Left, Operator,
            Right and Text. Left and Right have their surrounding quotes removed;
            Text is the raw input.
 
        .EXAMPLE
            ConvertFrom-HDTStepCondition -Condition '"%_HDTPhase%" == "FullOS"'
 
            Returns Left = '%_HDTPhase%', Operator = '==', Right = 'FullOS'.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyString()]
        [AllowNull()]
        [string] $Condition,

        [Parameter()]
        [string] $Path
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $grammar = 'A condition is <operand> <operator> <operand>, where an operand is a double-quoted string or a token with no whitespace, and an operator is one of == != -eq -ne -like -notlike. HDT has no condition language.'

    if ([string]::IsNullOrWhiteSpace($Condition)) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path `
                    -Message ("the condition is empty. {0}" -f $grammar)))
    }

    # An unquoted operand may not contain a double quote: that is what turns an
    # unterminated quote into a refusal instead of a silent misparse.
    $pattern = '^\s*(?<left>"[^"]*"|[^\s"]+)\s+(?<operator>==|!=|-eq|-ne|-like|-notlike)\s+(?<right>"[^"]*"|[^\s"]+)\s*$'

    $match = [regex]::Match($Condition, $pattern)

    if (-not $match.Success) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path `
                    -Message ("the condition '{0}' cannot be parsed. {1}" -f $Condition, $grammar)))
    }

    # A SECOND SPELLING OF THE SAME TOKEN. Everybody who writes these documents
    # writes PowerShell all day, and '$HDTIsUEFI -eq $true' is what their hands
    # produce. It always PARSED - the grammar has taken -eq from the start - and
    # then compared the literal '$HDTIsUEFI' to the literal '$true', which is
    # false on every machine forever. A condition that is silently never true is
    # worse than one that is refused.
    #
    # IT IS A REWRITE, NOT AN EVALUATION. $Name becomes %Name% and is expanded
    # by the same lookup as before; $true and $false become the words the
    # comparison already understands. Nothing reaches Invoke-Expression - a
    # condition field that ran arbitrary code would run it inside WinPE, as
    # SYSTEM, out of a file on a share that anybody with write access can edit.
    #
    # ONLY A WHOLE OPERAND IS REWRITTEN. '$100 -eq $100' is two prices, not two
    # variables, and a $ in the middle of a value is left alone.
    $readOperand = {
        param([string] $Operand)

        # A quoted operand is a literal and is never a token: "$true" is the
        # five characters, which is how an author says so.
        if ($Operand -match '^"(.*)"$') { return [string] $Matches[1] }

        if ($Operand -eq '$true') { return 'True' }
        if ($Operand -eq '$false') { return 'False' }
        if ($Operand -match '^\$([A-Za-z_][A-Za-z0-9_]*)$') { return ('%{0}%' -f $Matches[1]) }

        return $Operand
    }

    return [pscustomobject] @{
        Left     = [string] (& $readOperand $match.Groups['left'].Value)
        Operator = $match.Groups['operator'].Value
        Right    = [string] (& $readOperand $match.Groups['right'].Value)
        Text     = $Condition
    }
}