Compare all properties of two objects in Windows PowerShell

I've always said that the Compare-Object cmdlet, which has been around forever in Windows PowerShell, is one of the most flexible and useful cmdlets out there. If you're an IT Professional of any kind, whether you work with Exchange Server or manage file servers, Compare-Object can make you look like a genius. In the past, I've used it for a number of random tasks, from comparing Active Directory group memberships to detecting changed files/folders on a file server.

The Compare-Object cmdlet really shines when you're comparing collections of like objects, and, although it allows you to also compare based on specific object properties, that sometimes leaves a little to be desired. Take for instance, the comparison of two Active Directory user objects. It is fairly common for an administrator to come across scenarios where you need to compare two Active Directory users and find the specific differences. Usually, you're troubleshooting something strange that happens for one user but not the other. Often times it may come down to how particular attributes have been configured on the user object. So, Compare-Object to the rescue, right? Not so fast.

[ps]
$ad1 = Get-ADUser amelia.mitchell -Properties *
$ad2 = Get-ADUser carolyn.quinn -Properties *
Compare-Object $ad1 $ad2
[/ps]

Running this code gives you the following output. Not exactly helpful. Of course, we know that the identity of the reference object and difference object will not be the same.

 
InputObject                     SideIndicator
-----------                        -------------
CN=carolyn.quinn,OU=Users,DC=contosocorp,DC=us     =>           
CN=amelia.mitchell,OU=Users,DC=contosocorp,DC=us    <=

To take the comparison one step further, you can use the -Property parameter. This allows you to compare one or more specific parameters. More helpful than the last example, but it assumes you know where the differences between the objects lie.

[ps]
$ad1 = Get-ADUser amelia.mitchell -Properties *
$ad2 = Get-ADUser carolyn.quinn -Properties *
Compare-Object $ad1 $ad2 -Property title,department
[/ps]

Okay, this is definitely more helpful. We can see by the output below that the user objects have different title and department attributes. However, we had to specify the attributes we wanted to compare. If we wanted to compare all attributes, this would be a very long command or a very manual process to compare each attribute.

 
title             department SideIndicator
-----             ---------- -------------
Finance Associate Finance    =>           
Accountant II     Accounting <=

So, with a little extra logic, we can do this pretty easily. First, we define the Compare-ObjectProperties function. That function will take any two source objects and get a unique list of all of the property names of both objects we're comparing. This is necessary because objects aren't always going to have the same set of attributes. When that is the case, we want to see where one has a null value and the other is populated. With the list of unique property names, our function can iteratively process them through Compare-Object and only return the properties that are different.

[ps]
Function Compare-ObjectProperties {
Param(
[PSObject]$ReferenceObject,
[PSObject]$DifferenceObject
)
$objprops = $ReferenceObject | Get-Member -MemberType Property,NoteProperty | % Name
$objprops += $DifferenceObject | Get-Member -MemberType Property,NoteProperty | % Name
$objprops = $objprops | Sort | Select -Unique
$diffs = @()
foreach ($objprop in $objprops) {
$diff = Compare-Object $ReferenceObject $DifferenceObject -Property $objprop
if ($diff) {
$diffprops = @{
PropertyName=$objprop
RefValue=($diff | ? {$_.SideIndicator -eq '<='} | % $($objprop))
DiffValue=($diff | ? {$_.SideIndicator -eq '=>'} | % $($objprop))
}
$diffs += New-Object PSObject -Property $diffprops
}
}
if ($diffs) {return ($diffs | Select PropertyName,RefValue,DiffValue)}
}

$ad1 = Get-ADUser amelia.mitchell -Properties *
$ad2 = Get-ADUser carolyn.quinn -Properties *
Compare-ObjectProperties $ad1 $ad2
[/ps]

