如何递归删除PowerShell中的所有空文件夹?

Posted

技术标签:

【中文标题】如何递归删除PowerShell中的所有空文件夹?【英文标题】:How to recursively remove all empty folders in PowerShell? 【发布时间】:2015-04-22 06:37:25 【问题描述】:

我需要递归删除 PowerShell 中特定文件夹的所有空文件夹(检查任何级别的文件夹和子文件夹)。

目前我使用这个脚本没有成功。

你能告诉我如何解决它吗?

$tdc='C:\a\c\d\'
$a = Get-ChildItem $tdc -recurse | Where-Object $_.PSIsContainer -eq $True
$a | Where-Object $_.GetFiles().Count -eq 0 | Select-Object FullName

我在 Windows 8.1 版本上使用 PowerShell。

【问题讨论】:

在我看来,您唯一缺少的就是实际删除文件夹。添加了 -WhatIf... $a | Where-Object $_.GetFiles().Count -eq 0 | select -expand FullName | remove-item -whatif 感谢您的评论,但是使用您的代码,我得到一个弹出窗口说...有孩子并且没有指定 Recurse 参数。我需要它避免这个警告。 Remove-Item -Recurse 应该可以解决问题。但是,如果文件夹 ~\a\b 和子文件夹 ~\a\b\c 中没有文件,您将收到一条错误消息,指出在为 ~a\b\c 运行 $_.GetFiles()-part 时找不到路径,即如果 ~\a\b 已被删除,则为 true。 这就是 powershell 再次荒谬的地方。为此有一个 ISO 标准 C 函数:rmdir。这是 Windows 平台上的实现(作为 OP 标记的 Windows):docs.microsoft.com/en-us/cpp/c-runtime-library/reference/…。诸如再次列出每个目录内容以检查其空目录是否在性能方面真的很差的解决方法。 我在这里遇到了很多问题。请,如果您使用 .FullName,请使用 Get-ChildItem 的 -LiteralPath !!! 【参考方案1】:

你可以用这个:

$tdc="C:\a\c\d"
$dirs = gci $tdc -directory -recurse | Where  (gci $_.fullName).count -eq 0  | select -expandproperty FullName
$dirs | Foreach-Object  Remove-Item $_ 

$dirs 将是一个由Get-ChildItem 命令过滤后返回的空目录数组。然后您可以遍历它以删除项目。

更新

如果您想删除包含空目录的目录,您只需要继续运行脚本,直到它们全部消失。您可以循环直到$dirs 为空:

$tdc="C:\a\c\d"
do 
  $dirs = gci $tdc -directory -recurse | Where  (gci $_.fullName).count -eq 0  | select -expandproperty FullName
  $dirs | Foreach-Object  Remove-Item $_ 
 while ($dirs.count -gt 0)

如果您想确保隐藏的文件和文件夹也将被删除,请包含 -Force 标志:

do 
  $dirs = gci $tdc -directory -recurse | Where  (gci $_.fullName -Force).count -eq 0  | select -expandproperty FullName
  $dirs | Foreach-Object  Remove-Item $_ 
 while ($dirs.count -gt 0)

【讨论】:

谢谢,但我发现了一个可能的问题,它适用于文件夹的第一级嵌套。我需要深入任何层面。请您仔细检查一下吗? 我不明白你的意思...-recurse 将确保它一路遍历。如果这就是您的意思,它不会递归删除包含空文件夹的文件夹,因为它们不是空的。 感谢您的评论,我需要...删除包含空文件夹的文件夹.....您认为可以调整您的脚本吗?有没有可能? @Kayasax 这不起作用。 Count 不是 FileInfo 对象的属性。这是gci 返回的数组的属性,因此命令需要放在括号中 wonderul,我只需将 -verbose 添加到 remove-item 即可知道要删除的内容【参考方案2】:
ls c:\temp -rec |% if ($_.PSIsContainer -eq $True) if ( (ls $_.fullname -rec | measure |select -expand count ) -eq "0"  ) ri $_.fullname -whatif      

