使用 PowerShell 比较文件夹和内容

Posted

技术标签:

【中文标题】使用 PowerShell 比较文件夹和内容【英文标题】:Comparing folders and content with PowerShell 【发布时间】:2011-09-25 11:14:17 【问题描述】:

我有两个带有 xml 文件的不同文件夹。与另一个文件夹 (folder1) 相比,一个文件夹 (folder2) 包含更新的和新的 xml 文件。我需要知道文件夹 2 中的哪些文件与文件夹 1 相比是新的/更新的,并将它们复制到第三个文件夹(文件夹 3)。在 PowerShell 中完成此任务的最佳方法是什么?

【问题讨论】:

您想根据修改日期比较还是比较文件内容? 我想根据文件内容进行比较。 你想如何处理一个或另一个文件夹中不存在的文件? 如果文件夹 1 中不存在文件但文件夹 2 中存在文件,则这些文件是新文件,我想将它们复制到文件夹 3。如果文件夹 1 中存在文件但文件夹 2 中不存在,那么这些是过时的文件,所以我不想复制它们但想记录它们。 你想如何处理内容重复但文件名不同的文件? 【参考方案1】:

好的,我不会为你编写整个代码(这有什么乐趣?),但我会让你开始。

首先,有两种方法可以进行内容比较。懒惰/大部分正确的方法,即比较文件的长度;以及更准确但更复杂的方法,即比较每个文件内容的哈希值。

为简单起见,让我们用简单的方法比较文件大小。

基本上,您需要两个代表源文件夹和目标文件夹的对象:

$Folder1 = Get-childitem "C:\Folder1"
$Folder2 = Get-childitem  "C:\Folder2"

那你可以用Compare-Object来看看有哪些不一样的……

Compare-Object $Folder1 $Folder2 -Property Name, Length

它将通过仅比较每个集合中文件对象的名称和长度为您列出所有不同之处。

您可以将其通过管道传递给 Where-Object 过滤器以选择左侧不同的内容...

Compare-Object $Folder1 $Folder2 -Property Name, Length | Where-Object $_.SideIndicator -eq "<="

然后将其通过管道传送到 ForEach-Object 以复制到您想要的位置:

Compare-Object $Folder1 $Folder2 -Property Name, Length  | Where-Object $_.SideIndicator -eq "<=" | ForEach-Object 
        Copy-Item "C:\Folder1\$($_.name)" -Destination "C:\Folder3" -Force
        

【讨论】:

谢谢,JNK!我不得不将侧面指示器反转到另一侧(=>),但它似乎对我有用:Compare-Object $Folder1 $Folder2 -Property Name, Length | Where-Object $_.SideIndicator -eq "=&gt;" | ForEach-Object Copy-Item "$Folder2Path\$($_.name)" -Destination $Folder3 -Force @keith - 如果需要,您还可以颠倒文件夹的顺序。如果您想对其他文件(存在于 folder2 但不存在于 folder1 的文件)执行单独的逻辑,则只需使用 -property name 并反转侧面指示器,然后记录它们或您想要的任何内容。 上面的脚本似乎可以在平面文件夹结构中正常工作,但在处理带有文件的子目录的文件夹时似乎不起作用。有什么想法吗? 您可以使用Get-Childitem "C:\Folder2" | where-object $_.length -eq $null 制作特殊情况以仅获取文件夹。然后,您可以在其他地方进行管道传输。如果你需要做很多这样的事情,让它成为你提供文件夹的函数可能是有意义的,那么你可以让它递归地自动处理子文件夹。 感谢您的意见,我会试试看。【参考方案2】:

使用 MD5 散列的递归目录差异(比较内容)

这是一个纯 PowerShell v3+ 递归文件差异(无依赖关系),它为每个目录文件内容(左/右)计算 MD5 哈希。可以选择导出 CSV 以及摘要文本文件。默认输出结果到标准输出。可以将 rdiff.ps1 文件拖放到您的路径中,也可以将内容复制到您的脚本中。

