Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Cleaning with PowerShell Revisited

Posted on March 23, 2021March 23, 2021

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.

Manage and Report Active Directory, Exchange and Microsoft 365 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!


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

1 thought on “Cleaning with PowerShell Revisited”

  1. Pingback: Automated File Cleanup with Powershell – Curated SQL

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d