Why Tee-Object Fails to Save Variables Inside PowerShell Functions (Scope Explained)
If you've ever created a handy PowerShell helper in your $PROFILE only to discover that variables captured inside it disappear into thin air, you are not alone. A classic example is using Tee-Object -Variable (or its alias tee) inside a custom function. While the command works seamlessly when run directly in the terminal, it fails to persist variables outside the function.
The Core Problem: PowerShell Variable Scoping
The behavior you are seeing comes down to variable scoping in PowerShell. By default, PowerShell uses dynamic scoping with distinct levels:
- Global Scope: The root level active in your interactive console session.
- Script Scope: Active within a running script file.
- Local Scope: Created dynamically each time a function or block executes.
When you type a command directly into the terminal, such as:
(Get-FileHash -Algorithm sha256 $path).Hash | Tee-Object -Variable bThe current scope is the interactive session (Global scope). Therefore, $b is assigned globally and remains available for subsequent commands.
However, when wrapped inside a function in your $PROFILE:
function gethash {
param([string]$string = '')
...
(Get-FileHash -Algorithm sha256 $string).Hash | tee -Variable b
}PowerShell creates a new local scope for that function execution. Tee-Object creates and assigns the variable b inside that local scope. As soon as the function finishes running, its local scope is destroyed—taking $b with it.
Solution 1: Explicitly Set the Scope (Quick Fix)
If you specifically need Tee-Object to modify a variable in the caller's console session, use a scope modifier like global: inside the -Variable parameter:
(Get-FileHash -Algorithm sha256 $string).Hash | Tee-Object -Variable global:bNote that you omit the $ prefix when passing variable names to the -Variable parameter, but include the scope prefix (global:b). Now, running gethash file.txt will populate $b in your global console session.
Solution 2: The Idiomatic PowerShell Way (Recommended)
While using global: solves the immediate issue, relying on global side effects inside helper functions is generally considered an anti-pattern in PowerShell. It can lead to hard-to-debug code where variables are unintentionally overwritten.
A cleaner and more idiomatic approach is to output the value directly and let the user assign it to whatever variable they choose:
function Get-HashValue {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string]$Path
)
process {
(Get-FileHash -Path $Path -Algorithm SHA256).Hash
}
}With this approach, you can assign it in the terminal naturally:
# Assign directly to a variable
$b = Get-HashValue .\myfile.txt
# Or inspect and assign simultaneously using Tee-Object at the command line
Get-HashValue .\myfile.txt | Tee-Object -Variable bKey Improvements in the Modern Function
- Approved Verb-Noun Syntax: Adopting
Get-HashValueavoids naming conflicts and aligns with PowerShell standards. - Parameter Validation: Using
[ValidateNotNullOrEmpty()]replaces manualifstatements, giving you cleaner code and automatic error messages. - Pipeline Support: Adding
ValueFromPipeline = $trueallows you to pipe filenames directly into your function (e.g.,'file.txt' | Get-HashValue).