USAGE: rdiff path/to/left,path/to/right [-s path/to/summary/dir]

这里是gist。建议使用 gist 中的版本,因为它可能会随着时间的推移而具有其他功能。随时发送拉取请求。

#########################################################################
### USAGE: rdiff path/to/left,path/to/right [-s path/to/summary/dir]  ###
### ADD LOCATION OF THIS SCRIPT TO PATH                               ###
#########################################################################
[CmdletBinding()]
param (
  [parameter(HelpMessage="Stores the execution working directory.")]
  [string]$ExecutionDirectory=$PWD,

  [parameter(Position=0,HelpMessage="Compare two directories recursively for differences.")]
  [alias("c")]
  [string[]]$Compare,

  [parameter(HelpMessage="Export a summary to path.")]
  [alias("s")]
  [string]$ExportSummary
)

### FUNCTION DEFINITIONS ###

# SETS WORKING DIRECTORY FOR .NET #
function SetWorkDir($PathName, $TestPath) 
  $AbsPath = NormalizePath $PathName $TestPath
  Set-Location $AbsPath
  [System.IO.Directory]::SetCurrentDirectory($AbsPath)


# RESTORES THE EXECUTION WORKING DIRECTORY AND EXITS #
function SafeExit() 
  SetWorkDir /path/to/execution/directory $ExecutionDirectory
  Exit


function Print 
  [CmdletBinding()]
  param (
    [parameter(Mandatory=$TRUE,Position=0,HelpMessage="Message to print.")]
    [string]$Message,

    [parameter(HelpMessage="Specifies a success.")]
    [alias("s")]
    [switch]$SuccessFlag,

    [parameter(HelpMessage="Specifies a warning.")]
    [alias("w")]
    [switch]$WarningFlag,

    [parameter(HelpMessage="Specifies an error.")]
    [alias("e")]
    [switch]$ErrorFlag,

    [parameter(HelpMessage="Specifies a fatal error.")]
    [alias("f")]
    [switch]$FatalFlag,

    [parameter(HelpMessage="Specifies a info message.")]
    [alias("i")]
    [switch]$InfoFlag = !$SuccessFlag -and !$WarningFlag -and !$ErrorFlag -and !$FatalFlag,

    [parameter(HelpMessage="Specifies blank lines to print before.")]
    [alias("b")]
    [int]$LinesBefore=0,

    [parameter(HelpMessage="Specifies blank lines to print after.")]
    [alias("a")]
    [int]$LinesAfter=0,

    [parameter(HelpMessage="Specifies if program should exit.")]
    [alias("x")]
    [switch]$ExitAfter
  )
  PROCESS 
    if($LinesBefore -ne 0) 
      foreach($i in 0..$LinesBefore)  Write-Host "" 
    
    if($InfoFlag)  Write-Host "$Message" 
    if($SuccessFlag)  Write-Host "$Message" -ForegroundColor "Green" 
    if($WarningFlag)  Write-Host "$Message" -ForegroundColor "Orange" 
    if($ErrorFlag)  Write-Host "$Message" -ForegroundColor "Red" 
    if($FatalFlag)  Write-Host "$Message" -ForegroundColor "Red" -BackgroundColor "Black" 
    if($LinesAfter -ne 0) 
      foreach($i in 0..$LinesAfter)  Write-Host "" 
    
    if($ExitAfter)  SafeExit 
  


# VALIDATES STRING MIGHT BE A PATH #
function ValidatePath($PathName, $TestPath) 
  If([string]::IsNullOrWhiteSpace($TestPath)) 
    Print -x -f "$PathName is not a path"
  


# NORMALIZES RELATIVE OR ABSOLUTE PATH TO ABSOLUTE PATH #
function NormalizePath($PathName, $TestPath) 
  ValidatePath "$PathName" "$TestPath"
  $TestPath = [System.IO.Path]::Combine((pwd).Path, $TestPath)
  $NormalizedPath = [System.IO.Path]::GetFullPath($TestPath)
  return $NormalizedPath



