5 minute read

I thought I’d share some PowerShell (PS) functions that I wrote for some clean-up scripts at work. I use these functions to delete files older than a certain date. Note that these functions require PS v3.0; slower PS v2.0 compatible functions are given at the end of this article.

# Function to remove all empty directories under the given path.
# If -DeletePathIfEmpty is provided the given Path directory will also be deleted if it is empty.
# If -OnlyDeleteDirectoriesCreatedBeforeDate is provided, empty folders will only be deleted if they were created before the given date.
# If -OnlyDeleteDirectoriesNotModifiedAfterDate is provided, empty folders will only be deleted if they have not been written to after the given date.
function Remove-EmptyDirectories([parameter(Mandatory)][ValidateScript({Test-Path $_})][string] $Path, [switch] $DeletePathIfEmpty, [DateTime] $OnlyDeleteDirectoriesCreatedBeforeDate = [DateTime]::MaxValue, [DateTime] $OnlyDeleteDirectoriesNotModifiedAfterDate = [DateTime]::MaxValue, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force -Directory | Where-Object { $null -eq (Get-ChildItem -Path $_.FullName -Recurse -Force -File) } |
        Where-Object { $_.CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate -and $_.LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate } |
        Sort-Object { $_.FullName } -Descending | # Sort directories to ensure we delete child directories before parent directories.
        ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }

    # If we should delete the given path when it is empty, and it is a directory, and it is empty, and it meets the date requirements, then delete it.
    if ($DeletePathIfEmpty -and (Test-Path -Path $Path -PathType Container) -and $null -eq (Get-ChildItem -Path $Path -Force) -and
        ((Get-Item $Path).CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate) -and ((Get-Item $Path).LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate))
    { if ($OutputDeletedPaths) { Write-Output $Path } Remove-Item -Path $Path -Force -WhatIf:$WhatIf }
}

# Function to remove all files in the given Path that were created before the given date, as well as any empty directories that may be left behind.
function Remove-FilesCreatedBeforeDate([parameter(Mandatory)][ValidateScript({Test-Path $_})][string] $Path, [parameter(Mandatory)][DateTime] $DateTime, [switch] $DeletePathIfEmpty, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force -File | Where-Object { $_.CreationTime -lt $DateTime } |
        ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }
    Remove-EmptyDirectories -Path $Path -DeletePathIfEmpty:$DeletePathIfEmpty -OnlyDeleteDirectoriesCreatedBeforeDate $DateTime -OutputDeletedPaths:$OutputDeletedPaths -WhatIf:$WhatIf
}

# Function to remove all files in the given Path that have not been modified after the given date, as well as any empty directories that may be left behind.
function Remove-FilesNotModifiedAfterDate([parameter(Mandatory)][ValidateScript({Test-Path $_})][string] $Path, [parameter(Mandatory)][DateTime] $DateTime, [switch] $DeletePathIfEmpty, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force -File | Where-Object { $_.LastWriteTime -lt $DateTime } |
        ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }
    Remove-EmptyDirectories -Path $Path -DeletePathIfEmpty:$DeletePathIfEmpty -OnlyDeleteDirectoriesNotModifiedAfterDate $DateTime -OutputDeletedPaths:$OutputDeletedPaths -WhatIf:$WhatIf
}

Download File

The Remove-EmptyDirectories function removes all empty directories under the given path, and optionally (via the DeletePathIfEmpty switch) the path directory itself if it is empty after cleaning up the other directories. It also takes a couple parameters that may be specified if you only want to delete the empty directories that were created before a certain date, or that haven’t been written to since a certain date.

The Remove-FilesCreatedBeforeDate and Remove-FilesNotModifiedAfterDate functions are very similar to each other. They delete all files under the given path whose Created Date or Last Written To Date, respectfully, is less than the given DateTime. They then call the Remove-EmptyDirectories function with the provided date to clean up any left over empty directories.

To call the last 2 functions, just provide the path to the file/directory that you want it to delete if older than the given date-time. Here are some examples of calling all the functions:

# Delete all files created more than 2 days ago.
Remove-FilesCreatedBeforeDate -Path "C:\Some\Directory" -DateTime ((Get-Date).AddDays(-2)) -DeletePathIfEmpty

# Delete all files that have not been updated in 8 hours.
Remove-FilesNotModifiedAfterDate -Path "C:\Another\Directory" -DateTime ((Get-Date).AddHours(-8))

