You copied a folder to a backup drive, a USB stick, or a new laptop. Now you need to know one thing: is the copy actually the same? Not "roughly the same size". The same files, with the same content.
Windows Explorer can't answer that. Compare-Object plus Get-FileHash can, in a short script you can rerun any time.
The one-liner: which file names differ?
Start cheap. Compare just the file names in two folders:
Compare-Object (Get-ChildItem C:\Photos -Name) (Get-ChildItem D:\Backup\Photos -Name)
InputObject SideIndicator
----------- -------------
IMG_2041.jpg =>
IMG_1987.jpg <=
Reading the SideIndicator column:
-
<=means the item exists only in the first (reference) set, hereC:\Photos. -
=>means it exists only in the second (difference) set, the backup. - Matching items are hidden. Add
-IncludeEqualto see them marked==.
So IMG_1987.jpg never made it to the backup, and IMG_2041.jpg is only on the backup.
That catches missing files. It does not catch a file that exists on both sides but has different content, like a half-copied video or a document you edited after the backup. For that you need hashes.
The real job: compare names and content, recursively
This script builds a fingerprint for every file (relative path plus SHA256 hash), then compares the two lists:
$left = (Resolve-Path 'C:\Photos').Path
$right = (Resolve-Path 'D:\Backup\Photos').Path
function Get-FileFingerprint {
param([string]$Root)
Get-ChildItem -LiteralPath $Root -Recurse -File | ForEach-Object {
[pscustomobject]@{
RelPath = $_.FullName.Substring($Root.Length).TrimStart('\')
Hash = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash
}
}
}
$a = @(Get-FileFingerprint -Root $left)
$b = @(Get-FileFingerprint -Root $right)
Compare-Object -ReferenceObject $a -DifferenceObject $b -Property RelPath, Hash |
Sort-Object RelPath
Why each piece matters:
-
RelPathstrips the root, soC:\Photos\2025\a.jpgandD:\Backup\Photos\2025\a.jpgboth become2025\a.jpgand can actually match. -
-Property RelPath, HashtellsCompare-Objectan item is only "equal" when both the path and the content hash match. -
@(...)forces an array even when a folder is empty. More on that below.
Identical files produce no output. What's left is the interesting part:
RelPath Hash SideIndicator
------- ---- -------------
2025\IMG_1987.jpg 7427D152... <=
2025\trip.mp4 3BB2ABB6... <=
2025\trip.mp4 73CB3858... =>
IMG_2041.jpg A4FB6214... =>
Notice trip.mp4 shows up twice, once per side with different hashes. That's the signature of a file that exists in both places but changed.
Turn it into a readable report
Two rows per changed file is accurate but annoying. Group by path and label each case:
Compare-Object $a $b -Property RelPath, Hash |
Group-Object -Property RelPath |
ForEach-Object {
$status = if ($_.Count -eq 2) { 'Changed' }
elseif ($_.Group[0].SideIndicator -eq '<=') { 'Missing in backup' }
else { 'Only in backup' }
[pscustomobject]@{ Status = $status; File = $_.Name }
} |
Sort-Object Status, File
Status File
------ ----
Changed 2025\trip.mp4
Missing in backup 2025\IMG_1987.jpg
Only in backup IMG_2041.jpg
Need to send it to someone or keep a record? Pipe the same result to Export-Csv:
... | Export-Csv -Path .\backup-diff.csv -NoTypeInformation
An empty report means the two folders match, file for file and byte for byte.
Gotchas that bite on real folders
1. An empty folder throws a terminating error. If Get-ChildItem finds nothing, it returns $null, and Compare-Object refuses it:
Cannot bind argument to parameter 'ReferenceObject' because it is null.
Wrapping the call in @(...) gives you an empty array instead, and the comparison simply reports every file on the other side as missing. That's why the script above uses $a = @(...).
2. Comparison is case-insensitive by default. Report.docx and report.docx count as the same name, which matches how NTFS behaves. If you're comparing against a Linux share or a Git checkout where case matters, add -CaseSensitive.
3. Hashing big folders is slow, so filter first. Hashing reads every byte. For a 200 GB media folder, compare cheap properties first and only hash what's left:
function Get-QuickFingerprint {
param([string]$Root)
Get-ChildItem -LiteralPath $Root -Recurse -File |
Select-Object @{ n = 'RelPath'; e = { $_.FullName.Substring($Root.Length).TrimStart('\') } }, Length
}
Compare-Object @(Get-QuickFingerprint $left) @(Get-QuickFingerprint $right) -Property RelPath, Length
A different Length proves a file changed. The same length doesn't prove it's identical, so run the hash version when you need certainty.
4. Don't compare timestamps to verify a copy. CreationTime resets when a file is copied, and not every copy tool or sync client preserves LastWriteTime, so a time-based diff can flag thousands of false "changes". Content hashes don't care how the file got there.
5. It's not a line-by-line text diff. Compare-Object (Get-Content a.txt) (Get-Content b.txt) tells you which lines exist on only one side. It won't give line numbers, and a line that merely moved won't be reported. For code, use git diff. For "did this config gain or lose a line?", it's perfect.
Quick reference
| Goal | Command |
|---|---|
| Names only | Compare-Object (gci A -Name) (gci B -Name) |
| Show matches too | add -IncludeEqual
|
| Compare on chosen properties | -Property RelPath, Hash |
| Case-sensitive names | add -CaseSensitive
|
| Content check | Get-FileHash -LiteralPath $file -Algorithm SHA256 |
| Save report | `\ |
Practice it before you need it
The time you most need this is right after a backup or migration, which is the worst time to learn the syntax. If you want to get the pipeline habit into your fingers first, practice PowerShell pipelines like Get-ChildItem and Compare-Object in a free browser terminal on CMD Master. No install, and nothing on your real disk gets touched.
Official docs
-
Compare-Object on Microsoft Learn: {% raw %}
SideIndicator,-Property,-IncludeEqual,-CaseSensitive, and the$nullterminating error -
Get-FileHash on Microsoft Learn: hash algorithms and
-LiteralPath -
Get-ChildItem on Microsoft Learn:
-Recurse,-File, and-Name - Group-Object on Microsoft Learn
- Export-Csv on Microsoft Learn
Related in this series: sort files by size, date, or name with Sort-Object and mirror a folder safely with Robocopy.
Learning Windows command line from scratch? CMD Master's interactive CMD, PowerShell, and Bash lessons are free and run entirely in your browser.
Top comments (0)