1

I am trying to compare two folder structures with PowerShell. At the end, I should see an overview of all files/folders with the full path, which are just in folder A or folder B.

I also want to implement a switch which can be toggled to exclude the file extensions. The comparison should just be done at the path level. So when a file has the same name but a different size, it should be seen as "equal".

So far, it looks like this:

# Prompt for the paths of the two folders to compare
$folder1 = Read-Host "Enter the path for Folder A"
$folder2 = Read-Host "Enter the path for Folder B"

# Prompt for whether to ignore file extensions
$ignoreExtensions = Read-Host "Ignore file extensions? (y/n)"
$ignoreExtensions = $ignoreExtensions.ToLower() -eq "y"

# Get the files in each folder and store their names and paths in arrays
$dir1Dirs = Get-ChildItem -Recurse -Name $folder1 | ForEach-Object {
    $name = $_.Name
    if ($ignoreExtensions) {
        $name = $name -replace '\.[^.]*$'
    }
    [PSCustomObject]@{
        Name = $name
        FullName = $_.FullName
    }
}
$dir2Dirs = Get-ChildItem -Recurse -Name $folder2 | ForEach-Object {
    $name = $_.Name
    if ($ignoreExtensions) {
        $name = $name -replace '\.[^.]*$'
    }
    [PSCustomObject]@{
        Name = $name
        FullName = $_.FullName
    }
}

# Compare the two arrays of file names and display the paths to files that are different
$diff = Compare-Object -ReferenceObject $dir1Dirs -DifferenceObject $dir2Dirs | Where-Object { $_.SideIndicator -eq "=>" }

if ($diff) {
    Write-Host "Files that are different:"
    $diff | Select-Object -ExpandProperty FullName
} else {
    Write-Host "No differences found."
}

but I get an error:

PS C:\Windows\system32> D:\compare4.ps1
Enter the path for Folder A: D:\folder1
Enter the path for Folder B: D:\folder2
Ignore file extensions? (y/n): y

Files that are different:
Select-Object : Property "FullName" cannot be found.
At D:\compare4.ps1:36 char:13
+     $diff | Select-Object -ExpandProperty FullName
+             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (@{InputObject=; SideIndicator==>}:PSObject) [Select-Object], PSArgumen
   tException
    + FullyQualifiedErrorId : ExpandPropertyNotFound,Microsoft.PowerShell.Commands.SelectObjectCommand

Select-Object : Property "FullName" cannot be found.
At D:\compare4.ps1:36 char:13
+     $diff | Select-Object -ExpandProperty FullName
+             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (@{InputObject=; SideIndicator==>}:PSObject) [Select-Object], PSArgumen
   tException
    + FullyQualifiedErrorId : ExpandPropertyNotFound,Microsoft.PowerShell.Commands.SelectObjectCommand

When change the compare line from above with this one, the error is gone, but no output generated:

$diff = Compare-Object $dir1Dirs $dir2Dirs -Property Name -IncludeEqual -PassThru | Where-Object { $_.SideIndicator -eq "=>" }

What am I missing here?

Thanks in advance.

x0100
  • 101
  • 8

2 Answers2

2

You're trying to compare folder trees by the relative paths of the files in them.

While Get-ChildItem's -Name parameter does output relative paths, it is all that it outputs, i.e. it emits strings rather than the usual System.IO.FileInfo (and System.IO.DirectoryInfo) instances, which means that the output won't have properties such as .FullName

If the requirement to optionally ignore extension weren't in the picture, you would be able to reconstruct the original full paths, by inferring what root path to prepend, depending on the value of .SideIndicator in the output from Compare-Object.

To satisfy your requirements, you'll have to:

  • Add a .RelativePath property to the [pscustomobject] instances you construct, and manually determine the relative path, by removing the root path's substring from the .FullName property of each file.

  • Compare the arrays of custom objects by that property, using -Property RelativePath. Additionally, in order to ensure that your custom objects are passed through as a whole, you must add -PassThru

To put it all together:

$folder1 = Read-Host "Enter the path for Folder A"
$folder2 = Read-Host "Enter the path for Folder B"

# Prompt for whether to ignore file extensions
$ignoreExtensions = 'y' -eq (Read-Host 'Ignore file extensions? (y/n)')

