Wednesday, July 7, 2021

Display-Object: a PowerShell utility Cmdlet

How do you list all the objects and values within a PowerShell object, investigate an object or explore its structure? There must be a simple way. I used to use ConvertTo-JSON. This is fine up to a point but what if you just wish to search for strings or look for objects with a cartain name or value? What if you need their path so you can reference them? It isn’t plain-sailing. In the ISE, the intellisense will help you a lot but I want more and I want to do it in script rather than the IDE

Let’s start with a silly example just to get in the mood. Imagine we have a hashtable that tells us our servers and users.

$ServersAndUsers =
@{'Shem' =
  @{
    'version' = '2017'; 'Location' = 'Server room';
        'Users'=@('Fred','Jane','Mo')
     }; 
  'Ham' =
  @{
    'version' = '2019'; 'Location' = 'Floor two rack';
        'Downtime'=$null
        'Users'=@('Fred','Jane',@{'TheDevopsTeam'=@('Joe','Tracy','Arthur')},'Phil','Tony')
  }; 
  'Japeth' =
  @{
    'version' = '2008'; 'Location' = 'basement rack';
        'Users'=@('Karen','Wyonna','Lilibet')
  }
}

If we just pipe it to Format-Table we get this, which is a good start, but those values are more tantalising than helpful

Name                           Value                                                                                     
----                           -----                                                                                     
Ham                            {Users, Downtime, version, Location}                                                      
Japeth                         {Users, version, Location}                                                                
Shem                           {Users, version, Location}

Getting the names of the base members isn’t that helpful. After all,  we can also get the names of the servers through Get-Member

[PSCustomObject]$ServersAndUsers | Get-Member -MemberType NoteProperty|Select Name
PS C:\Users\Phil> [PSCustomObject]$ServersAndUsers | Get-Member -MemberType NoteProperty|Select Name   
Name  
----  
Ham   
Japeth 
Shem     

… Or, knowing it is a hashtable, use …

$ServersAndUsers.keys
PS C:\Users\Phil> $ServersAndUsers.keys
Ham
Japeth
Shem

If we can assume that it is a hashtable, we can get name and value as we did with Format-Table

$ServersAndUsers | Foreach-Object -ov obj{ $_ } | Foreach{ $_.Keys } |
Foreach{
  [pscustomobject]@{ 'Path' = $_; 'Value' = $obj.($_) }
}
Path   Value                              
----   -----                              
Ham    {Users, Downtime, version, Location}
Japeth {Users, version, Location}         
Shem   {Users, version, Location}  

Well. That’s progress of a sort in that we have something we can work on, but of course, not every object will be a hashtable.  but we can elaborate it to list more types of object by turning a hashtable into a pscustomobject (Beware: the order of properties isn’t preserved). We’ll output a PSCustomObject as well so that we can output the results or do filters on them.

[pscustomObject]$ServersAndUsers | Foreach-Object -ov obj  { $_ } |
  gm -MemberType NoteProperty |
    Foreach{ [pscustomobject]@{ 
        'Path' = "$.$($_.Name)"; 
         'Value' = $obj.($_.Name) } }

We changed that hashtable into a PSCustomPbject so we could iterate through the names using the Noteproperties. We get those noteproperties via the Get-Member (gm) cmdlet. We added a $ Reference to represent whatever name you used for the variable that referred to your object.

the problem here is that this trick only works for a pscustomObject or something we can change into it, such as a hashtable or ordered dictionary. For other objects, we  use the property members rather than the NoteProperty members.

You can see that we need recursion because all the values are themselves objects of some description. Most data is more than a simple hashtable or pscustomObject.

