PSCustomObject: Save Puppies and Avoid Dead Ends

Uncategorized

Welcome to Scripting Games 2013. Here's my favorite hint for improving your functions and scripts. Avoid writing to the console or formatting your output. Instead use PSCustomObject in Windows PowerShell 3.0 and leave the formatting to the end user.
Windows PowerShell provides lots of great ways to return the output of a command or function. You can write to the host program (Write-Host), write to a file (Out-File), and format your output to look really pretty (Format-*). But all of these techniques kill puppies and bring the pipeline to an abrupt halt.
"Puppies?," you ask. Yes! Windows PowerShell MVP and Scripting Games 2013 Viceroy Don Jones (@concentrateddon) famously says that every time you use Write-Host, a puppy dies. So sad!
The Format cmdlets are almost as bad, although no deaths have yet been attributed to them. Instead, when you use a Format cmdlet, a huge STOP sign should appear warning you that you've brought the pipeline to a halt. Not technically, of course, but for all practical purposes.
To see what I mean, take a peek at these two commands. The output of these commands looks very similar, but it's really quite different.

PS C:\ Get-Process csrss
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
885 14 2568 5092 49 516 csrss
714 19 3996 28036 92 632 csrss
PS C:\ Get-Process csrss | Format-Table
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
885 14 2568 5092 49 516 csrss
714 19 3996 28036 92 632 csrss

These two commands return different objects and the difference really matters. To see the different output types, you can pipe them to Get-Member. I've used a slightly different approach that gets only the names of types in the output, but it's the same idea.

PS C:\ Get-Process csrss | foreach {$_.gettype().fullname}
System.Diagnostics.Process
System.Diagnostics.Process
PS C:\ Get-Process csrss | Format-Table | foreach {$_.gettype().fullname}
Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
Microsoft.PowerShell.Commands.Internal.Format.GroupStartData
Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData
Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData
Microsoft.PowerShell.Commands.Internal.Format.GroupEndData
Microsoft.PowerShell.Commands.Internal.Format.FormatEndData

Instead of a process object, the formatted command returns a bunch of format objects. You usually discover this when you try to use them in another command. For example, these format objects don't have the properties of a process object, like PagedMemorySize or Handles.

PS C:\ $p = Get-Process csrss
PS C:\ $p | foreach PagedMemorySize
2629632
4075520
PS C:\ $pf = Get-Process csrss | Format-Table
PS C:\ $pf | foreach PagedMemorySize
PS C:\
PS C:\ get-process csrss | sort Handles
Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    723      19     3980      28416    87             632 csrss
    881      14     2568       5096    49             516 csrss
PS C:\ get-process csrss | ft | sort Handles
out-lineoutput : The object of type "Microsoft.PowerShell.Commands.
Internal.Format.FormatEntryData" is not valid or not in the correct
sequence. This is likely caused by a user-specified "format-*"
command which is conflicting with the default formatting.
    + CategoryInfo          : InvalidData: (:) [out-lineoutput],
InvalidOperationException
    + FullyQualifiedErrorId : ConsoleLineOutputOutOfSequencePacket,
Microsoft.PowerShell.Commands.OutLineOutputCommand

So you've lost the opportunity to use these objects in subsequent commands. Unless you really want formatting object, the pipeline is effectively dead. Almost as sad as those puppies.
I realized this problem when some colleagues at Microsoft asked me to generate a report that listed the CDXML files in a CIM module and the CIM commands that were defined in each CDXML file. I wrote a tiny script that produced a nice report that looked like this:

MSFT_NetIPAddress.cdxml-help.xml
------------------------------------
Get-NetIPAddress
Set-NetIPAddress
Remove-NetIPAddress
New-NetIPAddress
MSFT_NetIPInterface.cdxml-help.xml
------------------------------------
Get-NetIPInterface
Set-NetIPInterface
MSFT_NetIPv4Protocol.cdxml-help.xml
------------------------------------
Get-NetIPv4Protocol
Set-NetIPv4Protocol
MSFT_NetIPv6Protocol.cdxml-help.xml
------------------------------------
Get-NetIPv6Protocol
Set-NetIPv6Protocol
. . .

