I came across this forum question on the Hey, Scripting Guy! forum the other day and couldn’t help but take up the challenge of finding out and providing (hopefully) a good explanation as to why the following code snippet did not work in the way the poster was thinking it should work:
$test1 = get-service -Name W32Time Stop-service w32time $test2 = get-service -Name W32Time compare-object -referenceobject $test1 -differenceobject $test2
The expected output was something showing that there was a change with the W32Time service. However, that was not the case and in fact, nothing was returned giving the illusion that nothing was different between these two objects.
If you run the Compare-Object command this way using the –Property parameter and listing Name and Status, it works just fine.
compare-object -referenceobject $test1 -differenceobject $test2 ` -Property Name,Status
So, why doesn’t the other way work?
I believe this is by design. When you do not specify a property to compare between objects, it decides on its own what it will compare and show the results accordingly. Based on some research and experimenting, I can see that it actually converts the objects to a string and then continues comparing the string objects if no properties are given.
I am going to plug in the Compare-Object code snippet into Trace-Command and take a look at the output to see if I can determine what the issue is. Trace-Command is a cool low-level trace tool that is available with PowerShell to track down issues with using commands, much like I am doing here.
Start doing some tracing
In order to get an idea about what is different, I will be using both versions of the Compare-Object code with and without the –Property parameter.
First using the –Property parameter
trace-command ETS {compare-object -referenceobject $test1 ` -differenceobject $test2 –Property Name,Status} –pshost
Looking at the resulting output, it appears that it is performing the compare without any issues.
Now, without the –Property parameter
trace-command ETS {compare-object -referenceobject $test1 ` -differenceobject $test2} –pshost
Well, that is interesting, it says that the current object is not comparable. Note how it takes the object and converts it into a string format System.ServiceProcess.ServiceController prior to starting the compare similar to this:
$test1.ToString() $test2.ToString()
Based on this, Compare-Object sees no difference in this because both string objects are now the same name, therefore nothing is returned after running the command. Think of it as something like this:
Can you spot the difference?
Now if there was an added service or one removed, it would show up in the compare-object as being different, but it would be pretty hard to figure out which one was different.
Lets look at one other example, this time using Get-Process just to see how using Compare-Object can work in a different case.
This time I am going to get a baseline of the current processes and then start notepad up and take another snapshot of the current processes and perform a comparison of each collection.
$processes_before = get-process notepad $processes_after = get-process compare-object -referenceobject $processes_before ` -differenceobject $processes_after
But wait, how is this possible? Shouldn’t it be the same way for the process collection as it was with the Services collection? Well, yes and no. The objects are converted to a string format much like the Service object, but in this case there is more to the string object this time around than just the name of the object.
$processes_before[0].ToString() $processes_after[0].ToString()
As you can see, not only does it list the process object, but also the name of the process. Because of this,there will be a notepad object in one collection and not in the other collection.
Just for fun, lets run both this command along with a command using the –Property parameter just to see what we get.
First using the –Property parameter
trace-command ETS {compare-object -referenceobject $ processes_before ` -differenceobject $processes_after –Property Name,ID} –pshost
I chose this these properties on purpose as they are static and will not change unless I actually remove the process. The output is pretty large, but if you run it you can see that it has no problem comparing these properties specified.
Now, without the –Property parameter
trace-command ETS {compare-object -referenceobject $processes_before ` -differenceobject $processes_after} –pshost
Here is again will see that it cannot compare this object as is and will convert it to a string prior to comparison. But in this case, the object has the one thing that the service object did not, a name with it that can provide a more accurate comparison of the object. Keep in mind that this only works as designed in this instance when you are only caring about tracking processes by count (baseline of running processes compared to another baseline performed later).
This will not work in other cases where you want to see changes in handles or other properties without the use of the –Property parameter.
Getting back to the services
Ok, so how can we use Compare-Object without specifying the –Property parameter to see if a service had stopped since the last baseline? Well, there is one way that I found that seems to work rather well, but still has some manual checking after a difference is found. For this you have to use the ConvertTo-CSV command to make this work out.
$test1 = get-service -Name W32Time | ConvertTo-Csv Stop-service w32time $test2 = get-service -Name W32Time | ConvertTo-Csv Compare-Object -ref $test1 -diff $test2
You will have to do some manual inspection to validate what that difference is which was registered. Of course, you could also write some code to automate this process to find the difference.
ConvertFrom-Csv $test1 ConvertFrom-Csv $test2
In this case, we can see that the service was stopped after the initial baseline.
This all works because the string object is different between the baseline and the run afterwards.
Not exactly pleasant to the eyes, but it does work when using Compare-Object.
I did write a small code snippet that performs a more automated approach to this but I must stress that it is not a finished product in many ways. This is merely showing one example at achieving a more automated way of reviewing which property (or properties) have changed.
$t1 = ConvertFrom-Csv $test1 $t2 = ConvertFrom-Csv $test2 $t1 | gm -type noteproperty | Select -Expand Name | ForEach { If (-NOT ($t1.$_ -eq $t2.$_)) { New-Object PSObject -Property @{ ServiceName = $t1.Name PropertyName = $_ PreviousState = $t1.$_ CurrentState = $t2.$_ } } }
There you have it, a quick and dirty check to locate the changed properties.
So, the bottom line is if you want to compare the properties of two objects, you should use the –Property parameter to get the most accurate results.
Thanks! Great post! I was wondering what I was doing wrong with my compares…
Hi,
Very nice post!
I came across it while searching for the answer “What’s wrong with Compare-Object?”
However, I found Comparing txt files to be very convenient:
PS> $userDisabled = Get-ADObject -Identity (get-mailbox -Filter {Name -like “userdisable
*”}).DistinguishedName -Properties *
PS> $userDisabled = Get-ADObject -Identity (get-mailbox -Filter {Name -like “userdisable
*”}).DistinguishedName -Properties *
PS C:\Users\ysamorodov-a\Documents> Compare-Object -ReferenceObject (Get-Content .\userEnabled.txt) -DifferenceObject (Get-Content .\userDisabled.txt)
Kind regards,
Yuriy
I agree! Comparing txt files can be very nice using Compare-Object. Thanks for you comment and checking out my blog!
Pingback: Powershell: Tipps & Tricks Using Compare-Object « MS Tech BLOG
A very informative post.
When I encountered this ToString default behavior for complex objects before, I overwrote the method. That would also work in a compare I suppose.
$services = Get-Service | Add-Member -Force -Name ToString -MemberType ScriptMethod `
-PassThru -Value {$this.DisplayName + ‘ ‘ + $this.Status}
Agreed, you still have to specify the properties you want to see in the code block of the new ToString method.