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 b

The 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:b

Note 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 b

Key Improvements in the Modern Function

  • Approved Verb-Noun Syntax: Adopting Get-HashValue avoids naming conflicts and aligns with PowerShell standards.
  • Parameter Validation: Using [ValidateNotNullOrEmpty()] replaces manual if statements, giving you cleaner code and automatic error messages.
  • Pipeline Support: Adding ValueFromPipeline = $true allows you to pipe filenames directly into your function (e.g., 'file.txt' | Get-HashValue).