One thing that’s frequently frustrated me about PowerShell is that using the Compare-Object cmdlet doesn’t always yield useful results.
For instance, consider the following command:
Compare-Object (Get-ADUser UserA -Properties * ) ` (Get-ADUser UserB -Properties *) -IncludeEqual -ExcludeDifferent
This should return all the properties that are equal for these two users but it returns nothing when I know for a fact that many properties are identical. (Note: You need the Active Directory module loaded to call Get-ADUser.) Why doesn’t this work? Well, I’m not entirely sure but I believe the objects/properties don’t have meaningful comparison methods defined at the .Net level. As far as I can tell, out of the box, there is no meaningful way to use Compare-Object on many types of objects.
I considered that most of these objects do have structured output to the console and why not compare that? So, I proceeded to play around with the various cmdlets with Out- verbs. Out-String seemed the likely candidate but when I tried it I got no results from the following:
Compare-Object (Get-ADUser UserA -Properties * | Out-String ) `
(Get-ADUser UserB -Properties * | Out-String) -IncludeEqual -ExcludeDifferent
Other Out- cmdlets fail as well. So, I looked at what was generated by piping to Out-String:
PS C:\> Get-Member -InputObject (Get-ADUser UserA -Properties * | Out-String) TypeName: System.String
Hmm, PowerShell usually outputs multi-line strings as an array of strings with each element representing a single line. This spits out a string with line breaks in it, which Compare-Object can’t handle. Get-Help on Out-String yielded the following interesting information:
“By default, Out-String accumulates the strings and returns them as a single string, but you can use the stream parameter to direct Out-String to return one string at a time.”
So what about:
Compare-Object (Get-ADUser UserA -Properties * | Out-String -Stream) `
(Get-ADUser UserB -Properties * | Out-String -Stream) -IncludeEqual -ExcludeDifferent InputObject SideIndicator ----------- ------------- == == AccountExpirationDate : == accountExpires : 0 == AccountLockoutTime : == AccountNotDelegated : False == AllowReversiblePasswordEncryption : False == BadLogonCount : 0 ==
Aaand there we have it, useful results. But, that’s a lot of typing so, how about we generalize what we did there into a function? The Scripting Guys blog has been running a series on “splatting.” It’s a really neat feature that comes in handy for this kind of thing. Using some stuff I’ve learned from there and various other places, we can write a really nice, full featured and simple wrapper for Compare-Object like so:
function Compare-ObjectOutput { [CmdletBinding()] param ( [parameter( Position=0, Mandatory=$true )] [AllowEmptyCollection()] $ReferenceObject, [parameter( Position=1, Mandatory=$true, ValueFromPipeline=$true )] [AllowEmptyCollection()] $DifferenceObject, [int]$SyncWindow, [array]$Property, [switch]$ExcludeDifferent, [switch]$IncludeEqual, [string]$Culture, [switch]$CaseSensitive ) $newCompareParameters = @{} + $psBoundParameters if ($Property) { $newCompareParameters.Remove("Property") $newCompareParameters.ReferenceObject = $ReferenceObject | Select-Object -Property $Property | fl | Out-String -Stream $newCompareParameters.DifferenceObject = $DifferenceObject | Select-Object -Property $Property | fl | Out-String -Stream } else { $newCompareParameters.ReferenceObject = $ReferenceObject | fl | Out-String -Stream $newCompareParameters.DifferenceObject = $DifferenceObject | fl | Out-String -Stream } Compare-Object @newCompareParameters }
The function takes all of the same arguments as Compare-Object (except PassThru) and should work in most instances where Compare-Object fails. If the objects don’t support nice Format-List (fl) output, then something might go awry but most do.