Artykuł

Comparing two or more objects visually in PowerShell (cross-platform)

For the last few weeks I'm working on a small project, that should be released within next few weeks (it is open source so don't worry – you'll get to play with it). This project requires me to compare two or more objects and tell if those are equal and if those aren't to what degree. Of course, PowerShell offers built-in functionality via Compare-Object command. It's mighty but it leaves comparing differences, different properties to you. While there are probably other solutions that help users compare objects, I haven't found anything that would meet my requirements. After I've written Compare-MultipleObjects function, I thought it could be interesting to implement visual comparison – you know human-readable – and I had the perfect place to apply it.

Earlier resources for PSWriteHTML, Dashimo, Statusimo or Emailimo as PowerShell Modules

Did you guess it's going to be my favorite module? PSWriteHTML! This module has already been my go-to module for multiple projects and uses cases, so adding this functionality to it, seemed natural. If you don't know PSWriteHTML, Dashimo, Emailimo or Out-HTMLView, please read those articles below to understand how you can use its power to fulfill your goals.

Let's start shall we?

Comparing two or more objects with PSWriteHTML

Let's imagine we want to compare two objects. For this particular case, I'm going to use Active Directory, but any two or more objects should work.

When using standard Out-HTMLView without any commands, the output is pretty standard to what you would typically see.

Since Out-HTMLView has lots of features enabled by default you can of course export to Excel, PDF, CSV and generally play with those objects as you wish. Now let's see what happens if I use it with parameter Compare, which is one newly added parameters.

Get-ADUser -Filter * -Properties Modified, MemberOf, IsCriticalSystemObject | Select-Object -First 2 | Out-HtmlView -Compare

What you get is a comparison of two objects. It compares Properties, one by one and gives you Status with information whether two properties are the same or not. You can sort on it, filter and generally act as you want. It's useful, but I can hear you saying meh. Fair enough! Let's compare the 1st object with next four objects maybe?

Get-ADUser -Filter * -Properties Modified, MemberOf, IsCriticalSystemObject | Select-Object -First 5 | Out-HtmlView -Compare -ScrollX

Now, as you can see, I've also used ScrollX parameter. Comparing more objects makes it tricky to display on the same page quickly. That way Out-HTMLView added scroll so you can scroll to the right to see all objects that are compared. Of course, in this case, the Status column refers to matching all objects. In this case, the Source object is compared one by one with 1st, 2nd, 3rd, and 4th object. Useful right? Still meh? Yeah. Let's see one more switch, something that I expect using often, and something that you may want to use as well.

Get-ADUser -Filter * -Properties Modified, MemberOf, IsCriticalSystemObject | Select-Object -First 5 | Out-HtmlView -Compare -ScrollX -HighlightDifferences                                                                                                                         

What this HighlightDifferences parameter does? Magic :-)

What you see above was my end goal. I wanted to easily see what the differences between one or more objects are, and what I would have to do to that object to make it the same as the source one. HighlightDifferences switch marks all data that should be deleted with red, and all data that should be added with blue. Data that is the same stays black. As you can also see on the screenshot above, there are more pages available. Out-HTMLView has a limit of 15 rows, but you can either change it in the GUI or use other parameters that are available on that command such as DisablePaging or Simplify

Get-ADUser -Filter * -Properties Modified, MemberOf, IsCriticalSystemObject | Select-Object -First 5 | Out-HtmlView -Compare -ScrollX -HighlightDifferences -DisablePaging                                                                                                           

In case of Simplify, javascript functionality is removed, and you get everything in one go. It's not ideal, but it's useful, especially that this feature is also available in Emailimo – meaning you can compare objects in Emails if you wish so (and by comparing I mean display differences).

 Get-ADUser -Filter * -Properties Modified, MemberOf, IsCriticalSystemObject | Select-Object -First 5 | Out-HtmlView -Compare -HighlightDifferences -Simplify                                                                                                                