# Delete a single file if it is more than 30 minutes old.
Remove-FilesCreatedBeforeDate -Path "C:\Another\Directory\SomeFile.txt" -DateTime ((Get-Date).AddMinutes(-30))

# Delete all empty directories in the Temp folder, as well as the Temp folder itself if it is empty.
Remove-EmptyDirectories -Path "C:\SomePath\Temp" -DeletePathIfEmpty

# Delete all empty directories created after Jan 1, 2014 3PM.
Remove-EmptyDirectories -Path "C:\SomePath\WithEmpty\Directories" -OnlyDeleteDirectoriesCreatedBeforeDate ([DateTime]::Parse("Jan 1, 2014 15:00:00"))

# See what files and directories would be deleted if we ran the command.
Remove-FilesCreatedBeforeDate -Path "C:\SomePath\Temp" -DateTime (Get-Date) -DeletePathIfEmpty -WhatIf

# Delete all files and directories in the Temp folder, as well as the Temp folder itself if it is empty, and output all paths that were deleted.
Remove-FilesCreatedBeforeDate -Path "C:\SomePath\Temp" -DateTime (Get-Date) -DeletePathIfEmpty -OutputDeletedPaths

Notice that I am using Get-Date to get the current date and time, and then subtracting the specified amount of time from it in order to get a date-time relative to the current time; you can use any valid DateTime though, such as a hard-coded date of January 1st, 2014 3PM.

I use these functions in some scripts that we run nightly via a scheduled task in Windows. Hopefully you find them useful too.

PowerShell v2.0 Compatible Functions

As promised, here are the slower PS v2.0 compatible functions. The main difference is that they use $_.PSIsContainer in the Where-Object clause rather than using the –File / –Directory Get-ChildItem switches. The Measure-Command cmdlet shows that using the switches is about 3x faster than using the where clause, but since we are talking about milliseconds here you likely won’t notice the difference unless you are traversing a large file tree (which I happen to be for my scripts that we use to clean up TFS builds).

# Function to remove all empty directories under the given path.
# If -DeletePathIfEmpty is provided the given Path directory will also be deleted if it is empty.
# If -OnlyDeleteDirectoriesCreatedBeforeDate is provided, empty folders will only be deleted if they were created before the given date.
# If -OnlyDeleteDirectoriesNotModifiedAfterDate is provided, empty folders will only be deleted if they have not been written to after the given date.
function Remove-EmptyDirectories([parameter(Mandatory=$true)][ValidateScript({Test-Path $_})][string] $Path, [switch] $DeletePathIfEmpty, [DateTime] $OnlyDeleteDirectoriesCreatedBeforeDate = [DateTime]::MaxValue, [DateTime] $OnlyDeleteDirectoriesNotModifiedAfterDate = [DateTime]::MaxValue, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force | Where-Object { $_.PSIsContainer -and $null -eq (Get-ChildItem -Path $_.FullName -Recurse -Force | Where-Object { !$_.PSIsContainer }) } |
        Where-Object { $_.CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate -and $_.LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate } |
        Sort-Object { $_.FullName } -Descending | # Sort directories to ensure we delete child directories before parent directories.
        ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }

    # If we should delete the given path when it is empty, and it is a directory, and it is empty, and it meets the date requirements, then delete it.
    if ($DeletePathIfEmpty -and (Test-Path -Path $Path -PathType Container) -and $null -eq (Get-ChildItem -Path $Path -Force) -and
        ((Get-Item $Path).CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate) -and ((Get-Item $Path).LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate))
    { if ($OutputDeletedPaths) { Write-Output $Path } Remove-Item -Path $Path -Force -WhatIf:$WhatIf }
}

# Function to remove all files in the given Path that were created before the given date, as well as any empty directories that may be left behind.
function Remove-FilesCreatedBeforeDate([parameter(Mandatory=$true)][ValidateScript({Test-Path $_})][string] $Path, [parameter(Mandatory)][DateTime] $DateTime, [switch] $DeletePathIfEmpty, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $DateTime } |
        ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }
    Remove-EmptyDirectories -Path $Path -DeletePathIfEmpty:$DeletePathIfEmpty -OnlyDeleteDirectoriesCreatedBeforeDate $DateTime -OutputDeletedPaths:$OutputDeletedPaths -WhatIf:$WhatIf
}