# Get the files in each folder and store their relative and full paths
# in arrays, optionally without extensions.
$dir1Dirs, $dir2Dirs = $folder1, $folder2 | 
  ForEach-Object {
    $fullRootPath = Convert-Path -LiteralPath $_
    # Construct the array of custom objects for the folder tree at hand
    # and *output it as a single object*, using the unary form of the 
    # array construction operator, ","  
    , @(
      Get-ChildItem -File -Recurse -LiteralPath $fullRootPath |
        ForEach-Object {
          $relativePath = $_.FullName.Substring($fullRootPath.Length + 1)
          if ($ignoreExtensions) { $relativePath = $relativePath -replace '\.[^.]*$' }
          [PSCustomObject] @{
            RelativePath = $relativePath
            FullName = $_.FullName
          }
        }
    )
  }

# Compare the two arrays.
# Note the use of -Property RelativePath and -PassThru
# as well as the Where-Object SideIndicator -eq '=>' filter, which
# - as in your question - only reports differences
# from the -DifferenceObject collection.
# To report differences from *either* collection, simply remove the filter.
$diff = 
  Compare-Object -Property RelativePath -PassThru $dir1Dirs $dir2Dirs | 
  Where-Object SideIndicator -eq '=>'

# Output the results.
if ($diff) {
    Write-Host "Files that are different:"
    $diff | Select-Object -ExpandProperty FullName
} else {
    Write-Host "No differences found."
}

Note:

  • The use of , @(...) to ensure that the Get-ChildItem output objects are output as an array as a whole (a single object) is explained in this answer.
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thx for the detailed explanation. Two things: 1) As long as in folder 2 more files are then in folder 1, everything works fine. If in folder 1 are more files, these files are not mentioned as difference. I also noticed, empty folders are not recognized as being different. 2) What if I would like to integrate the option that empty folders also show up as difference? – x0100 Jun 09 '23 at 13:58
  • 1
    @x0100, re 2): If you want to include empty directories that have no counterpart in the respective other directory tree, remove the `-File` option from the `Get-ChildItem` call. – mklement0 Jun 09 '23 at 16:13
  • 1
    @x0100, re 1): To report differences from _either_ input collection, remove the `Where-Object SideIndicator -eq '=>'` filter - I only included it because your code in the question has such a filter. – mklement0 Jun 09 '23 at 16:16
1

You were missing two things in your code.

  1. When using Compare-Object you also need to provide the -Property to indicate which property value to check.
  2. When using Get-ChildItem you was using -Name property, which is not going to work.

Here is the updated code:

# Prompt for the paths of the two folders to compare
$folder1 = Read-Host "Enter the path for Folder A"
$folder2 = Read-Host "Enter the path for Folder B"

# Prompt for whether to ignore file extensions
$ignoreExtensions = Read-Host "Ignore file extensions? (y/n)"
$ignoreExtensions = $ignoreExtensions.ToLower() -eq "y"

# Get the files in each folder and store their names and paths in arrays
$dir1Dirs = Get-ChildItem $folder1 -Recurse | ForEach-Object {
    $name = $_.Name
    if ($ignoreExtensions) {
        $name = $name -replace '\.[^.]*$'
    }
    [PSCustomObject]@{
        Name = $name
        FullName = $_.FullName
    }
}
$dir2Dirs = Get-ChildItem $folder2 -Recurse | ForEach-Object {
    $name = $_.Name
    if ($ignoreExtensions) {
        $name = $name -replace '\.[^.]*$'
    }
    [PSCustomObject]@{
        Name = $name
        FullName = $_.FullName
    }
}

# Compare the two arrays of file names and display the paths to files that are different
$diff = Compare-Object -ReferenceObject $dir1Dirs -DifferenceObject $dir2Dirs -Property FullName | Where-Object { $_.SideIndicator -eq "=>" }

if ($diff) {
    Write-Host "Files that are different:"
    $diff | Select-Object -ExpandProperty FullName
} else {
    Write-Host "No differences found."
}
SavindraSingh
  • 878
  • 13
  • 39
  • Thanks for checking, but it is not quite there yet. A quick test didn't bring up the results I expected, but I will take a closer look later on this. Regarding the -Name and Get-ChildItem, I read [here](https://stackoverflow.com/questions/53693701/powershell-script-to-compare-folder-structure) that this combination will cause all subdirectories -Directory, -Recurse to be reported as paths relative to the input directory, which allows convenient comparison between directory trees. Or did I miss understand something? – x0100 May 24 '23 at 13:48
  • Taht's correct, if you use `-Recurse`, it will show all subdirectories recursively – SavindraSingh May 24 '23 at 19:24