function Display-Object ($TheObject, $Parent = '$')
{
        $MemberType = 'Property' #assume this for the time being
        $ObjType = $TheObject.GetType().Name;
        if ($ObjType -in 'Hashtable', 'OrderedDictionary')
        {
                $TheObject = [pscustomObject]$TheObject;
                $ObjType = 'PSCustomObject';
        }
        if ($ObjType -eq 'PSCustomObject')
        {
                $MemberType = 'NoteProperty'
        }
        
        $TheObject | gm -MemberType $MemberType |
        Foreach{
                Try { $child = $TheObject.($_.Name); }
                Catch { $Child = $null } # avoid crashing on write-only objects
                if ($child -eq $null -or #is the current child a value or a null?
                        $child.GetType().BaseType.Name -eq 'ValueType' -or
                        $child.GetType().Name -in @('String', 'Object[]'))
                {#output the value of this as a ps object
                        [pscustomobject]@{ 'Path' = "$Parent.$($_.Name)"; 'Value' = $Child; }
                }
                else #not a value but an object of some sort
                {
                        Display-Object -TheObject $child -Parent "$Parent.$($_.Name)"
                }
        }
}
Display-Object $ServersAndUsers
Path              Value                                              
----              -----                                              
$.Ham.Downtime                                                       
$.Ham.Location    Floor two rack                                     
$.Ham.Users       {Fred, Jane, System.Collections.Hashtable, Phil...}
$.Ham.version     2019                                               
$.Japeth.Location basement rack                                      
$.Japeth.Users    {Karen, Wyonna, Lilibet}                           
$.Japeth.version  2008                                               
$.Shem.Location   Server room                                        
$.Shem.Users      {Fred, Jane, Mo}                                   
$.Shem.version    2017               
   

 The problem here is that it doesn’t yet handle arrays that well.  We also need to allow the function to avoid a list of one or more names and allow it to work with a range of objects such as XML objects.  It is also wise to copy ConvertTo-JSON in the way that it specifies the allowable depth. As a final touch, we allow it to keep its cool when presented with write-only values.

<#
  .SYNOPSIS
    Displays an object's values and the 'dot' paths to them
  
  .DESCRIPTION
    A detailed description of the Display-Object function.
  
  .PARAMETER TheObject
    The object that you wish to display
  
  .PARAMETER depth
    the depth of recursion (keep it low!)
  
  .PARAMETER Avoid
    an array of names of pbjects or arrays you wish to avoid.
  
  .PARAMETER Parent
    For internal use, but you can specify the name of the variable
  
  .PARAMETER CurrentDepth
    For internal use
  
  .NOTES
    Additional information about the function.
#>
function Display-Object
{
  [CmdletBinding()]
  param
  (
    [Parameter(Mandatory = $true,
           ValueFromPipeline = $true)]
    $TheObject,
    [int]$depth = 5,
    [Object[]]$Avoid = @('#comment'),
    [string]$Parent = '$',
    [int]$CurrentDepth = 0
  )
  
  if ($CurrentDepth -ge $Depth) { return; } #prevent runaway recursion
  $ObjectTypeName = $TheObject.GetType().Name #find out what type it is
  if ($ObjectTypeName -in 'HashTable', 'OrderedDictionary')
  {
    #If you can, force it to be a PSCustomObject
    $TheObject = [pscustomObject]$TheObject;
    $ObjectTypeName = 'PSCustomObject'
  }
  if ($ObjectTypeName -ne 'Object[]') #not a system array.
  {
    # figure out where you get the names from
    if ($ObjectTypeName -in @('PSCustomObject'))
    # Name-Value pair properties created by Powershell 
    { $MemberType = 'NoteProperty' }
    else
    { $MemberType = 'Property' }
    #now go through the names     
    $TheObject |
    gm -MemberType $MemberType | where { $_.Name -notin $Avoid } |
    Foreach{
      Try { $child = $TheObject.($_.Name); }
      Catch { $Child = $null } # avoid crashing on write-only objects
      if ($child -eq $null -or #is the current child a value or a null?
        $child.GetType().BaseType.Name -eq 'ValueType' -or
        $child.GetType().Name -eq 'String')
      { [pscustomobject]@{ 'Path' = "$Parent.$($_.Name)"; 'Value' = $Child; } }
      
      else #not a value but an object of some sort
      {
        Display-Object -TheObject $child -depth $Depth -Avoid $Avoid -Parent "$Parent.$($_.Name)" `
                 -CurrentDepth ($currentDepth + 1)
      }
      
    }
  }
  else #it is an array
  {
    0..($TheObject.Count - 1) | Foreach{
      $child = $TheObject[$_];
      if (($child -eq $null) -or #is the current child a value or a null?
        ($child.GetType().BaseType.Name -eq 'ValueType') -or
        ($child.GetType().Name -eq 'String')) #if so display it 
      { [pscustomobject]@{ 'Path' = "$Parent[$_]"; 'Value' = "$($child)"; } }
      else #not a value but an object of some sort so do a recursive call
      {
        Display-Object -TheObject $child -depth $Depth -Avoid $Avoid -parent "$Parent[$_]" `
                 -CurrentDepth ($currentDepth + 1)
      }
      
    }
  }
}
Path                            Value        
----                            -----        
$.Ham.Downtime                               
$.Ham.Location                  Floor two rack
$.Ham.Users[0]                  Fred         
$.Ham.Users[1]                  Jane         
$.Ham.Users[2].TheDevopsTeam[0] Joe          
$.Ham.Users[2].TheDevopsTeam[1] Tracy        
$.Ham.Users[2].TheDevopsTeam[2] Arthur       
$.Ham.Users[3]                  Phil         
$.Ham.Users[4]                  Tony         
$.Ham.version                   2019         
$.Japeth.Location               basement rack
$.Japeth.Users[0]               Karen        
$.Japeth.Users[1]               Wyonna       
$.Japeth.Users[2]               Lilibet       
$.Japeth.version                2008         
$.Shem.Location                 Server room  
$.Shem.Users[0]                 Fred         
$.Shem.Users[1]                 Jane         
$.Shem.Users[2]                 Mo           
$.Shem.version                  2017  

