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.)

tshepang
  • 12,111
  • 21
  • 91
  • 136
ScriptSearcher
  • 303
  • 1
  • 3
  • 4

7 Answers7

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.

Keith Hill
  • 194,368
  • 42
  • 353
  • 369
  • 1
    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. :-) – Keith Hill Oct 15 '09 at 23:07
  • 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? – ScriptSearcher Oct 19 '09 at 22:10
  • 3
    The @() 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. :-) – Keith Hill Oct 19 '09 at 22:42
  • 1
    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`). – Alexander Groß Jul 22 '10 at 12:34
  • Or just specify `$_.FullName` to the `-LiteralPath` parameter. Piping effectively does the same thing. Thanks for the heads up. – Keith Hill Jul 22 '10 at 15:14
  • 1
    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: http://www.powershelladmin.com/wiki/Script_to_delete_empty_folders that is much more complicated but gives more immediate feedback. – MikeBaz - MSFT Aug 10 '12 at 18:05
  • 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](http://stackoverflow.com/a/32183279/1054378). – herzbube Aug 24 '15 at 13:50
  • 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! – wmac Dec 29 '15 at 14:24
  • @wmac - 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. – Keith Hill Dec 29 '15 at 23:41
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

Bogdan Calmac
  • 7,993
  • 6
  • 51
  • 64
  • 2
    It doesn't remove nested empty directories, do you have a workaround? – CharlesB Apr 17 '13 at 12:29
  • 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. – Amit Naidu Dec 13 '13 at 23:32
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
Doug Coburn
  • 2,485
  • 27
  • 24
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

bbo
  • 31
  • 1
  • 2
    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). – Peter Mortensen Oct 30 '15 at 16:06
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
Matt
  • 45,022
  • 8
  • 78
  • 119
John Weldon
  • 39,849
  • 11
  • 94
  • 127
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.
yzorg
  • 4,224
  • 3
  • 39
  • 57
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
cakan
  • 2,099
  • 5
  • 32
  • 42
ritikaadit2
  • 171
  • 1
  • 10