In this function, there are two odd behaviors (tested on Windows PS 5.1/7.4.6):
$FilePath
never casts to a FileInfo object and remains as a Stringfunction 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.
(Parameter) variables implement
type constraints (e.g., placing [string]
before of a parameter-variable declaration)
attribute decorations (e.g., placing a [ValidateScript({...})]
attribute before a parameter-variable declaration)
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:
Because you've type-constrained $FilePath
to [string]
, the corresponding attribute also converts whatever value you assign later to a [string]
, effectively preventing a type change.
Because you're assigning to $FilePath
again in the body of your function ($FilePath = New-Object IO.FileInfo ...
), the [ValidateScript()]
attribute's script block is invoked again.
You can bypass the (potentially surprising) behavior as follows:
Treat parameter-declaration variables as read-only - that way, whatever constraints and validation is associated with them are only evaluated once, during parameter binding.
Use separate, regular variables with different names in the body of your function or script to perform transformations on the parameter values; e.g., in your case:
# $filePathInfo is a function-local variable that stores a transformation of
# the $FilePath parameter-variable value.
$filePathInfo = New-Object IO.FileInfo (Resolve-Path -Path $FilePath).Path
[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).