Recently, I was chatting with my friend Gladys Kravitz about the hassle of comparing objects in PowerShell. Even after all these years. She has a specific use case, but you might also feel the need for a better comparison option. And to be clear, the comparison we're talking about is not the object's values, as you might see with Compare-Object. But rather the property names.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
In Gladys' situation, she imports data from CSV files which she processes through her scripts to get things done. Her use-case is to compare property names. Does this CSV file have the same property names as that CSV file? Are there extra properties? Or is something missing? So let's see how we might address this.
Think Objects
Even though we're starting with CSV files, we're really talking about objects. Gladys can import her CSV files and turn them into custom objects. You might be creating objects to compare through some other mechanism. The bottom line is that we should look at this as an object comparison problem, not a file problem.
Let me start by generating some sample data.
$csvA = @"
samantha,darren-1,darren-2,tabitha,esmerelda
1,2,3,4,5
2,4,6,8,10
3,6,9,11,12
11,22,33,44,55
"@
$csvB = @"
samantha,darren-1,darren-2,esmerelda,gladys,agatha
1,2,3,4,0,9
2,4,6,8,1,8
3,6,9,11,8,0
11,22,33,55,66,77
"@
$a = $csvA | ConvertFrom-Csv
$b = $csvB | ConvertFrom-Csv
For my purposes $A is the reference object and the $B is the difference object. In looking at the data you can see the $B is missing "tabitha" and has extra properties "agatha" and "gladys". I don't care about the values. We even don't really care about the order of property names because one we have an object we're going to reference by property name, not its position in the CSV header.
PSObject
To compare object property names, we don't need the entire object. All we need is one item to analyze because the property names are the same for objects. PowerShell goes to a lot of effort to make it easy to use for IT Pros. PowerShell hides a lot of the .NET sausage-making so you only have to work with the final results. But sometimes, we need to get our hands dirty. In this case, we're going to use a ubiquitous property called PSObject.
We're going to use the Properties property. Here's what it looks like.
It is easy then to create a list of property names for the reference and difference objects.
$refProp = $a[0].psobject.properties.name | Sort-Object
$diffProp = $b[0].psobject.properties.name | Sort-Object
Then I can go through each list and see what properties are missing or extra.
#find extra properties in the difference object
foreach ($name in $diffProp) {
if ($refProp -notcontains $name) {
$name
}
} #foreach
#find missing reference properties from the difference object
foreach ($name in $refProp) {
if ($diffProp -notcontains $name) {
$name
}
} #foreach
Of course, I want to create a meaningful result so that Gladys can tell at a glance how things compare.
Compare-PropertyName
Here's the PowerShell function I wrote.
Function Compare-PropertyName {
[cmdletbinding(DefaultParameterSetName="default")]
[alias("cpn")]
[Outputtype("PSPropertyNameDifference","string","boolean")]
Param(
[Parameter(Position = 0, Mandatory)]
[ValidateNotNullOrEmpty()]
[object]$Reference,
[Parameter(Position = 1, Mandatory)]
[ValidateNotNullOrEmpty()]
[object]$Difference,
[Parameter(HelpMessage = "Indicate if property names match with a simple True/False.",ParameterSetName="quiet")]
[switch]$Quiet,
[Parameter(HelpMessage = "Only show missing property names in the difference object.",ParameterSetName="missing")]
[switch]$MissingOnly,
[Parameter(HelpMessage = "Only show extra property names in the difference object.",ParameterSetName="extra")]
[switch]$ExtraOnly
)
#get property names from the first item
$refProp = $Reference[0].psobject.properties.name | Sort-Object
Write-Verbose "Found $(($refProp).count) reference properties"
$diffProp = $Difference[0].psobject.properties.name | Sort-Object
Write-Verbose "Found $(($diffProp). count) difference properties"
$missing = [System.Collections.Generic.List[string]]::new()
$extra = [System.Collections.Generic.List[string]]::new()
#find extra properties in the difference object
foreach ($name in $diffProp) {
if ($refProp -notcontains $name) {
Write-Verbose "$Name not found in the reference object"
$extra.Add( $name)
}
} #foreach
#find missing reference properties from the difference object
foreach ($name in $refProp) {
if ($diffProp -notcontains $name) {
Write-Verbose "$Name not found in the difference object"
$missing.Add( $name)
}
} #foreach
#create a custom object for the reseult
$result = [pscustomobject]@{
PSTypename = "PSPropertyNameDifference"
Missing = $missing
Extra = $Extra
ReferenceProperties = $refProp
DifferenceProperties = $diffProp
ReferenceCount = $refProp.count
DifferenceCount = $diffProp.count
}
switch ($pscmdlet.ParameterSetName) {
"Quiet" {
if ($result.missing.count -eq 0 -AND $result.extra.count -eq 0) {
$true
}
else {
$false
}
}
"Missing" {
$result.Missing
}
"Extra" {
$result.Extra
}
Default {
$result
}
} #switch
} #close function
The function generates one type of object by default.
But I also wanted to offer alternatives that would give Gladys information she might want quickly.
The results are all relative to the difference object. The difference object, $b, is missing the property "tabitha", for example.
I'd love to hear how this works for you. I think this will make a good addition to the PSScriptTools module so look for an update in the next few weeks.
compare-object $a[0].psobject.properties.name $b[0].psobject.properties.name
Sure. That will work. My initial ideas included this very technique. But I’m not a big fan of how Compare-Object works and I don’t think the output is very automation friendly. I also wanted to simplify the process, or abstract it, as much as possible. That’s where functions come in.
This is very helpful, thanks. But a bigger comparing challenge is with deep objects. Say a car object has three properties: Make and Model, both strings, and an Engine that is an object by itself with a NumOfCylinder property.
What is the best way to deep compare two cars? Ideally without knowing in advance the type structure and the properties names.
Comparing objects is harder, especially as they get more complex. I find Compare-Object ok with simple comparisons, although I don’t care for the output. I’m thinking of some ways to write my own object comparison command.
I also think that with any type of comparison, you have to know something about the object. That’s where Get-Member comes into the picture.
Hi Jeff, this is really helpful thanks. I had no idea that all objects had PSObject. This has greatly simplified the code I need for a task as I can dynamically create XML elements required based on the object being passed through the pipeline. I was using the Get-Member way of doing things, but that seems to present the property names in alphabetical order, which is not what I wanted. Your shared knowledge here solves that, thank you!
Changed the missing and extra variables into arrays on PowerShell 5.1:
$missing = @()
$extra = @()
And in stead of Add()-ing to them I just used +=.
The [System.Collections.Generic.List[string]]::new() were not found in my version of PowerShell but got it to work with the above changes.
Hope that helps others.
I used [System.Collections.Generic.List[string]] because technically it performs better than using arrays which must be recreated every time you change them. It is interesting that even though you are running PowerShell 5.1 you don’t have this .NET class. You must be running an older version of Windows or the .NET Framework.