Naturally, one can tinker with the values you return. Some people will want a ‘name/index’ column. I’ve kept it simple. I like to know the path because that tells me the structure and it provides the correct dot syntax to get the value from the object.

$ServersAndUsers.Shem.Location
Path           Value
----           -----
$.Ham.Users[4] Tony

You just substitute the name of the variable representing the objects for the $ symbol in the path.

You can filter what comes back. What server does Tony use?

Display-Object $ServersAndUsers | where {$_.Value -like '*Tony*' }
Path           Value
----           -----
$.Ham.Users[4] Tony

What are the server locations?

Display-Object $ServersAndUsers | where {$_.Path -like '*location*' }
Path              Value        
----              -----        
$.Ham.Location    Floor two rack
$.Japeth.Location basement rack
$.Shem.Location   Server room 

Let’s try a text file..

@'
Here lies John Bunn
who was killed by a gun
his name wasn't bun but Wood
'Wood' wouldn't rhyme with gun but 'Bunn' would

Anon
'@>"$env:Temp\Secondpoem.txt"
Display-Object (Get-Content "$env:Temp\Secondpoem.txt")
Path Value                                          
---- -----                                          
$[0] Here lies John Bunn                            
$[1] who was killed by a gun                        
$[2] his name wasn't bun but Wood                   
$[3] 'Wood' wouldn't rhyme with gun but 'Bunn' would
$[4]                                                
$[5] Anon

We can display other, meatier, objects such as the PowerShell process

Display-Object (get-process pwsh) -depth 2

(too much to display in a Blog!)

 

Once you can ‘walk’ through an object and search them some possibilities open up. You can slice and dice them; You can convert objects to markup or find out if they’ve changed and how. If you are in a DevOps Windows environment, you’re likely to have all sorts of objects that are delivered to you by cmdlets that monitor the servers. It is useful to be able to investigate them and quickly learn how to pull out just the data you need.  Hopefully, I’ll be able to show some of these in this series of Blog posts.

This function saved me a great deal of time when trying to get  the result of a regex match into a rational format, so I have a certain affection for it.

I’ve added the source to a collection of PowerShell utilities on my github repositories.

The post Display-Object: a PowerShell utility Cmdlet appeared first on Simple Talk.



from Simple Talk https://ift.tt/2SSxPbL
via

No comments:

Post a Comment