Springtime is approaching in North America. Where I live, the snow has finally melted and we have blue skies with warmer temperatures. Of course, this means Spring Cleaning. Time to clear out the winter debris and spruce up the house. For me, this is also a good time for some computing housecleaning as well. I don't know about your Windows environment, but I tend to accumulate a lot of junk. Most of the time I don't see it, but I know it's there. While the junk normally doesn't have a negative impact, I think mentally, I like clearing things out and tidying up. So I pulled out some older PowerShell code, freshened it up, and now I have a set of tools for clearing out junk and temporary folders. Let me show you what I came up with.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Remove Old Files
The first task is to delete files that are older than a given date. I typically want to clean out files from my temp folders. I know that some of these files might still be in use so I don't necessarily want to broadly delete all files. I want to only delete files where the last modified date is older than a given date. Eventually, I will use the last boot up time. Any file older than the last boot up time, should be safe to delete from any temp folder. Here's my PowerShell function.
Function Remove-File {
[cmdletbinding(SupportsShouldProcess)]
[Alias("rfi")]
Param(
[Parameter(Position = 0)]
[ValidateScript( { Test-Path $_ })]
[string]$Path = $env:temp,
[Parameter(Position = 1, Mandatory, HelpMessage = "Enter a cutoff date. All files modified BEFORE this date will be removed.")]
[ValidateScript( { $_ -lt (Get-Date) })]
[datetime]$Cutoff,
[Switch]$Recurse,
[Switch]$Force
)
Write-Verbose "Starting $($MyInvocation.MyCommand)"
Write-Verbose "Removing files in $path older than $cutoff"
#clean up PSBoundParameters which will be splatted to Get-ChildItem
[void]$PSBoundParameters.Add("File", $True)
[void]$PSBoundParameters.Remove("CutOff")
if ($WhatIfPreference) {
[void]$PSBoundParameters.Remove("Whatif")
}
Write-Verbose "Using these parameters: `n $($PSBoundParameters | Out-String)"
Try {
$files = Get-ChildItem @PSBoundParameters -ErrorAction Stop | Where-Object { $_.lastwritetime -lt $cutoff }
}
Catch {
Write-Warning "Failed to enumerate files in $path"
Write-Warning $_.Exception.Message
#Bail out
Return
}
if ($files) {
Write-Verbose "Found $($files.count) file(s) to delete."
$stats = $files | Measure-Object -Sum length
$msg = "Removing {0} files for a total of {1} MB ({2} bytes) from {3}." -f $stats.count, ($stats.sum / 1MB -as [int]), $stats.sum, $path.toUpper()
Write-Verbose $msg
#only remove files if anything was found
$files | Remove-Item -Force
#Display a WhatIf Summary
if ($WhatIfPreference) {
Write-Host "What if: $msg" -ForegroundColor CYAN
}
} #if $files
else {
Write-Warning "No files found to remove in $($path.ToUpper()) older than $Cutoff."
}
Write-Verbose "Ending $($MyInvocation.MyCommand)"
} #close function
I have support for -WhatIf in cmdletbinding. I don't need to code anything special to use it. Remove-Item supports -WhatIf so if I run the function with the parameter, Remove-Item will automatically detect it.
My function has a snippet of code that gives me a summary of what the function would do. I even make it look like WhatIf output, except that I use Write-Host and display the message in Cyan so it stands out.
Remove Empty Directories
Next, I want to remove empty directories. I've written a variety of functions and scripts over the years to do this. Here's my current iteration.
Function Remove-EmptyFolder {
[cmdletbinding(SupportsShouldProcess)]
[alias("ref")]
[outputType("None")]
Param(
[Parameter(Position = 0, Mandatory, HelpMessage = "Enter a root directory path")]
[ValidateScript( {
Try {
Convert-Path -Path $_ -ErrorAction stop
if ((Get-Item $_).PSProvider.Name -ne 'FileSystem') {
Throw "$_ is not a file system path."
}
$true
}
Catch {
Write-Warning $_.exception.message
Throw "Try again."
}
})]
[string]$Path
)
Write-Verbose "Starting $($myinvocation.mycommand)"
Write-Verbose "Enumerating folders in $Path"
$folders = (Get-Item -Path $Path -force).EnumerateDirectories("*", [System.IO.SearchOption]::AllDirectories).foreach( {
if ($((Get-Item $_.FullName -force).EnumerateFiles("*", [System.IO.SearchOption]::AllDirectories)).count -eq 0) {
$_.fullname
} })
If ($folders.count -gt 0) {
$msg = "Removing $($folders.count) empty folder(s) in $($path.ToUpper())"
Write-Verbose $msg
#Test each path to make sure it still exists and then delete it
foreach ($folder in $folders) {
If (Test-Path -Path $Folder) {
Write-Verbose "Removing $folder"
Remove-Item -Path $folder -Force -Recurse
}
}
#Display a WhatIf Summary
if ($WhatIfPreference) {
Write-Host "What if: $msg" -ForegroundColor CYAN
}
}
else {
Write-Warning "No empty folders found under $($path.ToUpper())."
}
Write-Verbose "Ending $($myinvocation.mycommand)"
} #end Remove-EmptyFolder
Instead of getting a child listing of each folder , I'm calling the EnumerateDirectories() and EnumerateFiles() methods. This appears to perform a bit faster.
$folders = (Get-Item -Path $Path -force).EnumerateDirectories("*", [System.IO.SearchOption]::AllDirectories).foreach( {
if ($((Get-Item $_.FullName -force).EnumerateFiles("*", [System.IO.SearchOption]::AllDirectories)).count -eq 0) {
$_.fullname
} })
The first part of this expression is getting all directories in the search path. Then I'm testing for any files in each folder. if there are no files, the full path to the empty directory is saved to $folders. This becomes the list of items to delete.
foreach ($folder in $folders) {
If (Test-Path -Path $Folder) {
Write-Verbose "Removing $folder"
Remove-Item -Path $folder -Force -Recurse
}
}
I'm doing a quick test of each path because I may have already deleted an empty parent.
Using a Control Script
While I can run the functions separately, I wrote a simple control script. The functions are the tools and the controller script "orchestrates" their use. The controller script becomes a re-usable tool itself.
#requires -version 5.1
<#
CleanTemp.ps1
A control script to clean temp folders of files since last boot
and empty folders.
#>
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Position = 0,HelpMessage = "Specify the temp folder path")]
[string[]]$Path = @($env:temp, 'c:\windows\temp', 'D:\Temp')
)
#dot source functions
. C:\scripts\Remove-EmptyFolder.ps1
. C:\scripts\Remove-File.ps1
#get last boot up time
$bootTime = (Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime
Write-Verbose "Last boot = $boottime"
#delete files in temp folders older than the last bootup time
foreach ($folder in $Path) {
if (Test-Path $Folder) {
Remove-File -path $folder -cutoff $bootTime -recurse -force
Remove-EmptyFolder -path $folder
}
else {
Write-Warning "Failed to validate $($folder.toUpper())"
}
}
The script parameters have default values that make this very simple for me to run. I could, and probably should, put the cleaning functions in a module since they are related. But until then, I'll simply dot-source the files. The controller script passes the necessary parameters to the underlying commands. Within seconds, my temp folders are cleaned.
Schedule the Task
Now that I think about it, what I should probably do, is create a PowerShell scheduled job to run this script at logon.
$params = @{
FilePath = "C:\scripts\CleanTemp.ps1"
Name = "ClearTemp"
Trigger = (New-JobTrigger -AtLogOn)
MaxResultCount = 1
}
Register-ScheduledJob @params
Now I never need to worry about Spring cleaning!
1 thought on “Cleaning with PowerShell Revisited”
Comments are closed.