But, instead of being delighted, they reported that they now had data that they couldn't use. I had created a dead end. Pretty, but useless. They were happier with a command that produced useable results, even if they weren't pretty.

PS C:\ (Get-Module $ModuleName).NestedModules | Select-Object Name, Path, ExportedCommands
Name                      Path                                    ExportedCommands
----                      ----                                    ----------------
MSFT_NetIPAddress         C:\windows\system32\WindowsPowerShel... {[Get-NetIPAddres
MSFT_NetIPInterface       C:\windows\system32\WindowsPowerShel... {[Get-NetIPInterf
MSFT_NetIPv4Protocol      C:\windows\system32\WindowsPowerShel... {[Get-NetIPv4Prot
MSFT_NetIPv6Protocol      C:\windows\system32\WindowsPowerShel... {[Get-NetIPv6Prot
. . .

To avoid this dead end in the silly Get-Process case, you just remove the Format-Table command. Or, you can use the Select-Object cmdlet to create an object that is a filtered subset of the current object, if that's what you need.
But how do you manage when you're returning values from different objects? It's easy to put them in a table, but there's a much better way that doesn't stop the pipeline.
Windows PowerShell 3.0 introduces PSCustomObject. You can read all about it, and about Windows PowerShell 2.0 alternatives, in about_Object_Creation. PSCustomObject makes it easy for you to create objects.
As the name implies, PSCustomObject creates a custom object with the properties that you specify. The resulting custom object works just like any .NET class object, so you can pass it through the pipeline and use it in subsequent commands.
The value of PSCustomObject is a hash table (@{Key = Value; Key=Value...}) where the keys are property names and the values are property values. When you define a PSCustomObject hash table in a script or function, Windows PowerShell magically creates an object for every instance that you pass to it.
Here's how I used it in a little script that tells you the versions of Updatable Help you have on your local machine.

foreach ($helpInfoFile in $helpInfoFiles)
{
    $ModuleName = $HelpInfoFile.Name.Split('_')[0]
    $CultureInfo = ([xml](Get-Content `
         $HelpInfoFile)).HelpInfo.SupportedUICultures.UICulture
    $UICulture = $CultureInfo.UICultureName
    $Version = $CultureInfo.UICultureVersion
    [PSCustomObject]@{"ModuleName"=$ModuleName;
                      "Culture"=$UICulture;
                      "Version"=$Version}
}

In this case , I was processing a bunch of HelpInfo XML files. I want to return an object that contains the module name, the name of the UI culture, and the version number for that UI culture. The details don't matter, except that the property values weren't all in the same object, so I couldn't just select from an object.
PSCustomObject to the rescue! See how easy this is!
In the ForEach loop, I get the values that I need. Then I just define a PSCustomObject and … voila! … I have my objects. The default formatting makes them look nice enough.

ModuleName              Culture        Version
----------              -------        -------
AppLocker               en-US          3.1.0.0
Appx                    en-US          3.1.0.0
BitLocker               en-US          3.1.0.0
BranchCache             en-US          3.1.0.0

But more importantly, the pipeline continues. When I pipe to Get-Member, it shows that I have a usable custom object:

PS C:\ $u | get-member
   TypeName: System.Management.Automation.PSCustomObject
Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
Culture     NoteProperty System.String Culture=en-US
ModuleName  NoteProperty System.String ModuleName=AppLocker
Version     NoteProperty System.String Version=3.1.0.0

And, I can use the output in subsequent commands.

PS C:\ $u | sort Version | group Version
Count Name                      Group
----- ----                      -----
   24 3.0.0.0                   {@{ModuleName=NetSwitchTeam;
    1 3.0.1.0                   {@{ModuleName=MsDtc; Culture=
    1 3.0.2.0                   {@{ModuleName=Wdac; Culture=e
   15 3.1.0.0                   {@{ModuleName=ScheduledTasks;
    4 3.2.0.0                   {@{ModuleName=Microsoft.WSMan
    1 {3.2.15.3, 3.2.15.0, 3... {@{ModuleName=Show-Calendar;
    1 3.4.0.0                   {@{ModuleName=NetTCPIP; Cultu

Now, go out and try it! Some of the Scripting Games challenges might require a table, list, or some other formatting, but if it doesn't, be sure return a really useful object.
Good luck to everyone!

13 Responses to " PSCustomObject: Save Puppies and Avoid Dead Ends "

  1. John Hammer says:

    Great topic, I often see folks struggling with this because they do some formatting and then try to reuse that and are stuck with the dead end.

  2. Martin9700 says:

    Saving puppies is important!

  3. Great post, helped me over a troublespot I was having on a script

  4. Other says:

    PowerShell should have an alias called “ObjectShell.”

  5. matt says:

    “Here’s how I used it in a little script that tells you the versions of Updatable Help you have on your local machine.”
    And right there is where you lost me.
    I don’t understand this: “…the property values weren’t all in the same object, so I couldn’t just select from an object.”
    I don’t get it at all; I seem to be missing the core concept. You’re processing some objects that have properties, so you make a new object with those properties?

  6. June Blender says:

    That’s it, exactly, Matt!
    The particular example isn’t important. The core idea is that you can use PSCustomObject to create objects even when the properties of the new object come from different objects and different sources. This characteristic distinguishes PSCustomObject from Select-Object and other easy techniques.

  7. […] is used, not only to save puppies, but so that you can see what is happening while still allowing you to save the output to file. […]

  8. mc says:

    This is a nice post. Unless I’m missing something you didn’t show all the code for your example, i.e. I don’t know where $helpInfoFiles comes from but I understand the concept. Where I’m really lost is where $u comes from, because I don’t understand where you specify the object name.
    So here’s how I found this article. I am trying to create a PSObject or PSCustomObject where the name of the new object is the value of a parameter or even the property value of another object. I want to create a new object using the name of each value in an existing object that was created by importing a CSV file (although I guess the source is hardly relevant)
    To try and explain:

    $props = @{
        p1="one";
        p2="two"}
    $newobjectname = "testobjectname"
    $newobjectvariablename = "$" + $newobjectname
    #so that newobjectvariablename should be $testobjectname
    foreach ($newobj in $newobjectvariablename) {
        $newobj = New-Object -TypeName PSObject -Property $props
    }
    # Now I should have an object of type PSCustomObject with properties p1 and p2 with values of "one" and "two" respectively. This doesn't work.
    # Instead I have an object called $newobj with the properties and values I needed in $newtestobjectname
    

    I’ve also tried things like:
    ($newobjectvariablename | Out-String).ToString() = New-Object …
    and
    $newobjectvariablename.ToString() = New-Object…
    Is this even possible or did I just completely miss something obvious? Thanks.

    • Mike Hanson says:

      Try:
      $sVarName = “Things”
      $sVarValue = “Lots of Stuff”
      New-Variable $sVarName -Value $sVarValue
      $Things.getType()
      $Things | GM

      • Mike Hanson says:

        More correctly, try this:
        $Props = @{
        P1=”One”;
        P2=”Two”}
        $NewObjectName = “TestObjectName”
        New-Variable -Name $NewObjectName -Value $Props

  9. […] Hmm. That doesn't quite work. The author property is in author.name and the link is in content.src. You can use a calculated property or a PSCustomObject to fix this. Let's use a PSCustomObject. […]

  10. […] brand-new ConvertFrom-String cmdlet takes text strings and returns custom objects (PSCustomObject) in which the text strings are values of object […]

  11. […] put them together. This command creates a custom object (I love PSCustomObject!) with the names and integer value of each enumerator […]