Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
July 21, 2022

Common Variables Are Anything But Common

When you run a PowerShell command, you have the benefit of also using several common parameters. These are parameters that are included with every cmdlet. You can also get these parameters in your function by adding [cmdletbinding()]. Today I want to begin an exploration of two common parameters that you should be using in your PowerShell scripting and interactive work. They may appear similar, but as you’ll see, OutVariable and PipelineVariable meet very different needs. I don’t think these parameters get the attention they deserve, so let’s explore how they work and why you might want to use them.

OutVariable

I am sure you are used to saving command output to a variable like this.

$a = Get-Service

Instead of writing the results to the pipeline, the output is redirected to the variable. Using the -OutVariable common parameter lets you see the output and save the results to a variable.

Get-Service -OutVariable b

Specify the variable name without the $. When the command is complete, you can reference $b.

PS C:\> $b.count
322
PS C:\> $b | Select-Object DisplayName, Status -first 3

DisplayName                        Status
-----------                        ------
Agent Activation Runtime_2ff889d7 Stopped
Intel® SGX AESM                   Running
AllJoyn Router Service            Stopped

But there is a subtle detail when using Out-Variable. Let me re-run the command.

$a = Get-Service -OutVariable b 

Both variables contain the same objects. But they are stored slightly differently. Look at the type names.

PS C:\> $a.psobject.TypeNames
System.Object[]
System.Array
System.Object
PS C:\> $b.psobject.TypeNames

System.Collections.ArrayList
System.Object

The first variable is an array, but the second is not.

PS C:\> $a -is [array]
True
PS C:\> $b -is [array]
False

The -OutVariable result is technically a different object type. This object has different methods.

You probably won’t notice the difference in most situations, but it is there.

I use this technique often.

Find-Module Burnttoast -ov m

In this example, I’m using the parameter alias. PowerShell displays the result. I might then try to get the ProjectUri property. I’ll start typing and press <Tab> to autocomplete.

$m.proj<tab>

PowerShell will typically complete the property name. But not here. The variable is an ArrayList. I need to do this:

$m[0].proj<tab>

Even though there is only one object, I have to use the index number. But only if I want tab completion. PowerShell is smart enough to understand this command if I type it out.

PS C:\> $m.additionalmetadata | Format-List authors, description, lastupdated

Authors     : Joshua (Windos) King
description : Module for creating and displaying Toast Notifications on Microsoft Windows 10.
lastUpdated : 7/19/2022 1:25:58 PM -04:00

Appending

You can append to a variable as well.

Get-Process -Id $pid -ov p

The variable $P contains the current PowerShell process. I can append to the variable like this:

Get-Process code -ov +p

Put the + symbol before the variable name.

Pipeline Troubleshooting

The -OutVariable parameter is per cmdlet, which makes it a great troubleshooting tool. Often, we run PowerShell expressions where things are changing in the pipeline. Using -OutVariable, you can capture distinct parts of an expression.

Get-Process -ov all | Where-Object { $_.ws -ge 100MB } -ov w |
Sort-Object WS -ov s | Select-Object Name, ID, WS -ov o

I have saved the output from each command in this expression to a separate variable.

PS C:\> $all.count

315
PS C:\>
PS C:\> $w[0..4]

Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
    318      18   163272     123988       1.30  23816   5 Code
    479      23   162832     212856      39.27  28324   5 Code
    669      30   491432     526548       9.56  31624   5 Code
   6506     166   235484     186800     118.44  20808   5 Dropbox
   1526     253   307252     176712     195.25  25148   5 dwm


PS C:\>
PS C:\> $s[0..4]

Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
    977      42    65824     104924       9.81  18672   5 slack
   1622     132   159476     111456       9.67  29384   5 SearchHost
    447      62   138820     120932      10.55  14196   5 firefox
    318      18   163272     123988       1.41  23816   5 Code
    786     111   144564     130252       4.55  31896   5 pwsh