At this point, I could probably end my article and let you use this excellent functionality, but while walking out my dog, I thought that there would be fields that will be different by default and those will be giving false-positives along the way. For example, comparing multiple Domain Controllers, for sure their name will be different, their DNS name, ServicePrincipalName and many other fields will have values that will differ since all data in them stores their name as part of each field. While you could probably skim thru Name, DNSHostName (or use ExcludeProperty totally), but what if you wanted to see differences for ServicePrincipalName field? Easy? Let's try to compare all the properties of three domain controllers that I have in my test domain.

Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter * -Properties * | Select-Object -First 5 | Out-HtmlView -Compare -HighlightDifferences

As you can see above, it found a lot of differences, but if you look closely, the only difference between them is Computer Name. That's a bummer isn't it? The comparison was made, it found differences but it's not really helpful in case of ServicePrincipalName is it? Let's try this next feature in action, which is sponsored by Kulkozaurr!

Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter * -Properties * | Select-Object -First 5 | Out-HtmlView -Compare -HighlightDifferences {
    New-HTMLTableReplace -Replacements 'AD2', 'AD1'
    New-HTMLTableReplace -Replacements 'AD3', 'AD1'
}

See what I did there? I've asked for all my Domain Controllers. I know their name will be AD1, AD2, and AD3. I specifically told Out-HTMLView compare feature that I want AD2 to be replaced with AD1 and AD3 to be replaced with AD1. So for comparison purposes, those values were replaced, and the results are much more readable, showing only one difference between objects. I can see that there's only difference for RPC ServicePrincipalName, although again that's a GUID comparison (something that's expected) so I could add it as part of replacement as well and have no differences for that field at all. Notice how the displayed values didn't change, so you can still see original values. This is something you need to be aware of. I could probably add a feature to display replaced values but thought this would be even more confusing.

Out-HTMLView is New-HTMLTable equivalent

Now, Out-HtmlView is just New-HTMLTable on steroids, and that means Compare and HighlightDifferences are both added as part of New-HTMLTable. Of course, that also means that it's available as part of Dashimo (Table -Compare -HighlightDifferences) or Emailimo (EmailTable -Compare -HighlightDifferences). Same feature working over different products and use cases.

Import-Module PSWriteHTML -Force

