powershell

Function parameter ValidateScript called multiple times


In this function, there are two odd behaviors (tested on Windows PS 5.1/7.4.6):

  1. The validation script is run more than once
  2. $FilePath never casts to a FileInfo object and remains as a String
function Test-Param {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ 
            Write-Host "Test $((Get-Date).Ticks)"
            [IO.File]::Exists( (Resolve-Path -Path $_).Path )
        })]
        [string] $FilePath
    )

    Start-Sleep -Milliseconds 50
    Write-Host "Start"

    $FilePath = New-Object IO.FileInfo (Resolve-Path -Path $FilePath).Path

    Write-Host "After expected object type change"

    $FilePath.GetType().FullName
    $FilePath
}


Test-Param -FilePath $MyInvocation.MyCommand.Path

Here is the output:

Test 638746054867610941
Start
Test 638746054868236274
After expected object type change
System.String
C:\test\test.ps1

This seems like it shouldn't happen, I'd expect parameter validation to only run once and the FilePath variable to get overwritten with a different type of object. Was thinking about posting on powershell github but figured I'd ask the SO community first for thoughts... Is this by design or a potential bug?

Update - as noted in the comments by @theo, if the parameter type is removed or changed to [object] FilePath object type will be a FileInfo object as expected.


Solution

  • (Parameter) variables implement

    as attributes that are attached to the variable object (verify with (Get-Variable FilePath).Attributes | Format-List from your function body), which are notably evaluated every time the variable is assigned to.[1]


    The above explains all behaviors you've observed:


    You can bypass the (potentially surprising) behavior as follows:


    [1] Note that this also applies to regular variables, i.e. variables created in the body of a function rather than to define parameters inside the param(...) block.
    While type constraints on regular variables are not uncommon (e.g., [int] $i = 42; see this answer), validation attributes are rare (e.g., [ValidateRange(1, 41)] [int] $i = 41; trying to assign a value outside that range later, e.g. $i = 42, then causes an (statement-terminating) error).