# VALIDATES STRING MIGHT BE A PATH AND RETURNS ABSOLUTE PATH #
function ResolvePath($PathName, $TestPath) 
  ValidatePath "$PathName" "$TestPath"
  $ResolvedPath = NormalizePath $PathName $TestPath
  return $ResolvedPath


# VALIDATES STRING RESOLVES TO A PATH AND RETURNS ABSOLUTE PATH #
function RequirePath($PathName, $TestPath, $PathType) 
  ValidatePath $PathName $TestPath
  If(!(Test-Path $TestPath -PathType $PathType)) 
    Print -x -f "$PathName ($TestPath) does not exist as a $PathType"
  
  $ResolvedPath = Resolve-Path $TestPath
  return $ResolvedPath


# Like mkdir -p -> creates a directory recursively if it doesn't exist #
function MakeDirP 
  [CmdletBinding()]
  param (
    [parameter(Mandatory=$TRUE,Position=0,HelpMessage="Path create.")]
    [string]$Path
  )
  PROCESS 
    New-Item -path $Path -itemtype Directory -force | Out-Null
  


# GETS ALL FILES IN A PATH RECURSIVELY #
function GetFiles 
  [CmdletBinding()]
  param (
    [parameter(Mandatory=$TRUE,Position=0,HelpMessage="Path to get files for.")]
    [string]$Path
  )
  PROCESS 
    ls $Path -r | where  !$_.PSIsContainer 
  


# GETS ALL FILES WITH CALCULATED HASH PROPERTY RELATIVE TO A ROOT DIRECTORY RECURSIVELY #
# RETURNS LIST OF @RelativePath, Hash, FullName
function GetFilesWithHash 
  [CmdletBinding()]
  param (
    [parameter(Mandatory=$TRUE,Position=0,HelpMessage="Path to get directories for.")]
    [string]$Path,

    [parameter(HelpMessage="The hash algorithm to use.")]
    [string]$Algorithm="MD5"
  )
  PROCESS 
    $OriginalPath = $PWD
    SetWorkDir path/to/diff $Path
    GetFiles $Path | select @N="RelativePath";E=$_.FullName | Resolve-Path -Relative,
                            @N="Hash";E=(Get-FileHash $_.FullName -Algorithm $Algorithm | select Hash).Hash,
                            FullName
    SetWorkDir path/to/original $OriginalPath
  


# COMPARE TWO DIRECTORIES RECURSIVELY #
# RETURNS LIST OF @RelativePath, Hash, FullName
function DiffDirectories 
  [CmdletBinding()]
  param (
    [parameter(Mandatory=$TRUE,Position=0,HelpMessage="Directory to compare left.")]
    [alias("l")]
    [string]$LeftPath,

    [parameter(Mandatory=$TRUE,Position=1,HelpMessage="Directory to compare right.")]
    [alias("r")]
    [string]$RightPath
  )
  PROCESS 
    $LeftHash = GetFilesWithHash $LeftPath
    $RightHash = GetFilesWithHash $RightPath
    diff -ReferenceObject $LeftHash -DifferenceObject $RightHash -Property RelativePath,Hash
  


### END FUNCTION DEFINITIONS ###

### PROGRAM LOGIC ###

if($Compare.length -ne 2) 
  Print -x "Compare requires passing exactly 2 path parameters separated by comma, you passed $($Compare.length)." -f

