Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <#
- .SYNOPSIS
- Report differences in the contents of two files, or two trees of files.
- .DESCRIPTION
- Compares the two main parameters (arbitrarily designated "Left" and
- "Right) and reports differences in content. If the two parameters are
- both files, the content of those two files is compared. If the two
- parameters are both folders (directories), each item in the two
- compared: Files as described, folders recursively.
- Each difference found is reported to standard output. Left/Right/Both is
- given, followed by the type of difference. If walking a set of
- folders, the path of each item is given, relative to the root of the
- both directories.
- .PARAMETER Left
- Path name of a file or folder to compare. Arbitrarily designated "Left".
- .PARAMETER Right
- Path name of a file or folder to compare. Arbitrarily designated "Right".
- .PARAMETER Same
- Also report items where the content is the same.
- .PARAMETER ShowDebug
- Shortcut to setting DebugPreference=Continue for this script run.
- Debugging information is always written to the Debug output/object
- stream. This switch will reveal that, without the constant prompting
- that a full -Debug entails.
- .NOTES
- While the ideal goal would be to be able to compare anything that
- supported Get-Content, performance limitations prevent the use of that
- API. (It is several orders of magnitude slower, and very memory
- inefficient.) Currently the .NET API is used instead. This limits the
- operation to regular files only.
- File metadata (dates, ownership, etc) is ignored. This is by design;
- files with different metadata can still have the same contents.
- Alternate Data Streams (forks) are ignored. This may change some day.
- Content is compared even if file sizes differ. This way we know if files
- are the same up to that point, and one is simply truncated/shorter, or
- if the files have differing content as well.
- .EXAMPLE
- Compare-File C:\FOO C:\BAR
- Compares FOO and BAR, and reports differences in content.
- .INPUTS
- None. You cannot pipe objects to this script.
- .OUTPUTS
- Differences are reported as strings to the output stream, one line per
- difference (typically, one line per file). Only the relative path (from
- the root of the two compares) is reported. Empty output means no
- differences were found.
- .LINK
- Compare-Object
- #>
- ########################################################################
- # parameters
- [CmdletBinding()]
- Param(
- [Parameter(
- Mandatory=$true,
- Position=0,
- HelpMessage="Path name of the first file or folder to compare. "
- )]
- [string]$Left,
- [Parameter(
- Mandatory=$True,
- Position=1,
- HelpMessage="Path name of the second file or folder to compare. "
- )]
- [string]$Right,
- [Parameter(
- Mandatory=$False,
- HelpMessage="Explicitly report items which have the same content?"
- )]
- [switch]$Same = $false,
- [Parameter(
- Mandatory=$False,
- HelpMessage="Display debug output (without debug prompting)?"
- )]
- [switch]$ShowDebug
- )
- ########################################################################
- # safety
- # throw errors on undefined variables
- Set-StrictMode -Version 1
- # stop immediately on error
- $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
- # Display debug output (without debug prompting)
- If ($ShowDebug) { $DebugPreference = 'Continue' }
- ########################################################################
- # constants
- # size of two compare buffers, in bytes
- # 1024 * 1024 = 1 binary megabyte
- Set-Variable -option Constant -name BufSize -value (1024 * 1024)
- # modes to use when calling [System.IO.File]::Open()
- Set-Variable -option Constant -name FileMode -value ([System.IO.FileMode]::Open)
- Set-Variable -option Constant -name FileAccess -value ([IO.FileAccess]::Read)
- Set-Variable -option Constant -name FileShare -value ([IO.FileShare]::Read)
- # messages for Report-Diff - all same length for alignment
- Set-Variable -option Constant -name DiffMissR -value 'Right: Missing'
- Set-Variable -option Constant -name DiffMissL -value 'Left : Missing'
- Set-Variable -option Constant -name DiffShortR -value 'Right: Shorter'
- Set-Variable -option Constant -name DiffShortL -value 'Left : Shorter'
- Set-Variable -option Constant -name DiffContent -value 'Both : Differ '
- Set-Variable -option Constant -name DiffNone -value 'Both : Same '
- ########################################################################
- # external library functions
- # Microsoft Visual C Run-Time (MSVCRT)
- # helped by https://www.powershellgallery.com/packages/PSMemory/1.0.0/Content/PSMemory.psm1
- $MSCRTWrapperCode = @"
- using System;
- using System.Runtime.InteropServices;
- public static class MSCRTWrapper {
- [DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl)]
- public static extern int memcmp(byte[] b1, byte[] b2, long count);
- }
- "@
- # compile and add the MSCRTWrapper class
- # Add-Type is too noisy for -ShowDebug
- Add-Type -Debug:$false -TypeDefinition $MSCRTWrapperCode
- ########################################################################
- # PoSh subroutines
- # ----------------------------------------------------------------------
- function Walk-LeftTree ($LeftPath, $RightPath) {
- # Starting from Left as relative root, walk the tree and compare against
- # Right. When things exist on both sides, compare their content, too.
- Write-Debug "walking left"
- # so "Resolve-Path -Relative" will return path relative to $LeftPath
- Push-Location $LeftPath
- try {
- Get-ChildItem -Recurse | Resolve-Path -Relative | ForEach-Object {
- $rel = $_
- # nuke leading ".\" if present
- if ($rel.substring(0, 2) -eq '.\') { $rel = $rel.substring(2) }
- Write-Debug "rel=<$rel>"
- $script:Items++
- # make sure counterpart exists on the other side
- if (-not (Test-Path $RightPath\$rel)) {
- Report-Diff $rel $DiffMissR
- $script:MissingRight++
- return
- }
- # compare contents for files (directories just have to exist)
- if (Test-Path -Type Container $rel) {
- Write-Debug "skipping directory"
- }
- else {
- Compare-FileContent "$LeftPath\$rel" "$RightPath\$rel" $rel
- }
- } # ForEach-Object
- } # try
- finally {
- Pop-Location
- }
- } # Walk-LeftTree
- # ----------------------------------------------------------------------
- function Walk-RightTree ($LeftPath, $RightPath) {
- # We have now checked everything in Left and made sure the counterpart in
- # Right exists and has the same content. But if something does *not*
- # exist in Left but *does* exist in Right, walking Left of course will
- # not find it. So now we walk Right and make sure everything in Right
- # also exists in Left. We do not need to check here; anything existing in
- # both will have already been found and compared in Walk-LeftTree.
- Write-Debug "walking right"
- Push-Location $RightPath
- try {
- Get-ChildItem -Recurse | Resolve-Path -Relative | ForEach-Object {
- $rel = $_
- # nuke leading ".\" if present
- if ($rel.substring(0, 2) -eq '.\') { $rel = $rel.substring(2) }
- Write-Debug "rel=<$rel>"
- # do not count this one (yet), Left may already have counted it
- if (-not (Test-Path $LeftPath\$rel)) {
- Report-Diff $rel $DiffMissL
- $script:MissingLeft++
- # since Left didn't see this one, count it here
- $Items++
- return
- }
- } # ForEach-Object
- } # try
- finally {
- Pop-Location
- }
- } # Walk-RightTree
- # ----------------------------------------------------------------------
- function Compare-FileContent ($LeftPath, $RightPath, $ReportingName) {
- Write-Debug "getting items"
- $LeftItem = Get-Item $LeftPath
- $RightItem = Get-Item $RightPath
- Write-Debug "opening streams"
- $LeftStream = [System.IO.File]::Open($LeftItem, $FileMode, $FileAccess, $FileShare)
- $RightStream = [System.IO.File]::Open($RightItem, $FileMode, $FileAccess, $FileShare)
- Compare-Stream $LeftStream $RightStream $ReportingName
- Write-Debug "closing streams"
- $LeftStream.Close()
- $RightStream.Close()
- } # Compare-FileContent
- # ----------------------------------------------------------------------
- function Compare-Stream ($LeftStream, $RightStream, $ReportingName) {
- Write-Debug "comparing streams"
- # start with ContentSize equal to entire buffer
- # a short read for either will reset it to the shorter length
- $ContentSize = $BufSize
- $foundDiff = $false
- $done = $false
- while (-not $done) {
- $LeftCount = $LeftStream.Read($LeftBuf, 0, $BufSize)
- $RightCount = $RightStream.Read($RightBuf, 0, $BufSize)
- if (($LeftCount -eq 0) -and ($RightCount -eq 0)) {
- Write-Debug "read zero from both, EOF, same"
- break
- }
- # if we read short from either stream:
- # - report the shorter item
- # - reduce scope of compare to the shorter size
- if ($LeftCount -lt $RightCount) {
- Report-Diff $ReportingName $DiffShortL
- $foundDiff = $true
- $script:SizeDiff++
- $ContentSize = $LeftCount
- $done = $true
- }
- if ($RightCount -lt $LeftCount) {
- Report-Diff $ReportingName $DiffShortR
- $foundDiff = $true
- $script:SizeDiff++
- $ContentSize = $RightCount
- $done = $true
- }
- # don't bother comparing if one file ended right on buffer boundary
- if ($ContentSize -eq 0) { break }
- Write-Debug "comparing content"
- if (Compare-Buffer $LeftBuf $RightBuf $ContentSize) {
- Report-Diff $ReportingName $DiffContent
- $foundDiff = $true
- $script:ContentDiff++
- break
- }
- } # while
- # this isn't really a *difference* we are reporting, but oh well
- if ( $Same -and (-not $foundDiff) ) {
- Report-Diff $ReportingName $DiffNone
- }
- } # Compare-Stream
- # ----------------------------------------------------------------------
- function Compare-Buffer ($LeftBuf, $RightBuf, $Length) {
- # memcmp() from MSVCRT
- return (([MSCRTWrapper]::memcmp($LeftBuf, $RightBuf, $Length)) -ne 0)
- } # Compare-Buffer
- # ----------------------------------------------------------------------
- function Report-Diff ($Thing, $Diff) {
- # report a difference reason ($Diff), for optional reporting name $Thing
- # the "two files" case has no reporting name, just the difference
- $msg = if ($Thing) { "${Diff}: $Thing" } else { $Diff }
- Write-Output $msg
- } # Report-Diff
- ########################################################################
- # main program
- # init counters
- $Items = $MissingRight = $MissingLeft = $ContentDiff = $SizeDiff = 0
- # make sure the given parameters are valid paths (throw errors if not)
- $left = Resolve-Path $left
- $right = Resolve-Path $right
- Write-Debug "Left=<$Left>"
- Write-Debug "Right=<$Right>"
- # are the given parameters directories/folders/containers?
- $LeftDir = Test-Path -Type Container $left
- $RightDir = Test-Path -Type Container $right
- if ($LeftDir -ne $RightDir) { throw "Parameter type mismatch. Both must be files, or both must be folders." }
- # allocate buffers, will be used for read chunks of each file
- $LeftBuf = New-Object byte[] $BufSize
- $RightBuf = New-Object byte[] $BufSize
- if ($LeftDir) {
- # two trees
- Walk-LeftTree $Left $Right
- Walk-RightTree $Left $Right
- }
- else {
- # two files
- $Items = 1
- Compare-FileContent $Left $Right $null
- }
- # done, report stats (kinda useless for "two files" case, but not wrong)
- Write-Verbose "$Items items, $ContentDiff content differed, $SizeDiff size differed, $MissingLeft missing from left, $MissingRight from right"
- ##### END ##############################################################
Advertisement