PowerShell ships a built-in diff capability — the Compare-Object
cmdlet — that has been part of Windows since PowerShell 1.0 in 2006. If you know how to
wire it up correctly, you can PowerShell diff two files in a single line,
compare two files in PowerShell by content or hash, diff entire directory
trees, validate CSV exports, and pipe results straight into CI/CD pipelines — all without
leaving the terminal. This guide walks through every method — from the two-liner quick start
to recursive folder diffing and binary integrity checks — so you can pick exactly the right
tool for your workflow.
What Is PowerShell Diff? (vs. Unix diff and fc.exe)
When sysadmins and developers say "PowerShell diff," they almost always mean
Compare-Object
— or its built-in alias diff. Introduced in PowerShell 1.0 (2006),
Compare-Object compares two collections of objects rather than raw
byte streams. That distinction matters enormously for how you use it.
There are three native Windows tools for file comparison, each with a different sweet spot:
| Tool | Speed | File types | Recursion | Output format | Best for |
|---|---|---|---|---|---|
| Compare-Object | Medium | Text, CSV, JSON, objects | Yes (manual) | PowerShell objects / pipeline | Scripted automation, structured data |
| fc.exe | Fast | Text, binary (limited) | No | Plain text to stdout | Quick one-off text diffs, batch scripts |
| Get-FileHash | Very fast | Any (hash only) | Yes (loop) | Hash string | Identity check, binary integrity |
| WinMerge | Fast (GUI) | Text, binary, archives | Yes | Visual side-by-side | Manual review, merge conflicts (3-way from v2.16+) |
| Diff Checker | Instant | Text, code | No | Side-by-side browser view | Sharing diffs, non-technical reviewers |
Unix diff vs. PowerShell diff. On Linux and macOS, the
diff command produces unified or
context-format output that can be applied as a patch with patch. PowerShell's
Compare-Object does not produce patch-compatible output — it outputs .NET
objects with a SideIndicator property. That makes it more powerful for
automation inside PowerShell pipelines, but less portable outside them.
PowerShell 7+
runs cross-platform (Windows, macOS, Linux), so you can use Compare-Object
anywhere if you prefer the object-oriented pipeline model.
fc.exe. The legacy Windows File Compare utility dates to the MS-DOS era
and is still bundled with Windows 10 and 11. As a file compare PowerShell
alternative it is a compiled binary, so it starts faster and uses less memory than
PowerShell for a simple two-file text diff. But it cannot integrate into a PowerShell
pipeline, has no structured-data awareness, and its binary mode is rudimentary. Use
fc.exe when you just need a quick sanity check from a batch script or command
prompt.
Quick Start: Compare Two Text Files in PowerShell
=> marks lines present only in the right file, red <= marks lines only in the left file, and == marks shared lines (with -IncludeEqual).The canonical way to PowerShell diff two files — or to file compare in PowerShell from the command line — is a single line:
Compare-Object (Get-Content .\file1.txt) (Get-Content .\file2.txt) Get-Content reads each file and returns an array of strings, one per line.
Compare-Object then performs a set-difference between those two arrays and
outputs only the lines that differ. Here is what the output looks like:
InputObject SideIndicator
----------- -------------
Line only in file2 =>
Line only in file1 <=
To also see lines that are identical in both files, add -IncludeEqual:
Compare-Object (Get-Content .\file1.txt) (Get-Content .\file2.txt) -IncludeEqual Output now includes three indicator values:
InputObject SideIndicator
----------- -------------
Shared line ==
Line only in file2 =>
Line only in file1 <=
To get the raw changed lines back as strings (stripping the SideIndicator
wrapper), add -PassThru:
Compare-Object (Get-Content .\file1.txt) (Get-Content .\file2.txt) -PassThru Case sensitivity. By default, string comparisons in
Compare-Object are case-insensitive. To treat Hello and
hello as different lines, add -CaseSensitive:
Compare-Object (Get-Content .\file1.txt) (Get-Content .\file2.txt) -CaseSensitive
For large files, you may also want to control the -SyncWindow parameter, which
sets how many lines ahead PowerShell looks when attempting to re-synchronize after a
mismatch. The default is 5; raising it to 50 or 100 gives more accurate diffs on files with
large inserted or deleted blocks (at the cost of more memory).
Understanding Compare-Object Output
Every row that Compare-Object emits is a
PSCustomObject with two properties:
- InputObject — the line or object that differs between the two collections.
- SideIndicator — a string that tells you which side the item came from:
<=— present only in the Reference (left) object, i.e., the first argument.=>— present only in the Difference (right) object, i.e., the second argument.==— present in both (only emitted when you use-IncludeEqual).
Because the output is a proper PowerShell object, you can filter it with
Where-Object. For example, to show only lines that were removed from
file1 (i.e., lines that appear in file1 but not file2):
Compare-Object (Get-Content .\file1.txt) (Get-Content .\file2.txt) |
Where-Object { $_.SideIndicator -eq '<=' } And to show only lines added in file2:
Compare-Object (Get-Content .\file1.txt) (Get-Content .\file2.txt) |
Where-Object { $_.SideIndicator -eq '=>' }
You can also pipe these results into Export-Csv, Out-File,
ConvertTo-Json, or any other PowerShell cmdlet — a capability that
fc.exe and Unix diff simply cannot match without additional
shell glue.
Comparing CSV and Structured Data
Raw Get-Content treats every line as a plain string. For structured files like
CSV, that means a row reordering will look like every row changed — even if the data is
identical. To PowerShell compare two files of CSV data correctly, the fix
is Import-Csv, which parses column headers and returns typed objects:
# PowerShell compare two files — CSV edition
$old = Import-Csv .\users-old.csv
$new = Import-Csv .\users-new.csv
Compare-Object $old $new -Property Name, Email, Status
The -Property parameter tells Compare-Object which columns to
consider when deciding whether two rows are "equal." Only rows where all specified
properties match are treated as identical — so a row with the same Name but a
different Email shows up as a change.
To find rows that were added to the new file (not in the old file):
Compare-Object $old $new -Property Name, Email, Status |
Where-Object { $_.SideIndicator -eq '=>' } |
Select-Object -ExpandProperty InputObject
This pattern is extremely useful for validating database exports, auditing user lists, or
comparing configuration snapshots. If you are comparing JSON instead of CSV, first convert
with Get-Content | ConvertFrom-Json to get typed objects, then pipe into
Compare-Object with the same -Property approach.
Fast File Comparison with Get-FileHash
Sometimes you do not need a line-by-line diff at all. If the goal is to
PowerShell compare two files for byte-level equality — knowing
whether they are identical, not how they differ —
Get-FileHash is the fastest approach and works on any file type:
# Instant identity check — works on any file size
$hash1 = (Get-FileHash .\file1.txt -Algorithm SHA256).Hash
$hash2 = (Get-FileHash .\file2.txt -Algorithm SHA256).Hash
if ($hash1 -eq $hash2) {
Write-Host "Files are identical."
} else {
Write-Host "Files differ."
} Get-FileHash supports SHA1, SHA256, SHA384, SHA512, and MD5. SHA256 is the
recommended choice — it is fast, collision-resistant, and widely used for integrity
verification in CI/CD pipelines. MD5 is faster but cryptographically weak; avoid it for
security-sensitive comparisons.
Comparing a folder of files by hash. You can pipe Get-ChildItem
into Get-FileHash to build a hash table for an entire directory:
Get-ChildItem .\deploy\ -Recurse -File |
Get-FileHash -Algorithm SHA256 |
Select-Object Hash, Path
Save this output as a CSV before deployment, then re-run it after deployment and diff the
two CSVs with Compare-Object and -Property Hash, Path to detect
any files that changed or were added or removed during deployment.
For a deep dive into binary-level comparison beyond hashes, see the
binary compare guide, which covers
hex editors, cmp, and dedicated binary diff tools.
Binary File Comparison with Chunked Buffering
Compare-Object operates on .NET objects, not raw bytes. For true byte-level
binary diffing in PowerShell, you need a FileStream-based approach that reads
both files in chunks and compares the buffers:
function Compare-BinaryFiles {
param(
[string]$Path1,
[string]$Path2,
[int]$BufferSize = 65536
)
$stream1 = [System.IO.File]::OpenRead($Path1)
$stream2 = [System.IO.File]::OpenRead($Path2)
try {
if ($stream1.Length -ne $stream2.Length) {
return $false
}
$buf1 = New-Object byte[] $BufferSize
$buf2 = New-Object byte[] $BufferSize
do {
$read1 = $stream1.Read($buf1, 0, $BufferSize)
$read2 = $stream2.Read($buf2, 0, $BufferSize)
if ($read1 -ne $read2) { return $false }
for ($i = 0; $i -lt $read1; $i++) {
if ($buf1[$i] -ne $buf2[$i]) { return $false }
}
} while ($read1 -gt 0)
return $true
} finally {
$stream1.Close()
$stream2.Close()
}
}
# Usage
Compare-BinaryFiles -Path1 .\image-v1.png -Path2 .\image-v2.png
This function returns $true if the files are byte-for-byte identical and
$false otherwise. The default buffer size of 64 KB is a good balance between
memory use and read performance; for very large files (multi-GB), increase it to 1 MB
(1048576).
For most binary integrity scenarios, Get-FileHash is simpler and sufficient.
Reserve the chunked buffer approach for cases where you need to identify the exact byte
offset where files diverge, or when you cannot afford the time to hash the entire file
before making a decision.
Recursive Folder and Directory Comparison
To compare two folders in PowerShell, combine
Get-ChildItem -Recurse with Compare-Object. The key is to compare
relative paths (not absolute paths) so files at the same relative location match up
correctly:
# Recursive folder diff — find files that exist in one folder but not the other
$folder1 = 'C:\Backup\v1'
$folder2 = 'C:\Backup\v2'
$files1 = Get-ChildItem $folder1 -Recurse -File |
Select-Object -ExpandProperty FullName |
ForEach-Object { $_.Replace($folder1, '') }
$files2 = Get-ChildItem $folder2 -Recurse -File |
Select-Object -ExpandProperty FullName |
ForEach-Object { $_.Replace($folder2, '') }
Compare-Object $files1 $files2
Lines with <= are files that exist only in v1 (deleted between
versions). Lines with => are files added in v2. Files present
in both folders (possibly with changed content) do not appear unless you add
-IncludeEqual.
Detecting changed content, not just presence. To also catch files that exist in both folders but have different content, combine the folder diff with hash comparison:
$hashes1 = Get-ChildItem $folder1 -Recurse -File | ForEach-Object {
[PSCustomObject]@{
RelPath = $_.FullName.Replace($folder1, '')
Hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
}
}
$hashes2 = Get-ChildItem $folder2 -Recurse -File | ForEach-Object {
[PSCustomObject]@{
RelPath = $_.FullName.Replace($folder2, '')
Hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
}
}
Compare-Object $hashes1 $hashes2 -Property RelPath, Hash Any file that moved, was renamed, or had its content changed will appear in the output. Files that are present and unchanged in both folders will not — keeping the output clean and actionable.
For GUI-based folder comparison on macOS, the macOS directory comparison guide covers FileMerge, Kaleidoscope, and terminal options. For Windows folder diffing without PowerShell, see how to tell which files are different in Windows folders.
Advanced Parameters and Performance Tuning
Compare-Object has several parameters that are rarely documented but
matter enormously in production scripts:
| Parameter | Default | Effect |
|---|---|---|
-CaseSensitive | Off | Treats "Hello" and "hello" as different values |
-SyncWindow | 5 | Lines ahead PS looks to re-sync after a mismatch; raise for large insertions |
-IncludeEqual | Off | Emits == rows for lines present in both collections |
-ExcludeDifferent | Off | Only emit == rows (useful for finding common lines) |
-Property | (whole object) | Compare specific object properties rather than the whole object |
-PassThru | Off | Returns the original object instead of a wrapped PSCustomObject |
Memory management for large files. Get-Content loads the
entire file into memory as a string array. For files larger than ~100 MB, this can be
slow or cause out-of-memory errors. Consider using
Get-Content -ReadCount 1000 to stream lines in chunks, or switch to
StreamReader for line-by-line processing without loading the whole file:
$reader = [System.IO.StreamReader]::new('.\huge-file.txt')
$lines = [System.Collections.Generic.List[string]]::new()
while (-not $reader.EndOfStream) {
$lines.Add($reader.ReadLine())
}
$reader.Close()
For files over 500 MB, skip Compare-Object entirely and use
Get-FileHash for an identity check, or reach for a dedicated binary diff tool.
When CLI Isn't Enough: Visual Diff Tools
PowerShell's Compare-Object is excellent for automation — but its output is a
flat list of changed lines. When you need to understand a complex diff — see
context around changes, spot a subtle whitespace issue, or share results with someone who
has never opened a terminal — a visual diff tool is the right companion.
The typical workflow looks like this:
- Run
Compare-Objectin PowerShell to confirm files differ and get a count of changes. - Export the differing content to a temp file or clipboard.
- Open both originals in a visual diff tool for side-by-side review.
- If changes are approved, use PowerShell to automate the next step (copy, deploy, archive).
For code files in an editor workflow, VS Code's built-in diff viewer is a natural choice — the VS Code file comparison guide walks through every method. For Notepad++ users, the Notepad++ Compare plugin guide covers the ComparePlus plugin.
For browser-based review — particularly when sharing diffs with non-technical reviewers or working in environments where desktop app installs are restricted — the Diff Checker Chrome extension provides instant side-by-side diffing in your browser. It works without installing PowerShell modules or any additional software, processes files locally (no upload), and is well-suited for sharing configuration changes, config file diffs, or document comparisons with teammates who do not use PowerShell.
For Linux-based comparison workflows with the classic diff utility, the
Linux compare files guide covers
diff, cmp, comm, vimdiff, and Meld.
Common Errors and Troubleshooting
Most file compare PowerShell frustrations come from three sources: encoding mismatches, line-ending differences, and memory limits. Here is how to diagnose and fix each one.
Encoding mismatches (UTF-8 vs. UTF-16)
Get-Content auto-detects encoding, but when one file is UTF-8 and the other is
UTF-16 (common for files generated by different Windows tools), every line appears different
even if the text content is identical. Fix: force a consistent encoding on both reads:
$ref = Get-Content .\file1.txt -Encoding UTF8
$diff = Get-Content .\file2.txt -Encoding UTF8
Compare-Object $ref $diff
On PowerShell 5.x, the -Encoding values are named constants
(UTF8, Unicode, ASCII). On PowerShell 7+, you can
also pass IANA encoding names like 'utf-8'.
CRLF vs. LF line endings
Windows files typically use CRLF (\r\n) line endings; Linux and macOS files
use LF (\n). When you compare a Windows-generated file with a Unix-generated
file, Get-Content may leave trailing carriage returns (\r) on each
line, causing every line to appear different. To strip them before comparison:
$ref = (Get-Content .\file1.txt) -replace '\r', ''
$diff = (Get-Content .\file2.txt) -replace '\r', ''
Compare-Object $ref $diff Out-of-memory on large files
Get-Content loads entire files into memory. For files over ~200 MB, you may
hit memory pressure or a script timeout. Options:
- Use
Get-FileHashinstead if you only need an identity check. - Use
StreamReaderto process line-by-line without full load. - Split the file into chunks with
-ReadCountand compare chunks. - Use
fc.exefor a fast first-pass on raw text.
Compare-Object returns nothing (no output)
If Compare-Object produces no output, the files are identical (from
PowerShell's perspective). Verify this is expected by checking:
- Are both files truly the same path? (Check for typos.)
- Are encoding differences masking real content differences? (See above.)
- Is one file empty?
(Get-Content .\file.txt).Countshould be greater than zero.
Automation: Scheduled Diff Reports for CI/CD
One of the strongest use cases for PowerShell diff is building automated change-detection pipelines. Here is a practical pattern: compare a configuration snapshot from yesterday against today's live config, and email the diff report if anything changed.
# config-diff-report.ps1
param(
[string]$BaselinePath = '.\baseline\config-snapshot.csv',
[string]$LivePath = '.\live\config-current.csv',
[string]$ReportPath = '.\reports\diff-report.txt'
)
$baseline = Import-Csv $BaselinePath
$live = Import-Csv $LivePath
$changes = Compare-Object $baseline $live -Property Key, Value
if ($changes.Count -gt 0) {
$report = $changes | Format-Table -AutoSize | Out-String
$report | Out-File $ReportPath -Encoding UTF8
Send-MailMessage `
-To 'ops-team@example.com' `
-From 'ci@example.com' `
-Subject "Config drift detected: $($changes.Count) changes" `
-Body $report `
-SmtpServer 'smtp.example.com'
Write-Host "Diff report sent. $($changes.Count) change(s) detected."
exit 1 # non-zero exit signals CI failure
} else {
Write-Host "No changes detected."
exit 0
} In a GitHub Actions or Azure Pipelines workflow, you would run this script as a PowerShell step. If it exits with code 1, the pipeline fails and the team is notified. If it exits with 0, the pipeline continues. This pattern is useful for:
- Configuration drift detection between environments (dev → staging → prod).
- Verifying that deployment scripts produced the expected file changes.
- Auditing user or permission lists for unauthorized additions.
- Comparing API response snapshots to catch regressions in a data pipeline.
For human review of the flagged diffs, the Diff Checker Chrome extension provides an easy way to paste the two file versions and share a visual side-by-side comparison with teammates — no terminal required on the reviewer's side.
Decision Tree: When to Use What
Not every file comparison problem has the same shape. Use this decision tree to pick the right tool the first time:
| Scenario | Best Tool | Why |
|---|---|---|
| Are two files byte-for-byte identical? | Get-FileHash | Fastest, works on any file type, no memory concerns |
| What lines differ between two text files? | Compare-Object + Get-Content | Object pipeline, filterable, scriptable output |
| Compare structured rows in two CSVs | Compare-Object + Import-Csv + -Property | Column-aware; handles row reordering gracefully |
| Find files added or removed across two folders | Get-ChildItem -Recurse + Compare-Object | Recursive, scriptable, hash-combinable |
| Quick one-off text file diff from CMD/batch | fc.exe | Fastest startup, no PowerShell required |
| Byte-level binary diff (find exact offset) | Chunked FileStream loop | Full byte control; Get-FileHash only gives yes/no |
| Visual review / share diff with teammate | VS Code or Diff Checker extension | Side-by-side view, inline highlighting, no CLI required |
| CI/CD automated diff with pass/fail signal | Compare-Object script + exit code | Pipeline integration, custom exit codes, email/Slack hooks |
The general principle: use Get-FileHash when you only need a yes/no answer,
use Compare-Object when you need structured, filterable output in a
PowerShell pipeline, and use a visual tool when a human needs to review or approve the
changes.
Frequently Asked Questions
How do I compare two files in PowerShell?
The standard approach is Compare-Object (Get-Content .\file1.txt) (Get-Content
.\file2.txt). Lines only in the first file appear with <=;
lines only in the second appear with =>. Add -IncludeEqual
to also see lines shared by both files (marked ==). For a faster
binary-identical check, use
(Get-FileHash file1.txt).Hash -eq (Get-FileHash file2.txt).Hash.
What is the PowerShell equivalent of Unix diff?
Compare-Object — aliased as diff — is the PowerShell equivalent
of the Unix diff command. The
key difference is that Compare-Object outputs .NET objects rather than
patch-compatible text, which makes it more powerful inside PowerShell pipelines but less
portable to external tools. PowerShell 7+ runs on Linux and macOS, so you can use it
alongside the native diff utility depending on your needs. For a broader look
at Linux file comparison methods, see the
Linux compare files guide.
Can PowerShell compare binary files?
Not directly with Compare-Object — it compares .NET objects, not raw bytes.
For an identity check on binary files, Get-FileHash is the right tool: if
both hashes match, the files are identical. For a true byte-level comparison (finding
exactly which bytes differ), use a FileStream-based chunked buffer loop as
shown in the binary comparison section above. For more on binary diff techniques and
tools, the binary compare guide covers hex editors,
cmp, and dedicated binary diff utilities.
How do I compare CSV files in PowerShell with Import-Csv?
Load both files with Import-Csv, then run
Compare-Object $old $new -Property Col1, Col2, Col3 where the
-Property list specifies which columns define row identity. This compares
structured rows by column value rather than raw text lines — so reordered rows and
reformatted values are handled correctly. Add | Where-Object { $_.SideIndicator
-eq '=>' } to isolate only added rows, or '<=' for
deleted rows.
Is fc.exe faster than Compare-Object for large files?
Yes, for a simple line-by-line text diff. fc.exe is a compiled C binary with
minimal startup overhead, while Compare-Object loads the entire file into
memory as a .NET object array before diffing begins. For a 50 MB log file,
fc.exe will typically finish faster. However, fc.exe cannot be
piped, filtered, or combined with structured data — making Compare-Object the
better choice for any automation scenario. For the fastest possible identity check on large
files, use Get-FileHash, which only needs to read the file once to produce a
hash.