Why Doesn’t My Pipeline Work?

talkbubble I saw a little discussion thread on Twitter this morning which I felt needed a little more room to explain. Plus since we’re in ScriptingGames season beginners might like a few pointers. I always talk about PowerShell, objects and the pipeline. But sometimes what looks like a pipelined expression in the PowerShell ISE doesn’t behave the way you might expect.

Here’s an example.

If you run this, you’ll see numbers 1 to 5 written to the pipeline. But if you try something like this it will fail.

You’ll get an error about an empty pipe. In fact, in the PowerShell ISE you’ll get a red squiggle under the | indicating this is not going to work. That’s because PowerShell isn’t writing to pipeline at the end of the scriptblock, but rather within in. Another way to think about it is at the While operator is not a cmdlet so the only thing writing objects to the pipeline is whatever commands are within the While loop.

What you can do is something like this:

Here, I’m capturing the pipeline output from the scriptblock and saving it to a variable. Then I have objects I can use. Or if you wanted to be clever, you could use a subexpression.

This same behavior also applies to Do and the ForEach enumerator. The latter trips people up all the time.

You think you’ll get the output of ForEach saved to the file, but you’ll run into the empty pipeline again. You could use a variable and then pipe the variable to the file or use a subexpression. Even better, use a pipelined expression.

Here I’m using the cmdlet ForEach-Object, which unfortunately has an alias of ForEach which confuses PowerShell beginners. So don’t assume that just because you see a set of { } that you get pipelined output. Remember, cmdlets write objects to the pipeline, not operators.

Rename Hashtable Key Revised

Last week I posted an advanced PowerShell function to rename a hashtable key. As usual, the more I worked with it the more I realized it was missing something – namely the ability the take a pipelined object. My original version assumed you had saved the hashtable to a variable. But as I was working with ConvertTo-Hashtable I realized the shortcoming. The solution was to modify Rename-Hashtable so that it could accept a hashtable as a piped value.

I won’t go through the function again. You can read the original post to learn more about how it works. Let’s look at what changed. Because I wanted to retain the option to also specify a variable name, I created two parameters sets. One for the piped object and one for the variable name.


HelpMessage="Enter the name of your hash table variable without the `$",
HelpMessage="Enter the existing key name you want to rename")]
HelpMessage="Enter the NEW key name")]

I defined parameter sets called Pipeline and Name and made the former the default in the cmdletbinding attribute. Because the remaining parameters would be in both parameter sets I didn’t specify one. When looking at the function’s help you can see the result.


Because I’m taking input from the pipeline, I needed to add a Process scriptblock. Within the scriptblock, if an object has been piped in, I turn on the passthru variable and create a temporary copy of the piped in hashtable.

