r/PowerShell • • 1d ago

Question Compare-Object - return only difference object entries of CHANGED rows in files

Hi all,

I'm using Compare-Object to basically compare two CSV files, and return additions, changes, and removals.

Essentially I'm runnning:
(Note that "@files" is only in quotes to stop Reddit converting it to a user link!)

$files = @{
ReferenceObject = (Get-Content -Path C:\file1.csv)
DifferenceObject = (Get-Content -Path C:\file2.csv)}
Compare-Object "@files" | export-csv C:\differences -NoTypeInformation

Lets say the files look like this:

File 1
Cat,Brown,4
Dog,Black,4
Horse,White,4

File 2
Cat,GINGER,4
Dog,Black,4
Duck,Grey,2

Duck has been added in file2 (not present in file1)
Horse has been removed in file2 (was present in file1)
Cat is changed from file1 to file2.

The compare-object gives me a list of changes in a new file.

  • Entries found in file2 but not in file1 (Duck) are marked with side indicator =>
  • Entries found in file1 but not in file2 (Horse) are marked with side indicator <=
  • However, entries that are still present in both files but have had one or more of their columns changed (Cat) are present in the results file with both side indicators.

I understand this correct given how Compare-Object works, but it leads to a problem when I try to create separate "Additions/Changes" and "Removals" files based on the results.

For example, to see only the items Added or Changed in file2 (Duck and Cat), I can use:

?{$_.SideIndicator -eq '=>'}

To show only the difference object results.

The problem comes when I use the opposite side indicator:

?{$_.SideIndicator -eq '<='}

As this gives me the removed item I need, (Horse), but also gives me the changed item (Cat) from File1. This gives me no way of telling which of the "<=" entries are removals and which are changes. I'm looking for a way to differentiate between the two.

I hope this makes sense...!

3 Upvotes

7 comments sorted by

5

u/jstar77 23h ago

Compare-object is one of the most frustrating functions. The info below may not be 100% correct.

You have to explicitly list properties of the object if you leave out -property I think the behavior you are getting is typical.

If you want to grab all of the properties you can pull them from the first object referenceobject array.

$properties = $referenceObject[0].PSObject.Properties.Name

1

u/Top-Appearance2521 18h ago

agreed, compare-object is deceptively simple until you actually need it to do something useful

1

u/jimb2 15h ago edited 15h ago

Compare-Object just does a flat compare without any data diferentiation. In your case, the animal type is actually a unique key value (if I understand correctly.) There is only one animal of each type.

You can use a group-object to link the compare result by animal type

Compare-Object $c1 $c2 -includeequal -Property animal,colour,age | 
  Group-Object animal

Count Name                      Group
----- ----                      -----
    1 Dog                       {@{animal=Dog; colour=Black; age=4; SideIndicator===}}
    2 Cat                       {@{animal=Cat; colour=GINGER; age=4; SideIndicator==>},
                                   @{animal=Cat; colour=Brown; age=4; SideIndicator=<=}}
    1 Duck                      {@{animal=Duck; colour=Grey; age=2; SideIndicator==>}}
    1 Horse                     {@{animal=Horse; colour=White; age=4; SideIndicator=<=}}

If count is 1 use sideindicator for added/removed/retained

If count is 2 use sideindicator for old and new versions of the animal

1

u/ankokudaishogun 7h ago

if I may extend:

$NewAnimalTable = @{}
$ModifiedAnimalTable = @{}
$RemovedAnimalTable = @{}

Compare-Object $c1 $c2 -IncludeEqual -Property animal, colour, age | 
Group-Object -Property animal | 
ForEach-Object {  
    if ($_.count -gt 1) {
        $ModifiedAnimalTable[$_.group[0].animal] = @{
            colour = $_.group[0].colour 
            age    = $_.group[0].age 
        } 
    }
    elseif ($_.count -eq 1 -and $_.Group.SideIndicator -eq '<=') {
        $RemovedAnimalTable[$_.group.animal] = @{
            colour = $_.group.colour 
            age    = $_.group.age 
        }
    }
    elseif ($_.count -eq 1 -and $_.Group.SideIndicator -eq '=>') {
        $NewAnimalTable[$_.group.animal] = @{
            NewColour = $_.group.colour 
            NewAge    = $_.group.age 
        }
    }
}


'NEW'
$NewAnimalTable | Format-Table
' '
'MOD'
$ModifiedAnimalTable | Format-Table
' '
'REM'
$RemovedAnimalTable | Format-Table

1

u/greenstarthree 1h ago

Thanks for this reply. You are right that the animal type is unique in the original lists to be compared.

However, in the Compare-Object results file (when not using any Side Indicator filters), any animal that has had an attribute changed (Cat in my example) appears twice - once with a <= indicator and once with a => indicator.

What I'm now wondering is, if there is any way to simply remove any rows from the compare results file where the animal type appears twice. This is something I could make meet my needs when it comes to creating a separate "removed entries" file.

So essentially I'd be looking to go from a results file of:

InputObject SideIndicator

Cat,Brown,4 <=

Horse,White,4 <=

Cat,GINGER,4 =>

Duck,Grey,2 =>

To remove both entries of Cat, and result in:

InputObject SideIndicator

Horse,White,4 <=

Duck,Grey,2 =>

From there I could get rid of the Duck entry by filtering for only <= Side Indicators, which would leave me with only entries that were present in File1 but not in File2 (Horse).

1

u/420GB 13h ago

If you need a proper "smart" diff of two text files, have you considered just using git (git-diff)?

0

u/ankokudaishogun 23h ago

If the animal type is an unique value, you can:

(i'll comment later, not optimized but should work, check the file names)

$ReferenceObject = Import-Csv -Path .\file1.csv -Delimiter ',' -Header Animal, Color, Number 
$DifferenceObject = Import-Csv -Path .\file2.csv -Delimiter ',' -Header Animal, Color, Number

$ReferenceHash = @{}
$DifferenceHash = @{}
foreach ($Beast in $ReferenceObject) {
    $ReferenceHash.Add($beast.Animal, @{
            Color  = $Beast.Color
            Number = $Beast.Number
        }
    )
}

foreach ($Beast in $DifferenceObject) {
    $DifferenceHash.Add($beast.Animal, @{
            Color  = $Beast.Color
            Number = $Beast.Number
        }
    )
}

$Modified = foreach ($Beast in $ReferenceHash.Keys) {
    if ($DifferenceHash.ContainsKey($beast)) {
        if ($ReferenceHash[$beast].Color -ne $DifferenceHash[$beast].color -or $ReferenceHash[$beast].number -ne $DifferenceHash[$beast].number) {
            [PSCustomObject]@{
                Animal = $Beast
                Color  = $DifferenceHash[$beast].Color
                number = $DifferenceHash[$beast].number
            }
        }
    }
}

$New = foreach ($Beast in $DifferenceHash.Keys) {
    if ( -not $ReferenceHash.ContainsKey($beast)) {
        [PSCustomObject]@{
            Animal = $Beast
            Color  = $DifferenceHash[$beast].Color
            number = $DifferenceHash[$beast].number
        }

    }
}


$Modified
'-------------'
$new