Print "Comparing $($Compare[0]) to $($Compare[1])..." -a 1
$LeftPath   = RequirePath path/to/left $Compare[0] container
$RightPath  = RequirePath path/to/right $Compare[1] container
$Diff       = DiffDirectories $LeftPath $RightPath
$LeftDiff   = $Diff | where $_.SideIndicator -eq "<=" | select RelativePath,Hash
$RightDiff   = $Diff | where $_.SideIndicator -eq "=>" | select RelativePath,Hash
if($ExportSummary) 
  $ExportSummary = ResolvePath path/to/summary/dir $ExportSummary
  MakeDirP $ExportSummary
  $SummaryPath = Join-Path $ExportSummary summary.txt
  $LeftCsvPath = Join-Path $ExportSummary left.csv
  $RightCsvPath = Join-Path $ExportSummary right.csv

  $LeftMeasure = $LeftDiff | measure
  $RightMeasure = $RightDiff | measure

  "== DIFF SUMMARY ==" > $SummaryPath
  "" >> $SummaryPath
  "-- DIRECTORIES --" >> $SummaryPath
  "`tLEFT -> $LeftPath" >> $SummaryPath
  "`tRIGHT -> $RightPath" >> $SummaryPath
  "" >> $SummaryPath
  "-- DIFF COUNT --" >> $SummaryPath
  "`tLEFT -> $($LeftMeasure.Count)" >> $SummaryPath
  "`tRIGHT -> $($RightMeasure.Count)" >> $SummaryPath
  "" >> $SummaryPath
  $Diff | Format-Table >> $SummaryPath

  $LeftDiff | Export-Csv $LeftCsvPath -f
  $RightDiff | Export-Csv $RightCsvPath -f

$Diff
SafeExit

【讨论】:

【参考方案3】:

对于@JNK 的回答,您可能希望确保始终使用文件而不是Compare-Object 的不太直观的输出。您只需要使用-PassThru 开关...

$Folder1 = Get-ChildItem "C:\Folder1"
$Folder2 = Get-ChildItem "C:\Folder2"
$Folder2 = "C:\Folder3\"

# Get all differences, i.e. from both "sides"
$AllDiffs = Compare-Object $Folder1 $Folder2 -Property Name,Length -PassThru

# Filter for new/updated files from $Folder2
$Changes = $AllDiffs | Where-Object $_.Directory.Fullname -eq $Folder2

# Copy to $Folder3
$Changes | Copy-Item -Destination $Folder3

这至少意味着您不必担心 SideIndicator 箭头指向哪个方向!

另外,请记住,您可能还想在 LastWriteTime 上进行比较。

子文件夹

递归遍历子文件夹稍微复杂一些,因为您可能需要在比较列表之前从 FullName 字段中删除相应的根文件夹路径。

您可以通过将新的 ScriptProperty 添加到您的 Folder1 和 Folder2 列表来做到这一点:

$Folder1 | Add-Member -MemberType ScriptProperty -Name "RelativePath" `
  -Value $this.FullName -replace [Regex]::Escape("C:\Folder1"),""

$Folder2 | Add-Member -MemberType ScriptProperty -Name "RelativePath" `
  -Value $this.FullName -replace [Regex]::Escape("C:\Folder2"),""

您应该能够在比较两个对象时使用 RelativePath 作为属性,并在复制时使用它来连接到“C:\Folder3”以保持文件夹结构不变。

【讨论】:

$Folder2 = "C:\Folder3\" 这可能是$Folder3 = "C:\Folder3\"【参考方案4】:

这是一种查找丢失或内容不同的文件的方法。

首先,一个快速而肮脏的单线(见下面的警告)。

dir -r | rvpa -Relative |% if (Test-Path $right\$_)  if (Test-Path -Type Leaf $_)  if ( diff (cat $_) (cat $right\$_ ) )  $_    else  $_  

在其中一个目录中运行上述代码,将$right 设置为(或替换为)另一个目录的路径。将报告$right 中缺少的内容或内容不同的内容。没有输出意味着没有发现差异。 CAVEAT:存在于$right 但左侧缺失的内容将不会被发现/报告。

这不会影响计算哈希值;它只是直接比较文件内容。当您想在另一个上下文中引用某些东西时(以后的日期,在另一台机器上等),散列是有意义的,但是当我们直接比较事物时,它只会增加开销。 (理论上,两个文件也有可能具有相同的哈希值,尽管这基本上不可能偶然发生。另一方面,蓄意攻击......)

这是一个更合适的脚本,它可以处理更多的极端情况和错误。