【讨论】:

这个脚本到底做了什么?它试图访问 c:\windows\system32 中的文件 它删除 c:\temp 文件夹中的空目录 感谢您的解释。但是,如果 c:\temp 不存在,它似乎会尝试访问整个硬盘驱动器,因此最好先检查目录是否存在。 你必须使用-LiteralPath和.FullName!!!【参考方案3】:

在查看此类问题时,您需要牢记一些关键事项:

    Get-ChildItem -Recurse 执行头递归,这意味着它在穿过树时一旦找到文件夹就会返回它们。由于您要删除空文件夹,并且在删除空文件夹后如果它们为空,还要删除它们的父文件夹,因此您需要使用尾递归,它处理从最深的子文件夹到根目录的文件夹。通过使用尾递归,无需重复调用删除空文件夹的代码——一次调用即可完成所有工作。 Get-ChildItem 默认不返回隐藏文件或文件夹。因此,您需要采取额外的步骤来确保不会删除看似空但包含隐藏文件或文件夹的文件夹。 Get-ItemGet-ChildItem 都有一个 -Force 参数,可用于检索隐藏文件或文件夹以及可见文件或文件夹。

考虑到这些要点,这是一个使用尾递归并正确跟踪隐藏文件或文件夹的解决方案,确保在隐藏文件夹为空时将其删除,并确保保留可能包含一个或多个隐藏文件夹文件。

首先这是完成这项工作的脚本块(匿名函数):

# A script block (anonymous function) that will remove empty folders
# under a root folder, using tail-recursion to ensure that it only
# walks the folder tree once. -Force is used to be able to process
# hidden files/folders as well.
$tailRecursion = 
    param(
        $Path
    )
    foreach ($childDirectory in Get-ChildItem -Force -LiteralPath $Path -Directory) 
        & $tailRecursion -Path $childDirectory.FullName
    
    $currentChildren = Get-ChildItem -Force -LiteralPath $Path
    $isEmpty = $currentChildren -eq $null
    if ($isEmpty) 
        Write-Verbose "Removing empty folder at path '$Path'." -Verbose
        Remove-Item -Force -LiteralPath $Path
    

如果您想测试它,这里的代码将创建有趣的测试数据(确保您还没有文件夹 c:\a,因为它将被删除):

# This creates some test data under C:\a (make sure this is not
# a directory you care about, because this will remove it if it
# exists). This test data contains a directory that is hidden
# that should be removed as well as a file that is hidden in a
# directory that should not be removed.
Remove-Item -Force -Path C:\a -Recurse
New-Item -Force -Path C:\a\b\c\d -ItemType Directory > $null
$hiddenFolder = Get-Item -Force -LiteralPath C:\a\b\c
$hiddenFolder.Attributes = $hiddenFolder.Attributes -bor [System.IO.FileAttributes]::Hidden
New-Item -Force -Path C:\a\b\e -ItemType Directory > $null
New-Item -Force -Path C:\a\f -ItemType Directory > $null
New-Item -Force -Path C:\a\f\g -ItemType Directory > $null
New-Item -Force -Path C:\a\f\h -ItemType Directory > $null
Out-File -Force -FilePath C:\a\f\test.txt -InputObject 'Dummy file'
Out-File -Force -FilePath C:\a\f\h\hidden.txt -InputObject 'Hidden file'
$hiddenFile = Get-Item -Force -LiteralPath C:\a\f\h\hidden.txt
$hiddenFile.Attributes = $hiddenFile.Attributes -bor [System.IO.FileAttributes]::Hidden