# Function to remove all files in the given Path that have not been modified after the given date, as well as any empty directories that may be left behind.
function Remove-FilesNotModifiedAfterDate([parameter(Mandatory=$true)][ValidateScript({Test-Path $_})][string] $Path, [parameter(Mandatory)][DateTime] $DateTime, [switch] $DeletePathIfEmpty, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.LastWriteTime -lt $DateTime } |
        ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }
    Remove-EmptyDirectories -Path $Path -DeletePathIfEmpty:$DeletePathIfEmpty -OnlyDeleteDirectoriesNotModifiedAfterDate $DateTime -OutputDeletedPaths:$OutputDeletedPaths -WhatIf:$WhatIf
}

Download PSv2 File

Happy coding!

Comments

Carl Reid

Thanks for submitting these scripts, coincidently I was looking to write something similar for tidying up old releases and will use your scripts as a starter for mine.

I did however hit problems with your Remove-EmptyDirectories function. It looks like it hits a null reference in the Get-ChildItems and then falls over when trying to access the Length property which of course doesn’t exist on a null object.

You can test this by simply creating two folders under c:\temp c:\temp\new1 c:\temp\new1\new2

Then run your example: Remove-FilesNotModifiedAfterDate -Path “C:\Temp” -DateTime ((Get-Date).AddDays(-2)) -DeletePathIfEmpty

Results in:

