【问题标题】:How to use Powershell to list duplicate files in a folder structure that exist in one of the folders如何使用 Powershell 列出其中一个文件夹中存在的文件夹结构中的重复文件
【发布时间】:2016-05-04 01:32:34
【问题描述】:

我有一个源代码树,比如 c:\s,有很多子文件夹。其中一个子文件夹称为“c:\s\Includes”,它可以递归地包含一个或多个 .cs 文件。

我想确保 c:\s\Includes... 路径中的任何 .cs 文件都不存在于 c:\s 下的任何其他文件夹中,递归。

我编写了以下有效的 PowerShell 脚本,但我不确定是否有更简单的方法。我使用 PowerShell 的经验不到 24 小时,所以我觉得有更好的方法。

我可以假设至少使用了 PowerShell 3。

我会接受任何能改进我的脚本的答案,但我会等几天再接受答案。当我说“改进”时,我的意思是它使它更短、更优雅或具有更好的性能。

任何人的帮助将不胜感激。

当前代码:

$excludeFolder = "Includes"

$h = @{}
foreach ($i in ls $pwd.path *.cs -r -file | ? DirectoryName -notlike ("*\" + $excludeFolder + "\*")) { $h[$i.Name]=$i.DirectoryName }
ls ($pwd.path + "\" + $excludeFolder) *.cs -r -file | ? { $h.Contains($_.Name) } | Select @{Name="Duplicate";Expression={$h[$_.Name] + " has file with same name as " + $_.Fullname}}

【问题讨论】:

  • “让它更短,更优雅”你会接受更长更容易阅读吗?

标签: powershell powershell-3.0


【解决方案1】:

1

我盯着这个看了一会儿,决定不研究现有答案就写出来,但我已经看了一眼马特答案的第一句话提到Group-Object。经过一些不同的方法后,我得到了基本相同的答案,除了他的格式很长而且很健壮,正则表达式字符转义和设置变量,我的很简洁,因为你要求更短的答案,因为这更有趣。

$inc = '^c:\\s\\includes'
$cs = (gci -R 'c:\s' -File -I *.cs) | group name
$nopes = $cs |?{($_.Group.FullName -notmatch $inc)-and($_.Group.FullName -match $inc)}
$nopes | % {$_.Name; $_.Group.FullName}

示例输出:

someFile.cs
c:\s\includes\wherever\someFile.cs
c:\s\lib\factories\alt\someFile.cs
c:\s\contrib\users\aa\testing\someFile.cs

概念是:

  1. 获取整个源代码树中的所有 .cs 文件
  2. 将它们分成 {filename: {files which share this filename}} 的组
  3. 对于每个组,仅保留那些文件集包含路径与包含文件夹匹配的任何文件以及包含路径与包含文件夹不匹配的任何文件的文件。这一步涵盖
    1. 重复(如果文件仅在无法通过两个测试时才存在)
    2. 在 {includes/not-includes} 划分中重复,而不是在一个分支中重复
    3. 也可以处理三次、n 次重复。

编辑:我将^ 添加到$inc 以表示它必须在字符串的开头匹配,因此对于不匹配的路径,正则表达式引擎可能会更快地失败。也许这算作过早优化。


2

在相当密集的尝试之后,更清晰的答案的形状要容易得多:

  1. 获取所有文件,将它们拆分为包含、不包含数组。
  2. 嵌套 for 循环测试每个文件与其他所有文件。

更长,但非常写起来更快(虽然运行速度更慢),我想对于不知道它做什么的人来说更容易阅读。

$sourceTree = 'c:\\s'

$allFiles = Get-ChildItem $sourceTree -Include '*.cs' -File -Recurse

$includeFiles = $allFiles | where FullName -imatch "$($sourceTree)\\includes"
$otherFiles = $allFiles | where FullName -inotmatch "$($sourceTree)\\includes"

foreach ($incFile in $includeFiles) {
    foreach ($oFile in $otherFiles) {
        if ($incFile.Name -ieq $oFile.Name) {
            write "$($incFile.Name) clash"
            write "* $($incFile.FullName)"
            write "* $($oFile.FullName)"
            write "`n"
        }
    }
}

3

因为代码高尔夫很有趣。如果哈希表更快,那么这个测试更少的单行列呢...

$h=@{};gci c:\s -R -file -Filt *.cs|%{$h[$_.Name]+=@($_.FullName)};$h.Values|?{$_.Count-gt1-and$_-like'c:\s\includes*'}

编辑:此版本的解释:它采用与版本 1 大致相同的解决方案方法,但分组操作显式地发生在哈希表中。哈希表的形状变为:

$h = {
    'fileA.cs': @('c:\cs\wherever\fileA.cs', 'c:\cs\includes\fileA.cs'),
    'file2.cs': @('c:\cs\somewhere\file2.cs'),
    'file3.cs': @('c:\cs\includes\file3.cs', 'c:\cs\x\file3.cs', 'c:\cs\z\file3.cs')
}

它为所有 .cs 文件访问磁盘一次,迭代整个列表以构建哈希表。我不认为它可以做的工作比这少。

它使用+=,因此它可以将文件添加到该文件名的现有数组中,否则它将覆盖每个哈希表列表,并且它们将是一个仅用于最近看到的文件的项目。

它使用@() - 因为当它第一次遇到文件名时,$h[$_.Name] 不会返回任何内容,并且脚本首先需要将数组放入哈希表,而不是字符串。如果它是+=$_.FullName,那么第一个文件将作为字符串进入哈希表,而+= 下一次将进行字符串连接,这对我没有用。这通过强制每个文件成为单项数组来强制哈希表中的第一个文件开始一个数组。获得此结果的最少代码方法是使用+=@(..),但为每个文件创建一次性数组的混乱是不必要的工作。也许将其更改为创建更少数组的更长代码会有所帮助?

更改部分

%{$h[$_.Name]+=@($_.FullName)}

类似

%{if (!$h.ContainsKey($_.Name)){$h[$_.Name]=@()};$h[$_.Name]+=$_.FullName}

(我猜,对于最有可能是缓慢的 PowerShell 代码,我没有太多直觉,也没有测试过)。

之后,使用h.Values 不会再次遍历每个文件,而是遍历哈希表中的每个数组——每个唯一文件名一个。必须检查数组大小并修剪不重复项,但-and 操作会短路 - 当Count -gt 1 失败时,右侧检查路径名的位不会运行。

如果数组中有两个或更多文件,则-and $_ -like ... 将执行并进行模式匹配,以查看至少一个重复文件是否在includes 路径中。 (错误:如果所有重复项都在 c:\cs\includes 中而其他任何地方都没有,它仍然会显示它们。

--

4

这是经过编辑的版本 3,带有哈希表初始化调整,现在它跟踪 $s 中看到的文件,然后只考虑多次看到的文件。

$h=@{};$s=@{};gci 'c:\s' -R -file -Filt *.cs|%{if($h.ContainsKey($_.Name)){$s[$_.Name]=1}else{$h[$_.Name]=@()}$h[$_.Name]+=$_.FullName};$s.Keys|%{if ($h[$_]-like 'c:\s\includes*'){$h[$_]}}

假设它有效,那就是它的作用。

-- 编辑主题分支;我一直认为应该有一种方法可以使用 System.Data 命名空间中的东西来做到这一点。任何人都知道您是否可以在没有大量样板的情况下将System.Data.DataTable().ReadXML() 连接到gci | ConvertTo-Xml

【讨论】:

  • 你的短款很有效。它有多短有点酷。但是你知道,它比 Ansgar 的版本慢了大约 10 倍。我的意思是,这还不错。但是有字典的版本需要一秒钟,而这个版本大约需要 5 秒。
  • 你最后的较长我不认为做正确的事。我没有调试它,但是当我运行它时显示冲突不适用于 .cs 文件。此外,我没有看到任何地方引用了“.cs”。您在寻找all 冲突吗?因为它正在捕获 .dll 之类的东西。不过还是谢谢你这样做。
  • 在第二个中:哎呀,是的,我在我的测试中包含 * 而不是 *.cs 文件,并且在发布时错过了编辑它;我已经更新了帖子以纠正它。在我的第一个脚本中,速度比较很有趣。您会尝试将 gci -I *.cs 更改为 -Filter *.cs 看看有什么不同吗? (我希望它更快,但我推迟了,因为 -Filter 似乎有时会返回错误的结果)。
  • @zumalifeguard 我已经解决了这个问题(在测试代码 2 时,我确实错过了将 * 更改为 *.cs。代码 2 很慢,它只是编写速度快且结构简单。添加了一个第三个 hashtable one-liner,也是如此。
  • 我尝试了您更新的解决方案 #2,它有效。运行大约需要 7 秒,而原来的运行时间大约不到 1 秒。
【解决方案2】:

我会做或多或少相同的事情,除了我会从包含文件夹的内容构建哈希表,然后运行其他所有内容以检查重复项:

$root     = 'C:\s'
$includes = "$root\includes"

$includeList = @{}
Get-ChildItem -Path $includes -Filter '*.cs' -Recurse -File |
  % { $includeList[$_.Name] = $_.DirectoryName }

Get-ChildItem -Path $root -Filter '*.cs' -Recurse -File |
  ? { $_.FullName -notlike "$includes\*" -and $includeList.Contains($_.Name) } |
  % { "Duplicate of '{0}': {1}" -f $includeList[$_.Name], $_.FullName }

【讨论】:

    【解决方案3】:

    我对此印象不深,但我认为Group-Object 可能在这个问题中占有一席之地,所以我提出以下内容:

    $base = 'C:\s'
    $unique = "$base\includes"
    $extension = "*.cs"
    
    Get-ChildItem -Path $base -Filter $extension -Recurse | 
            Group-Object $_.Name | 
            Where-Object{($_.Count -gt 1) -and (($_.Group).FullName -match [regex]::Escape($unique))} | 
            ForEach-Object {
                $filename = $_.Name
                ($_.Group).FullName -notmatch [regex]::Escape($unique) | ForEach-Object{
                    "'{0}' has file with same name as '{1}'" -f (Split-Path $_),$filename
                }
            }
    

    收集所有带有扩展过滤器$extension 的文件。根据文件名对文件进行分组。然后在这些组中找到具有多个该特定文件的每个组,并且其中一个组成员至少在目录$unique 中。取出这些组并打印出所有不是来自唯一目录的文件。

    来自评论

    对于它的价值,这是我用来测试创建一堆文件的东西。 (我知道文件夹 9 是空的)

    $base = "E:\Temp\dev\cs"
    Remove-Item "$base\*" -Recurse -Force
    0..9 | %{[void](New-Item -ItemType directory "$base\$_")}
    1..1000 | %{
        $number = Get-Random -Minimum 1 -Maximum 100
        $folder = Get-Random -Minimum 0 -Maximum 9
        [void](New-Item -Path $base\$folder -ItemType File -Name "$number.txt" -Force)
    }
    

    【讨论】:

    • 我无法评论它是否有效,但是当我在我的源代码树(大约一千个源文件)上运行它时,脚本只会继续运行并且不会返回。我不确定它在做什么。已经有几分钟了。原始脚本运行几秒钟。 Ansgar 的改进使其更快。性能不是这个脚本的硬性要求,但它应该慢几个数量级。我很好奇你测试的数据集是什么——我可以尝试运行它,看看我是否能找出它有什么问题。
    • @zumalifeguard 并没有真正测试它的性能......想知道为什么需要这么长时间。我的测试库是 txt 文件,大约有 20 个,其中 6 个会被注册为重复文件。将扩展到 100 个,看看会发生什么。谢谢你告诉我。
    【解决方案4】:

    看了所有其他人之后,我想我会尝试不同的方法。

    $includes = "C:\s\includes"
    $root = "C:\s"
    
    # First script
    Measure-Command {
        [string[]]$filter = ls $includes -Filter *.cs -Recurse | % name
        ls $root -include $filter -Recurse -Filter *.cs | 
            Where-object{$_.FullName -notlike "$includes*"}
    }
    
    # Second Script
    Measure-Command {
        $filter2 = ls $includes -Filter *.cs -Recurse 
        ls $root -Recurse -Filter *.cs | 
            Where-object{$filter2.name -eq $_.name -and $_.FullName -notlike "$includes*"}
    }
    

    在我的第一个脚本中,我将所有包含文件放入一个字符串数组中。然后我使用该字符串数组作为 get-childitem 的包含参数。最后,我从结果中过滤掉了包含文件夹。

    在我的第二个脚本中,我枚举了所有内容,然后在管道之后进行过滤。

    删除测量命令以查看结果。我用它来检查速度。使用我的数据集,第一个数据集的速度提高了 40%。

    【讨论】:

    • 我喜欢使用包含过滤器的方法;它会在第一个脚本中同时包含 -include 和 -exclude 并且没有“where-object”部分吗?它几乎是ls 'c:\s' -Filt *.cs -File -R -Ex 'c:\s\includes' -I ([string[]](ls 'c:\s\includes' -File -R *.cs))|%FullName。快速测试看起来可行;这比我的答案要好。
    • -include-exclude 是骗人的。它们仅过滤文件名,不查看文件路径。
    【解决方案5】:
    $FilesToFind = Get-ChildItem -Recurse 'c:\s\includes' -File -Include *.cs | Select Name
    Get-ChildItem -Recurse C:\S -File -Include *.cs | ? { $_.Name -in $FilesToFind -and $_.Directory -notmatch '^c:\s\includes' } | Select Name, Directory
    
    1. 创建要查找的文件名列表。
    2. 查找列表中但不属于生成列表的目录的所有文件
    3. 打印他们的姓名和目录

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-09
      • 1970-01-01
      • 2015-11-13
      • 1970-01-01
      • 2011-07-19
      相关资源
      最近更新 更多