[CmdletBinding()]
Param(
    [Parameter(Mandatory=$true,Position=0)][string]$Left,
    [Parameter(Mandatory=$True,Position=1)][string]$Right
    )

# throw errors on undefined variables
Set-StrictMode -Version 1

# stop immediately on error
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

# init counters
$Items = $MissingRight = $MissingLeft = $Contentdiff = 0

# make sure the given parameters are valid paths
$left  = Resolve-Path $left
$right = Resolve-Path $right

# make sure the given parameters are directories
if (-Not (Test-Path -Type Container $left))   throw "not a container: $left"  
if (-Not (Test-Path -Type Container $right))  throw "not a container: $right" 

# Starting from $left as relative root, walk the tree and compare to $right.
Push-Location $left

try 
    Get-ChildItem -Recurse | Resolve-Path -Relative | ForEach-Object 
        $rel = $_
        
        $Items++
        
        # make sure counterpart exists on the other side
        if (-not (Test-Path $right\$rel)) 
            Write-Output "missing from right: $rel"
            $MissingRight++
            return
            
    
        # compare contents for files (directories just have to exist)
        if (Test-Path -Type Leaf $rel) 
            if ( Compare-Object (Get-Content $left\$rel) (Get-Content $right\$rel) ) 
                Write-Output "content differs   : $rel"
                $ContentDiff++
                
            
        
    
finally 
    Pop-Location
    

# Check items in $right for counterparts in $left.
# Something missing from $left of course won't be found when walking $left.
# Don't need to check content again here.

Push-Location $right

try 
    Get-ChildItem -Recurse | Resolve-Path -Relative | ForEach-Object 
        $rel = $_
        
        if (-not (Test-Path $left\$rel)) 
            Write-Output "missing from left : $rel"
            $MissingLeft++
            return
            
        
    
finally 
    Pop-Location
    

Write-Verbose "$Items items, $ContentDiff differed, $MissingLeft missing from left, $MissingRight from right"

【讨论】:

【参考方案5】:

使用脚本参数的便捷版

简单的文件级比较

PS &gt; .\DirDiff.ps1 -a .\Old\ -b .\New\这样称呼它

Param(
  [string]$a,
  [string]$b
)

$fsa = Get-ChildItem -Recurse -path $a
$fsb = Get-ChildItem -Recurse -path $b
Compare-Object -Referenceobject $fsa -DifferenceObject $fsb

可能的输出:

InputObject                  SideIndicator
-----------                  -------------
appsettings.Development.json <=
appsettings.Testing.json     <=
Server.pdb                   =>
ServerClientLibrary.pdb      =>

【讨论】:

这不回答问题,不比较文件内容【参考方案6】:

这样做:

compare (Get-ChildItem D:\MyFolder\NewFolder) (Get-ChildItem \\RemoteServer\MyFolder\NewFolder)

甚至递归:

compare (Get-ChildItem -r D:\MyFolder\NewFolder) (Get-ChildItem -r \\RemoteServer\MyFolder\NewFolder)

而且很难忘记 :)

【讨论】:

【参考方案7】:

gci -path 'C:\Folder' -recurse |where$_.PSIsContainer

-recurse 将探索给定根路径下的所有子树,并且 .PSIsContainer 属性是您要测试的仅获取所有文件夹的属性。您可以将 where!$_.PSIsContainer 用于文件。

【讨论】:

看起来这可能是对上述 cmets 中提出的问题的回答,但没有提供足够的上下文。

以上是关于使用 PowerShell 比较文件夹和内容的主要内容,如果未能解决你的问题,请参考以下文章

比较两个目录,查找同名但内容不同的文件

powershell 比较文件对和其他比较

PowerShell - 从 csv 文件读取数据,比较特定列中的数据并将结果写入新文件

如何在 PowerShell 中比较两个字符串对象的内容

比较本地文本文件和在线文件

比较两个文件夹上的文件的 git / powershell 脚本[关闭]