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.