How to delete empty subfolders with PowerShell?
Asked Answered
A

7

30

I have a share that is a "junk drawer" for end-users. They are able to create folders and subfolders as they see fit. I need to implement a script to delete files created more than 31 days old.

I have that started with Powershell. I need to follow up the file deletion script by deleting subfolders that are now empty. Because of the nesting of subfolders, I need to avoid deleting a subfolder that is empty of files, but has a subfolder below it that contains a file.

For example:

  • FILE3a is 10 days old. FILE3 is 45 days old.
  • I want to clean up the structure removing files older than 30 days, and delete empty subfolders.
C:\Junk\subfolder1a\subfolder2a\FILE3a

C:\Junk\subfolder1a\subfolder2a\subfolder3a

C:\Junk\subfolder1a\subfolder2B\FILE3b

Desired result:

  • Delete: FILE3b, subfolder2B & subfolder3a.
  • Leave: subfolder1a, subfolder2a, and FILE3a.

I can recursively clean up the files. How do I clean up the subfolders without deleting subfolder1a? (The "Junk" folder will always remain.)

Alanealanine answered 15/10, 2009 at 22:29 Comment(0)
M
47

I would do this in two passes - deleting the old files first and then the empty dirs:

Get-ChildItem -recurse | Where {!$_.PSIsContainer -and `
$_.LastWriteTime -lt (get-date).AddDays(-31)} | Remove-Item -whatif

Get-ChildItem -recurse | Where {$_.PSIsContainer -and `
@(Get-ChildItem -Lit $_.Fullname -r | Where {!$_.PSIsContainer}).Length -eq 0} |
Remove-Item -recurse -whatif

This type of operation demos the power of nested pipelines in PowerShell which the second set of commands demonstrates. It uses a nested pipeline to recursively determine if any directory has zero files under it.

Multifid answered 15/10, 2009 at 22:56 Comment(9)
Yeah, they can be pretty darn useful. My hope is that for the next version of PoSh, we can dispense with the Where PSIsContainer tests. It would be soo much nicer if I could just request that directly from Get-ChildItem e.g. Get-ChildItem -Recurse -Container or Get-ChildItem -Recurse -Leaf. It could also be a very nice perf optimization for the provider to do that sort of filtering. One can dream. :-)Multifid
Thank you! This solution is exactly what I was after. The recursion checking the subfolders for remaining files was eluding me - not to mention the syntax and nesting. I am new to PowerShell - what is the '@' / how does that function?Alanealanine
The @(<some commands in here including pipelined commands>) syntax ensures that no matter whether we get no result ($null) or a single result or an array of results - the enternally visible result using @() will always be an array with either 0, 1 or N items in it. That is how I'm sure that the result will have a Length property and that the Length property is for an array (as opposed to a string, which might get returned if the result is a single, string value). Dynamic languages like this can be pretty powerful but they make you think. :-)Multifid
The deletion pipeline actually contains a slight bug. I tried it within a folder containing subfolders surrounded by []. The fix is pretty simple, @($_ | Get-ChildItem -Recurse | Where { !$_.PSIsContainer }) (instead of Get-ChildItem $_.FullName -Recurse).Kaleidoscopic
Or just specify $_.FullName to the -LiteralPath parameter. Piping effectively does the same thing. Thanks for the heads up.Multifid
One negative to this pipeline is that it's collecting the full set, then acting on it, as best I can tell.. so in a large tree, there's a very long delay as it scans everything before you see anything removed. There's a script here: powershelladmin.com/wiki/Script_to_delete_empty_folders that is much more complicated but gives more immediate feedback.Vanbuskirk
Re the problem with []: Neither your solution, nor the suggestion by @AlexanderGroß works for me. I'm on PowerShell 4.0, Windows 8.1. At the moment I'm using a rather ugly workaround that escapes the brackets (and uses -path instead of -LiteralPath). The workaround is presented in this SO answer.Consign
Don't you think that stupid message (performing remove file on ???) scares the crap out of people when they see all they files are listed with that statement? Such a fail!Christelchristen
@Christelchristen - You don't see the remove file messages if you get rid of the -WhatIf parameter. The point of -WhatIf is to show you what a potentially destructive command will do before actually executing it. I'm not sure why that should scare people unless it shows files they didn't want deleted. In that case, -WhatIf has done its job in preventing disaster.Multifid
B
9

In the spirit of the first answer, here is the shortest way to delete the empty directories:

ls -recurse | where {!@(ls -force $_.fullname)} | rm -whatif

The -force flag is needed for the cases when the directories have hidden folders, like .svn

Barram answered 20/11, 2010 at 18:29 Comment(2)
It doesn't remove nested empty directories, do you have a workaround?Diabolic
He means that in a directory structure md a\b\c where c is empty and b also has no other files, this will only delete c. He needs the entire empty \b\c subtree gone like Keith's script does.Alamode
P
5