PS C:\>
PS C:\> $o[0]

Name     Id        WS
----     --        --
slack 18672 107442176

If I wasn’t getting the expected results, I could look at each pipeline step to see what was happening.

Pipeline Processing

But you have to be careful. The variable isn’t fully populated until the pipeline process for the command is complete.

This will work.

PS C:\> Get-Process -ov a | Where-Object { $_.ws -ge 100MB } -ov w |
Sort-Object WS -ov s | Select-Object Name, ID, WS, @{Name = "PctWS" ; Expression = { $_.ws / ($a | Measure-Object ws -Sum).sum * 100 }},@{Name = "TotalProcesses"; Expression = { $a.count } }


Name           : firefox
Id             : 14196
WS             : 106897408
PctWS          : 0.745022596468551
TotalProcesses : 315

Name           : slack
Id             : 18672
WS             : 107311104
PctWS          : 0.747905855041749
TotalProcesses : 315

PowerShell passes objects down the pipeline in buckets. Because Sort-Object has to get everything by the time Select-Object is run, the variable from Get-Process is complete. Watch what happens when I remove Sort-Object.

Get-Process -ov a | Where-Object { $_.ws -ge 100MB } -ov w |
Select-Object Name, ID, WS, @{Name = "PctWS" ; Expression = { $_.ws ($a | Measure-Object ws -Sum).sum * 100 }},@{Name = "TotalProcesses"; Expression = { $a.count } }


Name           : Code
Id             : 23816
WS             : 124780544
PctWS          : 27.5057559478127
TotalProcesses : 10

Name           : Code
Id             : 28324
WS             : 236851200
PctWS          : 30.9756319671736
TotalProcesses : 12

Name           : Code
Id             : 31624
WS             : 608636928
PctWS          : 38.8077671424281
TotalProcesses : 15

The value from Get-Process keeps changing while the pipeline is processing. Sometimes, things will work as you expect.

PS C:\> Get-CimInstance -ClassName Win32_PhysicalMemory -ComputerName srv1 -OutVariable v |
Measure-Object -Property capacity -Sum |
Select-Object @{Name = "Computername"; Expression = { $v.pscomputername.toupper() } },
@{Name = "MemoryGB"; Expression = { $_.sum / 1GB } }


Computername MemoryGB
------------ --------
SRV1                2

But not this.

PS C:\> Get-CimInstance -ClassName Win32_PhysicalMemory -ComputerName dom1, srv1, srv2 -OutVariable v |
Measure-Object -Property capacity -Sum |
Select-Object @{Name = "Computername"; Expression = { $v.pscomputername.toupper() } },
@{Name = "MemoryGB"; Expression = { $_.sum / 1GB } }

Computername       MemoryGB
------------       --------
{DOM1, SRV1, SRV2}        6

Do you see why this doesn’t give the expected result? Even using ForEach-Object doesn’t work.

PS C:\> Get-CimInstance -ClassName Win32_PhysicalMemory -ComputerName dom1, srv1, srv2 -OutVariable v |
ForEach-Object {
    $_ | Measure-Object -Property capacity -Sum |
    Select-Object @{Name = "Computername"; Expression = { $v.pscomputername.toupper() } },
    @{Name = "MemoryGB"; Expression = { $_.sum / 1GB } }
}

Computername MemoryGB
------------ --------
DOM1                2
{DOM1, SRV1}        2
{DOM1, SR...        2

Again, you can see how the variable changes while the pipeline runs. There is a solution to this problem, but I think I’ll save that for next time.

Summary

Using -OutVariable is a very handy technique, provided you understand its nuances and limitations. If you haven’t used it before, I encourage you to try it. Next time we’ll look at -PipelineVariable.

(c) 2022-2025 JDH Information Technology Solutions, Inc. - all rights reserved
Don't miss what's next. Subscribe to Behind the PowerShell Pipeline:
Start the conversation:
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.