这里是你如何使用它。请注意,如果在删除其下的所有空文件夹后该文件夹最终为空,这将删除顶部文件夹(本示例中的 C:\a 文件夹,如果您使用上面的脚本生成测试数据,则会创建该文件夹。

& $tailRecursion -Path 'C:\a'

【讨论】:

这是一个绝妙的答案。编辑挂起以说明代码也会删除 start 文件夹,如果该文件夹为空。 感谢 Richard,感谢您的积极反馈和编辑 -- 绝对值得注意的是,如果 root/start 文件夹为空,它也可能会被删除。 @KirkMunro 我在 technet 和其他地方遇到了许多无法正常工作的答案,你得到了我迄今为止看到的唯一一个好的答案。 我赞同@Hart-CO 所说的话。这是我发现的唯一正确的答案。感谢分享! 请注意,这也会删除根文件夹,我的贡献是验证 RootPath Path,然后删除 if ($IsEmpty && $Path -ne $RootPath) Write-Verbose "删除路径 '$Path' 处的空文件夹。” -详细 【参考方案4】:

递归删除空子目录也可以使用“For循环”来完成。

在开始之前,让我们在 $HOME\Desktop\Test 中创建一些子目录和文本文件

MD $HOME\Desktop\Test\0\1\2\3\4\5 
MD $HOME\Desktop\Test\A\B\C\D\E\F
MD $HOME\Desktop\Test\A\B\C\DD\EE\FF
MD $HOME\Desktop\Test\Q\W\E\R\T\Y
MD $HOME\Desktop\Test\Q\W\E\RR
"Hello World" > $HOME\Desktop\Test\0\1\Text1.txt
"Hello World" > $HOME\Desktop\Test\A\B\C\D\E\Text2.txt
"Hello World" > $HOME\Desktop\Test\A\B\C\DD\Text3.txt
"Hello World" > $HOME\Desktop\Test\Q\W\E\RR\Text4.txt

首先,将以下脚本块存储在变量 $SB 中。稍后可以使用 &SB 命令调用该变量。 &SB 命令将输出 $HOME\Desktop\Test 中包含的空子目录列表

$SB = 
    Get-ChildItem $HOME\Desktop\Test -Directory -Recurse |
    Where-Object (Get-ChildItem $_.FullName -Force).Count -eq 0

注意:-Force 参数非常重要。它确保包含隐藏文件和子目录但为空的目录不会在“For循环”中删除。

现在使用“For 循环”递归删除 $HOME\Desktop\Test 中的空子目录

For ($Empty = &$SB ; $Empty -ne $null ; $Empty = &$SB) Remove-Item (&$SB).FullName

经测试可在 PowerShell 4.0 上运行

【讨论】:

您能解释一下“For Loop”位吗?为什么你一直调用返回所有空文件夹集合的 $SB? 当目录为空时,我遇到了(Get-ChildItem $_.FullName -Force).Count 评估为空字符串而不是“0”的问题(我猜这发生在 Powershell 2.0 之前)。在这种情况下,请将 where 语句的结果用 @() 括起来,如下所示:@((Get-ChildItem $_.FullName -Force)).Count。这会将语句的结果变成一个数组,并且“count”应该总是返回一个数字。 @rob。为了清楚起见,我正在重新发布。以下示例将解释 for 循环的用途:目录 A (DirA) 仅包含空子目录,没有其他内容。一旦删除其中包含的空子目录,DirA 本身将变为空。 DirA 当前未存储在变量 $SB 中,因为在 DirA 为空之前调用了 &$SB。因此,每次删除空子目录时都必须调用 &$SB,因为可能有新清空的目录需要添加到 $SB。 你必须使用-LiteralPath和.FullName!!!【参考方案5】:

假设您在感兴趣的父文件夹中

gci . -Recurse -Directory | % if(!(gci -Path $_.FullName)) ri -Force -Recurse $_.FullName

对于您使用 $tdc 的情况,它会是

gci $tdc -Recurse -Directory | % if(!(gci -Path $_.FullName)) ri -Force -Recurse $_.FullName

【讨论】:

你必须使用-LiteralPath和.FullName!!!【参考方案6】:

如果您只是想确保只删除可能包含子文件夹但其自身及其子文件夹中没有文件的文件夹,这可能是一种更简单、更快捷的方法。

$Empty = Get-ChildItem $Folder -Directory -Recurse |
Where-Object (Get-ChildItem $_.FullName -File -Recurse -Force).Count -eq 0

Foreach ($Dir in $Empty)

    if (test-path $Dir.FullName)
    Remove-Item -LiteralPath $Dir.FullName -recurse -force

【讨论】:

你必须使用-LiteralPath和.FullName!!!【参考方案7】:

我改编了 RichardHowells 的剧本。 如果有 thumbs.db,它不会删除该文件夹。

##############
# Parameters #
##############
param(
    $Chemin = "" ,  # Path to clean
    $log = ""       # Logs path
)




###########
# Process #
###########


if (($Chemin -eq "") -or ($log-eq "") )

    Write-Error 'Parametres non reseignes - utiliser la syntaxe : -Chemin "Argument"  -log "argument 2" ' -Verbose 
    Exit




#loging 
$date = get-date -format g
Write-Output "begining of cleaning folder : $chemin at $date" >> $log
Write-Output "------------------------------------------------------------------------------------------------------------" >> $log


<########################################################################
    define a script block that will remove empty folders under a root folder, 
    using tail-recursion to ensure that it only walks the folder tree once. 
    -Force is used to be able to process hidden files/folders as well.
########################################################################>
$tailRecursion = 
    param(
        $Path
    )
    foreach ($childDirectory in Get-ChildItem -Force -LiteralPath $Path -Directory) 
        & $tailRecursion -Path $childDirectory.FullName
    
    $currentChildren = Get-ChildItem -Force -LiteralPath $Path
    Write-Output $childDirectory.FullName



    <# Suppression des fichiers Thumbs.db #>
    Foreach ( $file in $currentchildren )
    
        if ($file.name -notmatch "Thumbs.db")break
        if ($file.name -match "Thumbs.db")
            Remove-item -force -LiteralPath $file.FullName

    



    $currentChildren = Get-ChildItem -Force -LiteralPath $Path
    $isEmpty = $currentChildren -eq $null
    if ($isEmpty) 
        $date = get-date -format g
        Write-Output "Removing empty folder at path '$Path'.  $date" >> $log
        Remove-Item -Force -LiteralPath $Path
    


# Invocation of the script block
& $tailRecursion -Path $Chemin

#loging 
$date = get-date -format g
Write-Output "End of cleaning folder : $chemin at $date" >> $log
Write-Output "------------------------------------------------------------------------------------------------------------" >> $log

【讨论】:

【参考方案8】:

除非您还想删除嵌套超过一个文件夹深度的文件,否则我不会把 cmets/1st 帖子放在心上。您最终将删除可能包含可能包含文件的目录的目录。这样更好:

$FP= "C:\Temp\"

$dirs= Get-Childitem -LiteralPath $FP -directory -recurse

$Empty= $dirs | Where-Object $_.GetFiles().Count -eq 0 **-and** $_.GetDirectories().Count -eq 0 | 

Select-Object FullName

上述检查以确保目录实际上是空的,而 OP 仅检查以确保没有文件。这反过来会导致紧挨着几个文件夹的文件也被删除。

您可能需要运行上述几次,因为它不会删除具有嵌套 Dirs 的 Dirs。所以它只删除最深的层次。所以循环它直到它们都消失了。

我不做的其他事情是使用 -force 参数。这是设计使然。如果实际上 remove-item 遇到了一个非空的目录,您希望被提示为额外的安全。

【讨论】:

$.getfiles 和 $.getdirectories() 应该是 $_.getfiles 和 $_.getdirectories()。您可能想要添加最后一行 $empty.fullname |删除项目 -whatif 你是对的。我忘记将我的帖子标记为代码,并且该网站出于某种原因删除了这些字符。我已更正。谢谢【参考方案9】:
Get-ChildItem $tdc -Recurse -Force -Directory | 
    Sort-Object -Property FullName -Descending |
    Where-Object  $($_ | Get-ChildItem -Force | Select-Object -First 1).Count -eq 0  |
    Remove-Item -Verbose

这里唯一新颖的贡献是使用Sort-Object 按目录的全名反向排序。这将确保我们始终在处理父母之前处理孩子(即 Kirk Munro 的回答所描述的“尾递归”)。这使它递归地删除空文件夹。

顺便说一句,我不确定Select-Object -First 1 是否会显着提高性能,但它可能会。

【讨论】:

它对我有用。我认为这比其他答案更好。 这应该是最好的答案。效果很好。 如果您想知道,Select-Object -First 1 实际上会在找到第一个文件时使嵌套的Get-ChildItem 停止。因此,它肯定会提高包含许多项目的目录的性能。如果目录为空,您可以将其稍微简化为Get-ChildItem 返回$null,在布尔上下文中计算为$falseWhere-Object ! ($_ | Get-ChildItem -Force | Select-Object -First 1) 对于大型目录结构,此方法将消耗比实际尾递归更多的内存,因为它将所有子目录的信息加载到内存中以便能够对它们进行排序。此外,该方法只会在遍历整个目录结构后报告删除进度,而实际的尾递归将更早报告进度,只要在遍历过程中找到第一个空目录。 @zett42 我认为如果您的目录结构太大以至于您甚至无法仅枚举内存中的目录足够长的时间来对它们进行排序,那么您就会遇到需要解决的问题已经分解成几个部分,或者可能已经需要磁盘存储。递归函数,IMX,更容易遇到内存问题,因为系统不仅要存储内存中的对象,还要存储仍在执行的父函数堆栈。【参考方案10】:

这样的东西对我有用。该脚本删除空文件夹和仅包含文件夹的文件夹(无文件,无隐藏文件)。

$items = gci -LiteralPath E:\ -Directory -Recurse
$dirs = [System.Collections.Generic.HashSet[string]]::new([string[]]($items |% FullName))
for (;;) 
    $remove = $dirs |?  (gci -LiteralPath $_ -Force).Count -eq 0 
    if ($remove) 
        $remove | rm
        $dirs.ExceptWith( [string[]]$remove )
    
    else 
        break
    

【讨论】:

【参考方案11】:
$files = Get-ChildItem -Path c:\temp -Recurse -Force | where psiscontainer ; [array]::reverse($files)

[Array]::reverse($files) 将反转您的项目,因此您首先获得层次结构中最低的文件。 我使用它来处理文件路径过长的文件名,然后再删除它们。

【讨论】:

【参考方案12】:

这是一个简单的方法

dir -Directory | ?  (dir $_).Count -eq 0  | Remove-Item

【讨论】:

【参考方案13】:

只是想我会在这里为已经很长的答案列表做出贡献。

许多答案都有一些怪癖,比如需要运行不止一次。其他的对于普通用户来说过于复杂(比如使用尾递归来防止重复扫描等)。

这是我多年来一直在使用的一个非常简单的单线,效果很好......

它不考虑隐藏文件/文件夹,但您可以通过将 -Force 添加到 Get-ChildItem 命令来解决此问题

这是长的、完全限定的 cmdlet 名称版本:

Get-ChildItem -Recurse -Directory | ?  -Not ($_.EnumerateFiles('*',1) | Select-Object -First 1)  | Remove-Item -Recurse

所以基本上......事情是这样的:

Get-ChildItem -Recurse -Directory - 开始递归扫描查找目录 $_.EnumerateFiles('*',1) - 对于每个目录...枚举文件 EnumerateFiles 将输出它的发现,GetFiles 将在完成后输出....至少,这就是它应该在 .NET 中工作的方式...由于某种原因在 PowerShell 中GetFiles 开始吐痰马上出去。但我仍然使用EnumerateFiles,因为在测试中它确实更快。 ('*',1) 表示递归查找所有文件。 | Select-Object -First 1 - 在找到的第一个文件处停止 这很难测试它有多大帮助。在某些情况下,它提供了极大的帮助,而在其他情况下,它根本没有帮助,在某些情况下,它会稍微减慢它的速度。所以我真的不知道。我想这是可选的。 | Remove-Item -Recurse - 递归删除目录(确保删除包含空子目录的目录)

如果你计算字符,这可以缩短为:

ls -s -ad | ?  -Not ($_.EnumerateFiles('*',1) | select -First 1)  | rm -Recurse
-s - -Recurse 的别名 -ad - -Directory 的别名

如果你真的不关心性能,因为你没有那么多文件......更是如此:

ls -s -ad | ? !($_.GetFiles('*',1)) | rm -Recurse

旁注: 在玩这个的过程中,我开始使用Measure-Command 对具有数百万个文件和数千个目录的服务器测试各种版本。

这比我一直使用的命令要快(上图):

(gi .).EnumerateDirectories('*',1) | ? -Not $_.EnumerateFiles('*',1)  | rm -Recurse

【讨论】:

-Not 运算符后是否缺少括号?例如? -Not (&lt;enumerate and select one&gt;) @tkokasih ,呵呵,你似乎是对的......我不知道为什么这是必要的,或者我在发帖时怎么没有注意到这一点。但我会编辑帖子。谢谢! @tkokasih 似乎只有在使用EnumerateFiles 时才需要这样做,如果您将脚本切换为使用GetFiles,那么即使使用select -First 1,也不需要括号,非常奇怪行为。 这不需要将文件夹传递给它吗?我试过了,但没用,但 Get-ChildItem $rootPath -Recurse 而不是 Get-ChildItem -Recurse 工作。 @Ste 不应该。提供目录只是告诉它您要清理的位置。不提供目录表示清理当前目录。【参考方案14】:

这将删除指定目录$tdc 中的所有空文件夹。 由于不需要多次运行,因此速度也快了很多。

    $tdc = "x:\myfolder" # Specify the root folder
    gci $tdc -Directory -Recurse `
        | Sort-Object  $_.FullName.Length  -Descending `
        | ?  $_.GetFiles().Count -eq 0  `
        | % 
            if ($_.GetDirectories().Count -eq 0)  
                Write-Host " Removing $($_.FullName)"
                $_.Delete()
                
            

【讨论】:

【参考方案15】:
#By Mike Mike Costa Rica
$CarpetasVacias = Get-ChildItem -Path $CarpetaVer -Recurse -Force -Directory | Where (gci $_.fullName).count -eq 0 | select Fullname,Name,LastWriteTime
$TotalCarpetas = $CarpetasVacias.Count
$CountSu = 1
ForEach ($UnaCarpeta in $CarpetasVacias)
    $RutaCarp = $UnaCarpeta.Fullname
    Remove-Item -Path $RutaCarp -Force -Confirm:$False -ErrorAction Ignore
    $testCar = Test-Path $RutaCarp
    if($testCar -eq $true) 
        $Datem = (Get-Date).tostring("MM-dd-yyyy HH:mm:ss")
        Write-Host "$Datem ---> $CountSu de $TotalCarpetas Carpetas Error Borrando Directory: $RutaCarp" -foregroundcolor "red"
    else
        $Datem = (Get-Date).tostring("MM-dd-yyyy HH:mm:ss")
        Write-Host "$Datem ---> $CountSu de $TotalCarpetas Carpetas Correcto Borrando Directory: $RutaCarp" -foregroundcolor "gree"
    
    $CountSu += 1

【讨论】:

虽然此代码可以解决问题,including an explanation 说明如何以及为什么解决问题将真正有助于提高您的帖子质量,并可能导致更多的赞成票。请记住,您正在为将来的读者回答问题,而不仅仅是现在提出问题的人。请edit您的答案添加解释并说明适用的限制和假设。

以上是关于如何递归删除PowerShell中的所有空文件夹?的主要内容,如果未能解决你的问题,请参考以下文章

如何一次性删除通用列表中的所有空元素?

从末尾删除向量中的所有空元素

当行不消失时删除data.frame中的所有空列和行

windows批量删除当前目录以及子目录的所有空文件夹

如何使用 PowerShell 递归删除具有特定名称的文件夹?

如何将默认值 0 赋予 Handsontable 中的所有空单元格?