Running the above code produces the following output. This is definitely what we're looking for.

 
PropertyName          RefValue                        DiffValue
------------           --------                        ---------
CanonicalName          contosocorp.us/Users/amelia.mitchell            contosocorp.us/Users/carolyn.quinn
CN                amelia.mitchell                     carolyn.quinn
Created                2/16/2017 9:31:12 AM                    2/16/2017 9:31:15 AM
createTimeStamp         2/16/2017 9:31:12 AM                    2/16/2017 9:31:15 AM
Department          Accounting                      Finance
DisplayName          Amelia Mitchell                     Carolyn Quinn
DistinguishedName      CN=amelia.mitchell,OU=Users,DC=contosocorp,DC=us    CN=carolyn.quinn,OU=Users,DC=contosocorp,DC=us
dSCorePropagationData     {2/16/2017 9:53:26 AM, 2/16/2017 9:31:12 AM,...     {2/16/2017 9:53:25 AM, 2/16/2017 9:31:15 AM, 12/31/1600 6:00:0...
employeeType           FullTime                        Contractor
GivenName         Amelia                          Carolyn
Manager              CN=christopher.walsh,OU=Users,DC=contosocorp,DC=us
MemberOf          {CN=Group8842,OU=Groups,DC=contosocorp,DC=us...     {CN=Group8896,OU=Groups,DC=contosocorp,DC=us...
Modified         4/24/2017 7:51:46 AM                    4/24/2017 7:51:56 AM
modifyTimeStamp         4/24/2017 7:51:46 AM                    4/24/2017 7:51:56 AM
Name                amelia.mitchell                     carolyn.quinn
ObjectGUID         e3436c96-ac59-4b0a-a2cd-cea586676089            27abcbb9-57f8-4e4d-8f87-a5ee0e3d6ddd
objectSid           S-1-5-21-827217070-865584383-1086049070-1208        S-1-5-21-827217070-865584383-1086049070-1249
Office              LON                         NYC
PasswordLastSet          2/16/2017 9:31:12 AM                    2/16/2017 9:31:15 AM
physicalDeliveryOfficeName  LON                         NYC
pwdLastSet           131317326721610425                  131317326757222285
SamAccountName            amelia.mitchell                     carolyn.quinn
SID                S-1-5-21-827217070-865584383-1086049070-1208        S-1-5-21-827217070-865584383-1086049070-1249
sn              Mitchell                        Quinn
Surname                Mitchell                        Quinn
Title              Accountant II                       Finance Associate
UserPrincipalName      amelia.mitchell@contosocorp.us              carolyn.quinn@contosocorp.us
uSNChanged          372797                          372798
uSNCreated            17224                           17511
whenChanged            4/24/2017 7:51:46 AM                    4/24/2017 7:51:56 AM
whenCreated         2/16/2017 9:31:12 AM                    2/16/2017 9:31:15 AM

One of the best things about this function is that you're not limited to comparing Active Directory user objects. You can use any two objects you want, and they don't even have to be the same type of object! Try for yourself using the example below, which compares a file object to a service. Granted, there probably aren't many use cases for such a scenario, but you never know. The capability is there nonetheless.

[ps]
$file = Get-Item -Path C:\Windows\System32\notepad.exe
$service = Get-Service -Name WinRM
Compare-ObjectProperties $file $service
[/ps]

Happy PowerShell'ing. See you next time.

Comments

  • Anonymous
    June 16, 2017
    Hi Jamie,Could you help expand this to compare each object within an array when the arrays are seperate lengths? I'm trying to compare file names and sizes on a local to a remote directory.Thanks in advance!
    • Anonymous
      June 16, 2017
      *different lengths
      • Anonymous
        June 16, 2017
        Hi Adam, I think in your case you should just be able to use the native Compare-Object cmdlet.$folder_remote = Get-ChildItem \myserver\myshare\myfolder$folder_local = Get-ChildItem C:\myfolderCompare-Object $folder_remote $folder_local -Property Name,LengthThen you would just have to process the output of Compare-Object according to whatever it is you're trying to accomplish.
  • Anonymous
    August 26, 2017
    Hi Jamie,This is beautiful stuff, thanks! This is exactly what i was looking for.I added the parameter 'IncludeEqual', because having a function that returns nothing kinda scares me... i like being able to debug and seeing all the results (like the original Compare-Object).Here's my update:Function Compare-ObjectProperties { Param( [PSObject]$ReferenceObject, [PSObject]$DifferenceObject, [Switch]$IncludeEqual ) $objprops = $ReferenceObject | Get-Member -MemberType Property,NoteProperty | % Name $objprops += $DifferenceObject | Get-Member -MemberType Property,NoteProperty | % Name $objprops = $objprops | Sort | Select -Unique $diffs = @() foreach ($objprop in $objprops) { $diff = Compare-Object $ReferenceObject $DifferenceObject -Property $objprop if ($diff) { $diffprops = @{ PropertyName=$objprop RefValue=($diff | ? {$_.SideIndicator -eq ''} | % $($objprop)) DiffIndicator='' } $diffs += New-Object PSObject -Property $diffprops } elseif ($IncludeEqual) { $diffprops = @{ PropertyName=$objprop RefValue=$ReferenceObject."$objprop" DiffValue=$DifferenceObject."$objprop" DiffIndicator='==' } $diffs += New-Object PSObject -Property $diffprops } } if ($diffs) {return ($diffs | Select PropertyName,RefValue,DiffValue,DiffIndicator)} }