使用PowerShell比较文件夹和内容

时间:2011-06-29 19:54:40

标签: file powershell compare directory

PowerShell noob在这里。

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

7 个答案:

答案 0 :(得分:26)

好的,我不会为你编写全部内容(那有什么好玩的?)但是我会让你开始。

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

为简单起见,让我们做一些简单的方法并比较文件大小。

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

$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
        }

答案 1 :(得分:7)

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

这是一个纯PowerShell v3 +递归文件diff(无依赖项),它为每个目录文件内容(左/右)计算MD5哈希值。可以选择导出CSV和摘要文本文件。默认输出结果为stdout。可以将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

答案 2 :(得分:1)

除了@ JNK的答案之外,您可能希望确保始终使用文件而不是-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 开关...

$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"),""}

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

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

子文件夹

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

您可以通过在Folder1和Folder2列表中添加新的ScriptProperty来实现此目的:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>...</title>
        <link href="css/magicledger.css" rel="stylesheet" type="text/css">
        <link href="css/normalize.css" rel="stylesheet" type="text/css">
        <link href="css/bootstrap.min.css" rel="stylesheet">

    <script>
    $(function() {
 $(".section").css({"height":$(window).height()});
  $.scrollify({
    section:".section"
  });


  $(".scroll").click(function(e) {
    e.preventDefault();
    $.scrollify("move",$(this).attr("href"));
  });
});
        </script>
</head>

<body>
        <section class="section section1">
        <header class="jumbotron vertical-center">
            <div class="container">
                <h1>...</h1>
                <h3>...</h3>
            <script type="text/javascript">
          window.Maitre = { uuid: "MF301766dea0" };
  </script>
  <script data-maitre src='https://maitreapp.co/widget.js' async></script> 
            </div>    
        </header>
    </section>


 <div class="container-fluid">
     <section class="section section2">
     <div class="row"> <!------- Row 1 ----->
         <div class="col-md-6">
             <div class="inside-row">
            <img src="icons/animat-bezier-color-2.gif" alt="icon1">
             </div>
         </div>
         <div class="col-md-6">
            <div class="inside-row">
                <h3>...</h3>
                <p>...</p>
            </div>
         </div>
     </div>
     </section>
     <section class="section section3">
        <div class="row"> <!-- Row 2 -------->
            <div class="col-md-6">
                <div class="inside-row">
                <img src="icons/animat-layers-color-2.gif" alt="icon2"> 
                </div>
            </div>
            <div class="col-md-6">
                <div class="inside-row">
                    <h3>...</h3>
                    <p>...</p>
                </div>
            </div>
        </div> 
     </section>
     <section class="section section3">
        <div class="row"> <!-- Row 3 ---------->
            <div class="col-md-6">
                <div class="inside-row">
                <img src="icons/animat-network-color-2.gif" alt="icon3">
                </div>
            </div>
            <div class="col-md-6">
                <div class="inside-row">
                    <h3>...</h3>
                    <p>...</p>
                </div>
            </div>
        </div>
     </section>
</div> <!-- ENd of container fluid ------>

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

答案 3 :(得分:0)

使用脚本参数

的便捷版本

简单的文件级别比较

将其称为PS > .\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      =>

答案 4 :(得分:0)

执行以下操作:

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

甚至递归:

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

甚至很难忘记:)

答案 5 :(得分:0)

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

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

example = {
    'Baker': '16,765',
    'Benton': '93,590',
    'Clackamas': '419,425',
    'Clatsop': '39,200',
    'Columbia': '51,900',
    'Coos': '63,275'
}

new_dict = {k: [float(i) for i in v.split(',')]
            for k, v in example.items()}

# pretty print the output
from pprint import pprint
pprint(new_dict)

在其中一个目录中运行以上代码,将 dir -r | rvpa -Relative |%{ if (Test-Path $right\$_) { if (Test-Path -Type Leaf $_) { if ( diff (cat $_) (cat $right\$_ ) ) { $_ } } } else { $_ } } 设置为(或替换为)另一个目录的路径。 $right 中缺少的内容或内容不同的内容将被报告。没有输出意味着没有发现差异。 注意事项$right 中存在但左侧缺失的内容不会被发现/报告。

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

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

$right

答案 6 :(得分:-2)

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

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