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
.