This will sort subdirectories before parent directories working around the empty nested directory problem.

dir -Directory -Recurse |
    %{ $_.FullName} |
    sort -Descending |
    where { !@(ls -force $_) } |
    rm -WhatIf
Psychasthenia answered 31/7, 2015 at 22:20 Comment(0)
H
3

Adding on to the last one:

while (Get-ChildItem $StartingPoint -recurse | where {!@(Get-ChildItem -force $_.fullname)} | Test-Path) {
    Get-ChildItem $StartingPoint -recurse | where {!@(Get-ChildItem -force $_.fullname)} | Remove-Item
}

This will make it complete where it will continue searching to remove any empty folders under the $StartingPoint

Hope answered 15/5, 2012 at 14:47 Comment(1)
Which answer are you referring to? References to relative positions of answers are not reliable as they depend on the view (votes/newest/active) and changing of the accepted answer and change over time (for votes, active, and accepted state).Photodynamics
M
2

To remove files older than 30 days:

get-childitem -recurse |
    ? {$_.GetType() -match "FileInfo"} |
    ?{ $_.LastWriteTime -lt [datetime]::now.adddays(-30) }  |
    rm -whatif

(Just remove the -whatif to actually perform.)

Follow up with:

 get-childitem -recurse |
     ? {$_.GetType() -match "DirectoryInfo"} |
     ?{ $_.GetFiles().Count -eq 0 -and $_.GetDirectories().Count -eq 0 } |
     rm -whatif
Methane answered 15/10, 2009 at 22:37 Comment(2)
If you want to match on a type, you can use the -is operator e.g. $_ -is [IO.FileInfo].Multifid
Those trailing backticks are redundant. Code works fine without them.Warden
G
2

I needed some enterprise-friendly features. Here is my take.

I started with code from other answers, then added a JSON file with original folder list (including file count per folder). Removed the empty directories and log those.

https://gist.github.com/yzorg/e92c5eb60e97b1d6381b

param (
    [switch]$Clear
)

# if you want to reload a previous file list
#$stat = ConvertFrom-Json (gc dir-cleanup-filecount-by-directory.json -join "`n")

if ($Clear) { 
    $stat = @() 
} elseif ($stat.Count -ne 0 -and (-not "$($stat[0].DirPath)".StartsWith($PWD.ProviderPath))) {
    Write-Warning "Path changed, clearing cached file list."
    Read-Host -Prompt 'Press -Enter-'
    $stat = @() 
}

$lineCount = 0
if ($stat.Count -eq 0) {
    $stat = gci -Recurse -Directory | %{  # -Exclude 'Visual Studio 2013' # test in 'Documents' folder

        if (++$lineCount % 100 -eq 0) { Write-Warning "file count $lineCount" }

        New-Object psobject -Property @{ 
            DirPath=$_.FullName; 
            DirPathLength=$_.FullName.Length;
            FileCount=($_ | gci -Force -File).Count; 
            DirCount=($_ | gci -Force -Directory).Count
        }
    }
    $stat | ConvertTo-Json | Out-File dir-cleanup-filecount-by-directory.json -Verbose
}

$delelteListTxt = 'dir-cleanup-emptydirs-{0}-{1}.txt' -f ((date -f s) -replace '[-:]','' -replace 'T','_'),$env:USERNAME

$stat | 
    ? FileCount -eq 0 | 
    sort -property @{Expression="DirPathLength";Descending=$true}, @{Expression="DirPath";Descending=$false} |
    select -ExpandProperty DirPath | #-First 10 | 
    ?{ @(gci $_ -Force).Count -eq 0 } | %{
        Remove-Item $_ -Verbose # -WhatIf  # uncomment to see the first pass of folders to be cleaned**
        $_ | Out-File -Append -Encoding utf8 $delelteListTxt
        sleep 0.1
    }

# ** - The list you'll see from -WhatIf isn't a complete list because parent folders
#      might also qualify after the first level is cleaned.  The -WhatIf list will 
#      show correct breath, which is what I want to see before running the command.
Gidgetgie answered 14/3, 2014 at 16:47 Comment(0)
G
1

This worked for me.

$limit = (Get-Date).AddDays(-15) 

$path = "C:\Some\Path"

Delete files older than the $limit:

Get-ChildItem -Path $path -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $limit } | Remove-Item -Force

Delete any empty directories left behind after deleting the old files:

Get-ChildItem -Path $path -Recurse -Force | Where-Object { $_.PSIsContainer -and (Get-ChildItem -Path $_.FullName -Recurse -Force | Where-Object { !$_.PSIsContainer }) -eq $null } | Remove-Item -Force -Recurse
Gymkhana answered 17/8, 2016 at 13:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.