$Objects1 = @(
    [PSCustomobject] @{ Test = 'My value'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 2'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test2 = 'My value1', 'my value 3' }
)

$Objects2 = @(
    Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter * #-Properties *
)

$Objects3 = @(
    @{ Test = 'My value'; Test1 = 'My value1' }
    @{ Test = 'My value 2'; Test1 = 'My value1' }
    @{ Test = 'My value 3'; Test1 = 'My value1' }
    @{ Test = 'My value 3'; Test2 = 'My value1', 'my value 3' }
)

New-HTML -TitleText $Title -UseCssLinks:$true -UseJavaScriptLinks:$true -FilePath $PSScriptRoot\Example15.html {
    New-HTMLSection -HeaderText 'Comparing Objet with Highlighting Differences (PSCustomObject)' {
        New-HTMLTable -DataTable $Objects1 -Compare -AllProperties -HighlightDifferences
    }
    New-HTMLSection -HeaderText 'Comparing Objet with Highlighting Differences (Domain Controllers)' {
        New-HTMLTable -DataTable $Objects2 -Compare -AllProperties -HighlightDifferences
    }
    New-HTMLSection -HeaderText 'Comparing Objet with Highlighting Differences (Hashtables)' {
        New-HTMLTable -DataTable $Objects3 -Compare -AllProperties -HighlightDifferences
    }
} -ShowHTML

You can easily mix and match it with other functionality. Standard table data, next to table with comparison results, next to a chart. Your choice.

Import-Module PSWriteHTML -Force

$Objects1 = @(
    [PSCustomobject] @{ Test = 'My value'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 2'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test2 = 'My value1', 'my value 3' }
)

$Objects2 = @(
    Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter * #-Properties *
)

$Objects3 = @(
    @{ Test = 'My value'; Test1 = 'My value1' }
    @{ Test = 'My value 2'; Test1 = 'My value1' }
    @{ Test = 'My value 3'; Test1 = 'My value1' }
    @{ Test = 'My value 3'; Test2 = 'My value1', 'my value 3' }
)

New-HTML -TitleText $Title -UseCssLinks:$true -UseJavaScriptLinks:$true -FilePath $PSScriptRoot\Example-Comparing02.html {
    New-HTMLSection -HeaderText 'Comparing Objet with Highlighting Differences (PSCustomObject)' {
        New-HTMLTable -DataTable $Objects1
        New-HTMLTable -DataTable $Objects1 -Compare -AllProperties -HighlightDifferences
        New-HTMLPanel  {a
            New-HTMLChart {
                New-ChartBar -Name 'Test' -Value 1
                New-ChartBar -Name 'Test1' -Value 2
                New-ChartBar -Name 'Test2' -Value 3
            }
        }
    }
    New-HTMLSection -HeaderText 'Comparing Objet with Highlighting Differences (Domain Controllers)' {
        New-HTMLTable -DataTable $Objects2
        New-HTMLTable -DataTable $Objects2 -Compare -AllProperties -HighlightDifferences
    }
    New-HTMLSection -HeaderText 'Comparing Objet with Highlighting Differences (Hashtables)' {
        New-HTMLTable -DataTable $Objects3
        New-HTMLTable -DataTable $Objects3 -Compare -AllProperties -HighlightDifferences
    }
} -ShowHTML

Nice right? I'm not sure if you've noticed, but there's one more parameter that I was using in New-HTMLTable, and that is AllProperties switch. This switch is available as part of Out-HtmlView as well. Its purpose is to allow all properties of all objects to be displayed. It's not always necessary, and it impacts speed, but it comes in handy. If you have ever worked with Active Directory, Exchange or Office 365, you probably noticed that not each object returned by those systems has all the same properties.

$AllUsers = Get-ADUser -Filter * -Properties *
$PropertiesCount = foreach ($User in $AllUsers) {
    $User.PSObject.Properties.Name.Count
}
$PropertiesCount -join ','

In my small domain following code returns

145,111,111,115,114,118,113,114,112,111,127,146,150,147,151,152,151,114,118,155,155,152,151,152,151,151,151,151,152,151,155,154,154,146,146,151,151,151,151,151,151,151,119,108,117,114,111,111,113

That shows that each object in Active Directory can have different properties. Why does it matter? It doesn't matter if you work with those objects in PowerShell and doing manipulation on those objects. Things get tricky if you try to display them with Format-Table, convert to CSV, Excel or HTML. You see, if you don't account for different properties per object (which is not that uncommon), most conversions are taking properties from 1st object and go with that for the rest of objects in an array losing all additional properties in the process. While Active Directory example is a bit extream because there's no way you would want to see 145 properties with Format-Table in PowerShell, but the following example can show you the problem I'm talking about.

$Objects1 = @(
    [PSCustomobject] @{ Test = 'My value'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 2'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test2 = 'My value1, my value 3' }
)

$Objects1 | Format-Table

As you can see, the fourth object lost Test2 property, and Format-Table skipped it. While it may not be something that happens very often, it does happen. And if you work with AD, Exchange or Office 365 it happens more often then you think. So what AllProperties switch does? Well, it tells New-HTMLTable to scan all objects first, take all of their properties, and start adding missing ones to the first object. That way, the first object in an Array suddenly has all unique properties from all objects, and we take it from there.

$Objects1 = @(
    [PSCustomobject] @{ Test = 'My value'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 2'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test2 = 'My value1, my value 3' }
)

New-HTML -TitleText $Title -UseCssLinks:$true -UseJavaScriptLinks:$true -FilePath $PSScriptRoot\Example-Comparing02.html {
    New-HTMLSection -HeaderText 'Comparing Object without all properties(PSCustomObject)' {
        New-HTMLTable -DataTable $Objects1
    }
    New-HTMLSection -HeaderText 'Comparing Object with all properties' {
        New-HTMLTable -DataTable $Objects1 -AllProperties
    }
} -ShowHTML

In the example above, you can see AllProperties switch made sure all properties are visible. There is a small performance impact because we first need to loop once and scan for all property names before we can process table, but for smaller datasets, this time should not be noticeable. This, of course, applies to EmailTable, Out-HtmlView.

Out-HTMLView supports all the goodies New-HTMLTable does

While Out-HTMLView is made to be used in ad-hoc situations, sometimes you may want to spice it up and not bother with setting up PSWriteHTML or Dashimo. I've made sure that you can apply the same conditions, the same styling of headers, or content as you can do with New-HTMLTable or EmailTable. This means you can easily highlight certain parts of your comparisons or display without much effort on your side. Following example shows usage of New-HTMLTableCondition to highlight column Status data to Red if it's set to False. The idea is precisely the same as you saw with New-HTMLTableReplace, but you get to play with all the features that are currently implemented as part of PSWriteHTML.

$Objects1 = @(
    [PSCustomobject] @{ Test = 'My value'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 2'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test1 = 'My value1' }
    [PSCustomobject] @{ Test = 'My value 3'; Test2 = 'My value1', 'my value 3' }
)

$Objects2 = @(
    Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter * -Properties *
)

$Objects1 | Out-HtmlView -Compare -HighlightDifferences -AllProperties {
    New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value $false -BackgroundColor Red -Color White
}
$Objects2 | Out-HtmlView -Compare -HighlightDifferences  {
    New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value $false -BackgroundColor Red -Color White
}

You can, of course, have multiple table conditions for various columns. Here's another example with three conditions. It makes Status column content Green or Red and at the same time highlights the whole row if the value in column Name will be AllowReversiblePasswordEncryption.
$Objects2 = @(
    Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter * -Properties *
)
$Objects2 | Out-HtmlView -Compare -HighlightDifferences  {
    New-HTMLTableCondition -Name 'Name' -Value 'AllowReversiblePasswordEncryption' -BackgroundColor Gold -Row
    New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value $false -BackgroundColor Red -Color White
    New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value $true -BackgroundColor Green -Color White
}

Here's another example, that has multiple conditions and also shows replacements in action

Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter * -Properties * | Select-Object -First 5 | Out-HtmlView -Compare -HighlightDifferences {
    New-HTMLTableReplace -Replacements 'AD2', 'AD1'
    New-HTMLTableReplace -Replacements 'AD3', 'AD1'
    New-HTMLTableReplace -Replacements 'AD4', 'AD1'
    New-HTMLTableCondition -Name 'Name' -Value 'ServicePrincipalName' -BackgroundColor Gold -Row
    New-HTMLTableCondition -Name 'Name' -Value 'CanonicalName' -BackgroundColor Gold -Row
    New-HTMLTableCondition -Name 'Name' -Value 'accountExpires' -BackgroundColor Gold -Row
    New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value $false -BackgroundColor Red -Color White
    New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value $true -BackgroundColor Green -Color White
}

Cool right? If you want to find out what options are available to make sure to read earlier articles. If you're the first time hearing about PSWriteHTML / Dashimo or Emailimo you're missing out. And it's not just a marketing slogan! I did spend a lot of time polishing those modules making sure you can build HTML quickly and efficiently. Compare feature is only one of those cool features with a real practical use case.

Compare-MultipleObjects in PowerShell Console

All that comparing functionality in PSWriteHTML is based on a function I wrote for this purpose Compare-MultipleObjects. Its usage is pretty simple, and you can see a couple of options below

$DomainControllers = Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter *                                                                                              $DomainControllers | Ft -a
Compare-MultipleObjects -Objects $DomainControllers -Summary -FormatOutput -FormatDifferences | ft -a 
Compare-MultipleObjects -Objects $DomainControllers -Summary -FormatOutput -FormatDifferences -Replace @( @{'' = 'AD1','AD2'}; @{'' = 'AD2','AD3'}) | ft -a 

Most complicated syntax is for replacements. You need to pass an Array of Hashtables for it to work. The way it works is that you provide Key and Replacement values. For example if you want to do replacements in DNSHostName you would define Key for Hashtables. However if you leave hashtable key empty it will do replacements to every single property/object it finds. In our example above key for both hashes in an array is empty therefore the replacement is done on all properties.

Since I'm using Format-Table in examples above some stuff is not shown on screen and is truncated. Compare-MultipleObject function has more fields. For each compared object you get Add, Remove, Same properties. You can see those without Format-Table in use.

Compare-MultipleObjects -Objects $DomainControllers -Summary -FormatOutput -FormatDifferences

Let's see another example using the Replace method. In this example, I'm going to limit replacement to only DNSHostName and leave everything else untouched.

$DomainControllers = Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter *
$Replace = @(
    @{ 'DNSHostName' = 'AD2', 'AD1' }
    @{ 'DNSHostName' = 'AD3', 'AD1' }
    #@{ '' = 'AD2', 'AD1' }
    #@{ '' = 'AD3', 'AD1' }
)
Compare-MultipleObjects -Objects $DomainControllers -Summary -FormatOutput -FormatDifferences -Replace $Replace |ft -a

Notice, how I'm using FormatOutput and FormatDifferences switches. This is because I want to have a nicely formatted output for visual comparison. You can, of course, skip those switches or give your own Splitter/Joiner. Have a look at examples below

$DomainControllers = Get-ADComputer -SearchBase 'OU=Domain Controllers,DC=ad,DC=evotec,DC=xyz' -Filter *
$Replace = @(
    @{ 'DNSHostName' = 'AD2', 'AD1' }
    @{ 'DNSHostName' = 'AD3', 'AD1' }
    #@{ '' = 'AD2', 'AD1' }
    #@{ '' = 'AD3', 'AD1' }
)
Compare-MultipleObjects -Objects $DomainControllers -Summary -Replace $Replace |ft -a   
Compare-MultipleObjects -Objects $DomainControllers -Summary -Replace $Replace -Splitter "`r`n" -FormatOutput -FormatDifferences |ft -a 
Compare-MultipleObjects -Objects $DomainControllers -Summary -Replace $Replace -FormatOutput -FormatDifferences |ft -a     

Comparison use cases

While my focus on this blog post was strictly on Active Directory, Out-HTMLView -Compare, and New-HTMLTable have no problems working with other objects. Let's try and compare my test virtual machines running on my Windows 10 machine, shall we?

get-vm | Out-HtmlView -Compare -HighlightDifferences

How about comparing your disk drives?

Get-Disk | Out-HtmlView -Compare -HighlightDifferences  

Let's try to compare two groups and their members?

$Group = @(
    Get-ADGroup 'Domain Admins' -Properties Member,MemberOf
    Get-ADGroup 'GDS-TestGroup3' -Properties Member,MemberOf
)
$Group | Out-HtmlView -Compare -HighlightDifferences

It should work on all objects, whether it's Azure, Office 365, or some other stuff that you need to compare – it should be supported. If you get in trouble, or something doesn't work, please let me know by opening an issue on GitHub. I can see myself using this on migrations projects, comparing source tenant data to target tenant data, one Active Directory to another, and so on. Hope it's as useful for you!

Using PSWriteHTML, Dashimo, Statusimo or Emailimo as PowerShell Module

For easy use and installation, all modules are available from PowerShellGallery. Installing it is as easy as it gets. Keep in mind that when you install Emailimo, you get PSWriteHTML installed by default, so you don't have to install it separately. Same goes for Dashimo.

Install-Module PSWriteHTML -AllowClobber -Force # (covers Out-HtmlVIew)
Install-Module Dashimo -AllowClobber -Force
Install-Module Emailimo -AllowClobber -Force

Code as always is stored on GitHub and is free to use and take.

Tags: , , , , , ,

This is a unique website which will require a more modern browser to work! Please upgrade today!