Where-Object : Property ‘Length’ cannot be found on this object. Make sure that it exists. At line:4 char:49

  • Get-ChildItem -Path $Path -Recurse -Force Where-Object { $_.PSIsContainer …
  • ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    • CategoryInfo : NotSpecified: (:) [Where-Object], PropertyNotFoundException
    • FullyQualifiedErrorId : PropertyNotFoundStrict,Microsoft.PowerShell.Commands.WhereObjectCommand

I have modified it like this which seems ok although not as concise!

function Remove-EmptyDirectories([parameter(Mandatory=$true)][ValidateScript({Test-Path $})][string] $Path, [switch] $DeletePathIfEmpty) { $childDirectories = Get-ChildItem -Path $Path -Recurse -Force | Where-Object { $.PSIsContainer } | Sort -Property @{ Expression = {$_.FullName.Split(‘').Count} } -Desc foreach ($childDirectory in $childDirectories) { $childFiles = Get-ChildItem -Path $childDirectory.FullName -Recurse -Force | Where-Object { -not $childDirectory.PSIsContainer } if (($childFiles -eq $null) -or ($childFiles.Length -eq 0)) { Remove-Item -Path $childDirectory.FullName -Force -Recurse } }

# If we should delete the given path when it is empty, and it is a directory, and it is empty, then delete it.
if ($DeletePathIfEmpty -and (Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -Force).Length -eq 0) { Remove-Item -Path $Path -Force } }

Cheers,

carl

deadlydog

@Carl Reid Hi Carl, I’m not able to reproduce your problem. I’ve created your sample scenario, as well as empty directories up to 5 levels deep and everything works fine for me. What version of PowerShell are you using? I’m using v3.0. If you are using a lower version maybe that is the problem.

Thanks for posting your modified solution though in case anybody else runs into the same problem.

deadlydog

@Carl Reid I confirmed a hunch I had that the error you were seeing arose when using “Set-StrictMode -Version Latest” in the script. I typically use this in my scripts, but wasn’t using it when I did my testing.

I’ve updated my code to simply check for $null instead of trying to check .Length, so the problem is fixed and it is still nice and concise :)

Thanks.

Marc

I have a newbie question… I save this .ps1 file to my disk, but if I run the function names in powershell it’s telling me always that this is not a name of a cmdlet or a function or a script or an executable.

I’d like to run “Remove-FilesNotModifiedAfterDate -Path “C:\Temp” -DateTime ((Get-Date).AddDays(-2))” directly in powershell.

What I’m doing wrong here?

deadlydog

@Marc Make sure you first dot source the script into your PowerShell session (to make the functions available) using:

. [Path to Remove-FilesOlderThan.ps1]

e.g. If using an absolute file path:

. “C:\Some Folder\Remove-FilesOlderThan.ps1”

e.g. If using a relative file path, for example your command prompt session is currently in the same directory where the script file is saved:

. “.\Remove-FilesOlderThan.ps1”

Once you do this step you should then be able to call the Remove-FilesNotModifiedAfterDate function.

Robert

It would seem there is a potential race condition in your script to remove old files when used to cleanup something like a server output directory. Specifically, if an application creates an empty directory newer than the CreatedBeforeDate or NotModifiedAfterDate and your Remove-Files… script finds it before the application writes a file in the directory, the directory will be deleted and will potentially leave the application broken. IMO, the date of the directory should be checked to meet the CreatedBeforeDate or NotModifiedAfterDate criteria to qualify for deletion.

deadlydog

@Robert Do you mean that you are writing files to a directory using some app, while at the same time running this script to clean files up in that same directory? That sounds like poor planning. If you are scheduling this script to run regularly, you should try and do it when you know other applications will not be writing to the same directory.

Having said that, I agree with you that to stay true to their names, the Remove-Files* functions should also check the dates on directories, not just files, so I’ve updated the scripts on this post to do just that. Thanks :)

JEmlay

This is a horrible solution! You’re using CREATED and MODIFIED times. ($_.LastWriteTime)

A file could have been written recently yet modified years ago. You’re script has the ability to delete WAY MORE then a person might want.

I just got done stripping out all the extra’s that aren’t wanted. Creation time only!

deadlydog

@JEmlay Why did you need to modify anything? If you are only concerned with the date/time a file was created, then just use the Remove-FilesCreatedBeforeDate function, and not the Remove-FilesNotModifiedAfterDate fuction.

Chris

Dan,

How can I use the “Remove-EmptyDirectories” with the “-DeletePathIfEmpty” flag but not delete the root folder defined in the $path variable?

i.e. In your example, not delete the ‘temp’ folder?

Peter

Michael : love the scripts – but how would you add output to these so a list of what is deleted can be produced?

I would also like this, I am trying to Out-File the function call however does not seem to be working for me, how would we output to the file what the script did?

thank you

deadlydog

@Michael

@Peter Hey guys, I’ve updated the scripts to now take 2 additional switch parameters: -WhatIf will now tell you what files/directories would be deleted without actually deleting them. -OutputDeletedPaths will output every file/directory that is deleted. So if provided it will output all of the deleted paths to the console window. If you want, you can also pipe the output to something like Out-File to create a file with every path that was deleted.

I hope that helps you :)

John Wagenman

Thank you. I spent a few days playing with this function thing you revealed to me and today I’m putting the finishing touches on my magnum opus of a backup script that handles two different vm environments, moves things to tape and removes the oldest backups to make room for new ones.

Thank you again for sharing this wonderful creation.

Alex

Hello, I am pretty new in the power shell. Thank you very much for sharing your knowledge. I saved your scripts in a file and named it as cleanup.ps1 Now I would like to run it in windows schedule task every night to clean up my server and delete all files older than 30 days ago. I am wondering if there a a way to pass the parameters to the function “Remove-FilesCreatedBeforeDate”

Josh

@Michael

In my scripts I usually add a Tee-Variable before the last step (in this case, deleting the files), then add a line that pipes that variable to an Export-CSV with a time stamp.

I actually found this script because I was looking to delete those exported CSVs after a certain time as part of my nightly maintenance processes, so thanks @deadlydog for providing a tidy set of functions for accomplishing this.

Ian

Scripts are a big help! Wondering if you could explain why this is happening though as I can’t see it?

I’m calling “Remove-FilesNotModifiedAfterDate” but when it is running I am being presented with the message “The item at “path” has children and the Recurse parameter was not specified. If you continue, all children will be removed with the item”

Now, as far as I can see the Recurse switch is set with -Force? The main problem is, while the message box is popping up, I can’t automate it :/

Thanks!

Jamie

Great script thanks but when trying to use a wildcard with the path parameter it doesn’t want to work as expected.

e.g I have the path “C:\Temp\Applications\New Folder” & “New Folder(2)”. This is just a test for a similar but much larger folder hierarchy I’ll be using in the live environment.

Using the following command it only removes the second folder contents but without the wildcard it checks all folders:

Remove-FilesCreatedBeforeDate -Path “C:\Temp\Applications\New*” -DateTime ((Get-Date).AddDays(-0)) -Whatif -OutputDeletedPaths

Am I missing something?

Jamie

Ok, managed to get round this by adding the switch “-Exclude” to line 9 under the Remove-EmptyDirectories function. Would like to know why specifying a wildcard in the parameter wouldn’t work but at least this can be addressed in another way.

BMA

Script runs great and I get it to parse out the path and file/folder/item names that should be deleted, but nothing is actually getting deleted. It just finds the same stuff and parses it out again and again.

I’m running Remove-FilesNotModifiedAfter like so

Remove-FilesNotModifiedAfterDate -Path “D:\directory” -DateTime ((Get-Date).AddDays(-728)) -DeletePathIfEmpty -OutputDeletedPaths Out-File $LogFile -Append

It’s driving me crazy and I know it’s going to be to be such a simple solution, but I just can’t seem to stumble upon it. Thanks for any help that you can provide

Hardik Thaker

Seems like “-Recurse” switch should not be there at first line of “Remove-EmptyDirectories” Function, because it will lead to below error

“Get-ChildItem : Could not find a part of the path \xyz\abc\pqr.”

and anyway you have “-Recurse” at the same line which will not make any difference.

Where-Object { $.PSIsContainer -and (Get-ChildItem -Path $.FullName -Recurse -Force Where-Object { !$_.PSIsContainer }) -eq $null }

In short:

Correct :

function Remove-EmptyDirectories([parameter(Mandatory=$true)][ValidateScript({Test-Path $})][string] $Path, [switch] $DeletePathIfEmpty, [DateTime] $OnlyDeleteDirectoriesCreatedBeforeDate = [DateTime]::MaxValue, [DateTime] $OnlyDeleteDirectoriesNotModifiedAfterDate = [DateTime]::MaxValue, [switch] $OutputDeletedPaths, [switch] $WhatIf) { Get-ChildItem -Path $Path -Force | Where-Object { $.PSIsContainer -and (Get-ChildItem -Path $.FullName -Recurse -Force | Where-Object { !$.PSIsContainer }) -eq $null } | Where-Object { $.CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate -and $.LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate } | ForEach-Object { if ($OutputDeletedPaths) { Write-Output $.FullName } Remove-Item -Path $.FullName -Force -WhatIf:$WhatIf }

# If we should delete the given path when it is empty, and it is a directory, and it is empty, and it meets the date requirements, then delete it.
if ($DeletePathIfEmpty -and (Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -Force) -eq $null -and
    ((Get-Item $Path).CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate) -and ((Get-Item $Path).LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate))
{ if ($OutputDeletedPaths) { Write-Output $Path } Remove-Item -Path $Path -Force -WhatIf:$WhatIf } }
Yarr!!

Maybe my situation is unique but using the following command on a folder structure as listed below is giving me errors for children because its attempting to delete the top level before the lower levels. This is bad because data could be in the lower levels. I hacked it to work for me by removing the “-file” from line 7. This allows it to see other directories when checking if the folder is empty. In this way, it will only remove the parent folder in the next run after removing the last child folder. This isnt the best way to do it but it works.

Command run: Remove-FilesCreatedBeforeDate -Path $sourcePath -DateTime ((Get-Date).AddDays(-3)) -DeletePathIfEmpty

Folder structure: (name\year\month\day\hour*) Y:\Yarr\2019\01\05\01\filescreatedeveryhour.txt

Bert

Hi @Deadlydog,

First of all, thanks for the script.

I seem to run in a problem with empty directories. I want all files older than 7 days to be deleted and all empty folders but NOT the TEMP folder itself.

I use this line but the directories, that are older than 7 days, are not deleted. Remove-FilesCreatedBeforeDate -Path “c:\temp” -DateTime ((Get-Date).AddDays(-7)) -DeletePathIfEmpty

Thanks

scriven_j

I just want to say thank you! After trying a few other vastly inferior solutions on the web, I found these amazing functions and they are perfect!

c0d3

Hello,

thanks for your great work!

Just question:

“Remove-FilesCreatedBeforeDate” in this function you’re calling function by passing $DateTime:

Remove-EmptyDirectories -OnlyDeleteDirectoriesCreatedBeforeDate $DateTime

And in function “Remove-EmptyDirectories” declaring MaxTime of OnlyDeleteDirectoriesCreatedBeforeDate , then why we need to pass $DateTime instead of defining only MaxTime of variable?

Thank you

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...