Process {
#validate Key and NewKey are not the same
if ($key -eq $NewKey) {
Write-Warning "The values you specified for -Key and -NewKey appear to be the same. Names are NOT case-sensitive"

Try {
#validate variable is a hash table
if ($InputObject) {
Set-Variable -Name $name -Scope $scope -value $InputObject

The rest of the code worked just fine and there was no reason to change it. All I needed to do was transform the -Inputobject value into the -Name value since I already had code that used $Name. Sometimes you need separate code blocks but in this case I didn’t. Once the transformation is complete, the rest of the function runs as originally designed. With this version I can now run commands like this:

PS C:\> $h = get-service spooler -computer Serenity | convertto-hashtable -NoEmpty -Exclude CanStop,CanPauseAndcontinue | rename-hashtable -key machinename -new computername
PS C:\> $h

Name Value
---- -----
computername Serenity
Name spooler
ServiceName spooler
RequiredServices {RPCSS, http}
DependentServices {Fax}
ServiceType Win32OwnProcess, InteractiveProcess
Status Running
ServicesDependedOn {RPCSS, http}
ServiceHandle SafeServiceHandle
DisplayName Print Spooler

Download Rename-Hashtable2 and give it a go.

Pipeline Power

Last week I came across a blog post that had a decent example using PowerShell and PowerCLI to get the disk location for all virtual machines. The posted code works and does display the information you might be after.

$myVMs = get-vm

foreach($vm in $myVMs){
$myDisks = @($vm | get-harddisk)
foreach ($disk in $myDisks) {
write-host $vm.Name, ( $disk|select -ExpandProperty Filename)

But I saw an teaching opportunity. Because the code works I can’t say it is “wrong”, but it really doesn’t adopt the PowerShell paradigm. The first issue is that using Write-Host only writes content to the console. There is no way with this command to do anything else with the results such as sorting, grouping or sending to a text file.

The other issue is the need to use ForEach. This is what we had to do in VBScript but in PowerShell we can take advantage of the pipeline.

get-vm | Select Name,@{Name="Disk";Expression= {$_ | get-harddisk | Select -ExpandProperty Filename }}

But now I can do something with this such as sorting by disk:

PS S:\> get-vm | Select Name,@{Name="Disk";Expression= {$_ | get-harddisk | Select -ExpandProperty Filename }} | sort Disk,Name

Name Disk
---- ----
Cluster Alpha {[datastore1] Cluster Alpha/Cluster ...
Globomantics Mail [datastore1] globomantics mail/Win2K...
MyCompany Exchange 2007 {[datastore1] MyCompany Exchange 200...
MyCompany XP {[datastore1] MyCompany XP/MyCompany...
MyCompany Windows 2008 [datastore1] MyCompany2008/Windows S...
MyCompanyDC 2K3R2 {[datastore1] MyCompanyDC 2K3R2/MyCo...
R2 Server Core -DEL [datastore1] Research Core DC/R2 Ser...
Cluster Bravo {[datastore2] Cluster Bravo/Cluster ...
MyCompany Vista {[datastore2] MyCompany Vista/Vista ...

Or if there are multiple disks, it is much easier to work with them. Write-Host can’t.

PS S:\> $vminfo=get-vm | Select Name,@{Name="Disk";Expression= {$_ | get-harddisk | Select -ExpandProperty Filename }}
PS S:\> $vminfo[1].disk
[datastore2] MyCompany Vista/Vista Baseline.vmdk
[datastore2] MyCompany Vista/MyCompany Vista.vmdk
PS S:\> $vminfo | Export-Clixml c:\work\vminfo.xml

The tricky part here I realize is pulling up a value from a nested object, in this case the Filename and adding it to the VM object. I totally get that this is not something a beginner would necessarily discover on their own, which is why I write stuff like this. But the big difference is that I know have an object written to the pipeline that I can do something with and I didn’t have to resort to keep track of what goes where in some foreach loops.

The other advantage, although not universal, is performance. Running the ForEach code against my 23 VMs took almost 6 seconds. My PowerShell one line took a tad over 3 seconds.

I don’t want you to think you can never use Write-Host or ForEach. Sometimes they make sense and may even perform better. But always ask yourself if you are thinking the PowerShell way and pushing objects through the pipeline or are you writing something that could be mistaken for VBScript.

By the way, I have posted most of this on the blog as a comment that is awaiting moderation. But I figured I would share it with my readers as well.

Have Your Output and Variable Too

There’s a relatively useful suggestion floating around on Twitter on how to save results of PowerShell command to a variable and see the results at the same time.

PS C:\> ($data=get-process)

I’ll admit this is a clever technique: you get the results from Get-Process written to the pipeline AND a variable $data. The other way, and frankly I think a better way, is to use Tee-Object. Usually we use Tee-Object to save output to a file and still write the results to the pipeline.

PS C:\> get-process | tee -variable data

To me, this is much more straightforward and I don’t have to remember a punctuation trick. I’ve also found that using Tee-Object is also better performing. I’ve tried a number of tests and they all seem to verify that Tee-Object is faster. Here’s one test:

PS C:\Users\Jeff> measure-command {($a=dir $env:temp -rec)}

Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 468
Ticks : 4686037
TotalDays : 5.42365393518518E-06
TotalHours : 0.000130167694444444
TotalMinutes : 0.00781006166666667
TotalSeconds : 0.4686037
TotalMilliseconds : 468.6037

PS C:\Users\Jeff> measure-command {dir $env:temp -rec | tee -Variable a}

Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 158
Ticks : 1583448
TotalDays : 1.83269444444444E-06
TotalHours : 4.39846666666667E-05
TotalMinutes : 0.00263908
TotalSeconds : 0.1583448
TotalMilliseconds : 158.3448

I get the same result with both commands but you can see that Tee-Object is significantly faster.

Using the parentheses trick is handy and especially useful in the interactive console. Personally, I’ll probably stick to using Tee-Object and certainly in a script using Tee-Object is easier to follow and understand.

Friday Fun – Get Ranked Object

Earlier this week on Google Plus, Hal Rottenberg posted a PowerShell idea he had. His goal was to identify a group of objects that would be statistically significant. For example, given a collection of files, group the files by size but ranked by size so that you might have a group for the largest files, then big files, then average files and finally small files. His idea was to calculate a rank using a logarithm of the file size. His code was quick and dirty so I took it, as I usually do, and ran with it. The result is a function that will analyse any group of objects and rank them based on a numeric property. Continue reading