Context
Consider the following helper function:
Filter If-Null(
[Parameter(ValueFromPipeline=$true)]$value,
[Parameter(Position=0)]$default
) {
Write-Verbose "If ($value) {$value} Else {$default}"
if ($value) {$value} else {$default}
}
It's basically a null-coalescing operator implemented as a pipeline function. It's supposed to work like so:
PS> $myVar = $null
PS> $myVar | If-Null "myDefault" -Verbose
VERBOSE: If () {} Else {myDefault}
myDefault
However, when I set $myVar
to the first element in an empty array...
PS> $myVar = @() | Select-Object -First 1
...which should effectively be the same as $null
...
PS> $myVar -eq $null
True
PS> -not $myVar
True
...then the piping does not work anymore:
PS> $myVar | If-Null "myDefault" -Verbose
There is not output at all. Not even the verbose print. Which means If-Null
is not even executed.
The Question
So it seems like @() | select -f 1
, although being -eq
to $null
, is a somewhat different $null
that somehow breaks piping?
Can anyone explain this behaviour? What am I missing?
Additional Information
PS> (@() | select -f 1).GetType() You cannot call a method on a null-valued expression. At line:1 char:1 + (@() | select -f 1).GetType() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [], RuntimeException + FullyQualifiedErrorId : InvokeMethodOnNull PS> (@() | select -f 1) | Get-Member Get-Member : You must specify an object for the Get-Member cmdlet. At line:1 char:23 + (@() | select -f 1) | Get-Member + ~~~~~~~~~~ + CategoryInfo : CloseError: (:) [Get-Member], InvalidOperationException + FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand
PS> $PSVersionTable Name Value ---- ----- PSVersion 5.0.10586.117 PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...} BuildVersion 10.0.10586.117 CLRVersion 4.0.30319.42000 WSManStackVersion 3.0 PSRemotingProtocolVersion 2.3 SerializationVersion 1.1.0.1
Solution
Ansgar's explanation is correct (a better explanation can be found in mklement0's answer to the duplicate question). I just wanted to share my solution to the problem.
I fixed If-Null
such that it returns the $default
even when nothing is processed:
Function If-Null(
[Parameter(ValueFromPipeline = $true)]$value,
[Parameter(Position = 0)]$default
) {
Process {
$processedSomething = $true
If ($value) { $value } Else { $default }
}
# This makes sure the $default is returned even when the input was an empty array or of
# type [System.Management.Automation.Internal.AutomationNull]::Value (which prevents
# execution of the Process block).
End { If (-not $processedSomething) { $default }}
}
This version does now correctly handle empty pipeline results:
PS> @() | select -f 1 | If-Null myDefault
myDefault
Arrays are unrolled by pipelines, so that each array element is passed on separately. If you pass an empty array into a pipeline it essentially is unrolled into nothing, meaning that the downstream cmdlet is never invoked, thus leaving you with an empty variable.
You can observe this behavior by passing
$null
and@()
into a loop that just echoes a string for each input item:Depending on the context this is different from a variable with the "value"
$null
. Even though in most cases PowerShell will automatically convert the "empty" variable to a value of$null
(as seen in your checks) it doesn't do so when passing the variable into the pipeline. In that case you still don't pass anything into the pipeline, hence your filter is never invoked.