The Issue#
An issue on the PowerShell GitHub repository claims that the Tee-Object cmdlet exhibits unexpected behavior when $WhatIfPreference = $true.
The example provided creates a function that supports the -WhatIf parameter. This function calls Tee-Object -Variable which, according to the PowerShell Docs, ‘Saves command output in a … variable and also sends it down the pipeline.’ The example includes console output when calling the function with and without the -WhatIf parameter.
function Test-Tee {
[CmdletBinding(SupportsShouldProcess)]
Param()
$true | Tee-Object -Variable b # Explicit call to 'Tee-Object' because I'm on a Mac and 'tee' is a built-in system command.
$b
}
> Test-Tee
True
True
> Test-Tee -WhatIf
True
What if: Performing the operation "Set variable" on target "Name: b Value: True".
The Investigation#
-WhatIf and $WhatIfPreference#
First, I looked at the -WhatIf parameter and its functionality. Microsoft Learn and PowerShell Docs explain that passing a supported command the -WhatIf parameter or setting $WhatIfPreference = $true, sets this preference for the current scope or session, respectively.
It’s my understanding that no state change should occur when $WhatIfPreference is set to $true.
Next, I looked at the the issue’s reported behavior, What if: Performing the operation "Set variable" on target "Name: b Value: True".. I wondered what the output would be if we tee a file instead of a variable.
function Test-Tee {
[CmdletBinding(SupportsShouldProcess)]
Param()
$true | Tee-Object -FilePath (Join-Path -Path $env:HOME -ChildPath 'test.txt')
$b
}
Test-Tee -WhatIf
What if: Performing the operation "Output to File" on target...
As I expected, the What if:... message was printed to the console. It’s interesting though, the messages that are output. These messages are exactly the same as if we called Set-Variable or Out-File.
Out-File -Path (Join-Path -Path $env:HOME -ChildPath 'test.txt') -InputObject $true -WhatIf
Set-Variable -Name b -Value $true -WhatIf
What if: Performing the operation "Output to File" on target...
What if: Performing the operation "Set variable" on target "Name: b Value: True".
PowerShell Source#
I suspected that Tee-Object directly calls Out-File or Set-Variable. I forked the PowerShell repository and went digging. The code in question lives in Tee-Object.cs.
// src/Microsoft.PowerShell.Commands.Utility/commands/utility/Tee-Object.cs
protected override void BeginProcessing()
{
_commandWrapper = new CommandWrapper();
if (string.Equals(ParameterSetName, "File", StringComparison.OrdinalIgnoreCase))
{
_commandWrapper.Initialize(Context, "out-file", typeof(OutFileCommand));
...
}
else if (string.Equals(ParameterSetName, "LiteralFile", StringComparison.OrdinalIgnoreCase))
{
_commandWrapper.Initialize(Context, "out-file", typeof(OutFileCommand));
...
}
else
{
// variable parameter set
_commandWrapper.Initialize(Context, "set-variable", typeof(SetVariableCommand));
...
}
}
Sure enough, we can see that, depending on which ParameterSet is active, Out-File or Set-Variable are being called, passing through a Context, which includes the $WhatIfPreference.
Tee-Object itself does not declare SupportsShouldProcess = $true in its attributes. However, by calling Out-File or Set-Variable and passing the current Context, it allows these underlying cmdlets (which do support ShouldProcess) to detect and respond to the $WhatIfPreference present in that context.
The Findings#
Tee-Object appears to be working as intended following PowerShell’s designed behaviors around state altering commands.
When calling Test-Tee -WhatIf, $WhatIfPreference is set to $true for this function and any command it spawns through its scope. This is different from calling Tee-Object -WhatIf, which, as shown in the issue produces the expected Tee-Object: A parameter cannot be found that matches parameter name 'WhatIf'. error.
The Script#
I wanted to see if I could write supported PowerShell that would write to the pipeline and set a variable’s value while $WhatIfPreference = $true.
function Test-Tee {
[CmdletBinding(SupportsShouldProcess)]
Param()
Write-Output $true -OutVariable b
$b
}
> Test-Tee
True
True
> Test-Tee -WhatIf
True
True
This example does just that. By using Write-Output, a non-state changing command, and using the CommonParameter -OutVariable, we can achieve the intended behavior.
The Better Question#
Set-Variable clearly and accurately respects the $WhatIfPreference. As expected, direct variable assignment ($var = val) allows the variable to be set regardless of the $WhatIfPreference value. What’s interesting though is that -OutVariable also allows the variable to be set. This raises another very interesting question that should be discussed with a larger audience.
If direct variable assignment and -OutVariable both allow setting the value of a variable when $WhatIfPreference = $true, why does Set-Variable implement SupportsShouldProcess?
The Source Solution#
We now have to ask ourselves where the best place to update source would be. I believe there are a few options, listed in what I believe to be most to least acceptable.
- Update
Tee-Objectto addSupportsShouldProcessfunctionality.- Minimizes risk to existing scrips.
- Doesn’t require changing current implementations of
Out-FileorSet-Variable - Allows passing
-WhatIf:$falseto override$WhatIfPreference = $trueso thatOut-FileorSet-Variableperform their function.
- Update
Tee-Objectto remove the dependency onSet-Variable.- Write or reference code that sets the variable inside
Tee-Object. - Would still show the
WhatIfoutput when teeing to a file.
- Write or reference code that sets the variable inside
- Update
Tee-Objectto add another NamedParameter value settingWhatIf = $false- Potentially breaking change: any script that uses
Tee-Objectand expects theWhatIf:output would break. - Hacky and effectively hides implementation details.
- Potentially breaking change: any script that uses
- Update
Set-Variableto removeSupportsShouldProcessfunctionality.- Breaking change: any script that references
Set-Variable -WhatIfis now broken. - Brings
Set-Variableinto alignment with direct variable assignment and-OutVariablefunctionality.
- Breaking change: any script that references
