<# .SYNOPSIS Empties the Recycle Bin without a prompt, for the current user or everyone, optionally only for items deleted more than N days ago. .DESCRIPTION With no parameters it calls the Windows SHEmptyRecycleBin API for the current user, which is exactly what "Empty Recycle Bin" in Explorer does, minus the dialog and the sound. With -OlderThanDays or -AllUsers it works on the $Recycle.Bin folders directly. Every deleted item is a pair of files: $I (the metadata) and $R (the actual content). The script removes both halves together so Explorer never ends up with ghost entries. .PARAMETER OlderThanDays Only remove items that have been in the bin longer than this. 0 (the default) means everything. .PARAMETER AllUsers Clean every user's Recycle Bin, not just yours. Needs to run elevated. .PARAMETER DriveLetter Which drives to clean. Defaults to every local fixed drive. .EXAMPLE .\Clear-RecycleBinItem.ps1 .EXAMPLE .\Clear-RecycleBinItem.ps1 -AllUsers -OlderThanDays 30 -WhatIf #> [CmdletBinding(SupportsShouldProcess)] param( [ValidateRange(0, 3650)] [int]$OlderThanDays = 0, [switch]$AllUsers, [ValidatePattern('^[A-Za-z]$')] [string[]]$DriveLetter ) if (-not $DriveLetter) { $DriveLetter = Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' | ForEach-Object { $_.DeviceID.TrimEnd(':') } } $mySid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value # Fast path: current user, everything. Let Windows do it the official way. if (-not $AllUsers -and $OlderThanDays -eq 0) { if (-not ('RecycleBin.NativeMethods' -as [type])) { Add-Type -Namespace RecycleBin -Name NativeMethods -MemberDefinition @' [DllImport("shell32.dll", CharSet = CharSet.Unicode)] public static extern int SHEmptyRecycleBin(IntPtr hwnd, string pszRootPath, uint dwFlags); '@ } # SHERB_NOCONFIRMATION | SHERB_NOPROGRESSUI | SHERB_NOSOUND $flags = 0x1 -bor 0x2 -bor 0x4 foreach ($letter in $DriveLetter) { $root = "${letter}:\" if (-not $PSCmdlet.ShouldProcess("Recycle Bin on $root for $env:USERNAME", 'Empty')) { continue } $hr = [RecycleBin.NativeMethods]::SHEmptyRecycleBin([IntPtr]::Zero, $root, $flags) [pscustomobject]@{ Drive = $root Sid = $mySid ItemsRemoved = $null MBFreed = $null # A non-zero result on an already-empty bin is normal, not an error. Status = if ($hr -eq 0) { 'Emptied' } else { 'Nothing to empty (0x{0:X8})' -f $hr } } } return } $cutoff = (Get-Date).AddDays(-$OlderThanDays) foreach ($letter in $DriveLetter) { $binRoot = "${letter}:\`$Recycle.Bin" if (-not (Test-Path -LiteralPath $binRoot)) { Write-Verbose "No Recycle Bin on ${letter}:"; continue } $sidFolders = Get-ChildItem -LiteralPath $binRoot -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -like 'S-1-*' -and ($AllUsers -or $_.Name -eq $mySid) } foreach ($folder in $sidFolders) { try { # $I files are created at the moment of deletion, so their age is the item's age in the bin. $metaFiles = @(Get-ChildItem -LiteralPath $folder.FullName -Filter '$I*' -File -Force -ErrorAction Stop | Where-Object { $OlderThanDays -eq 0 -or $_.CreationTime -lt $cutoff }) } catch { Write-Warning "Can't read $($folder.FullName): $($_.Exception.Message)" continue } if ($metaFiles.Count -eq 0) { continue } $pairs = foreach ($meta in $metaFiles) { $dataPath = Join-Path $folder.FullName ('$R' + $meta.Name.Substring(2)) $bytes = 0 if (Test-Path -LiteralPath $dataPath) { $bytes = (Get-ChildItem -LiteralPath $dataPath -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum if (-not $bytes) { $bytes = (Get-Item -LiteralPath $dataPath -Force).Length } } [pscustomobject]@{ Meta = $meta.FullName; Data = $dataPath; Bytes = [long]$bytes } } $totalMB = [math]::Round((($pairs | Measure-Object Bytes -Sum).Sum) / 1MB, 1) $target = "$($pairs.Count) item(s), $totalMB MB in $($folder.FullName)" if (-not $PSCmdlet.ShouldProcess($target, 'Permanently delete')) { continue } $removed = 0; $freed = 0L foreach ($p in $pairs) { try { # Content first, metadata second. If the content won't go, the entry stays visible. if (Test-Path -LiteralPath $p.Data) { Remove-Item -LiteralPath $p.Data -Recurse -Force -ErrorAction Stop } Remove-Item -LiteralPath $p.Meta -Force -ErrorAction Stop $removed++; $freed += $p.Bytes } catch { Write-Warning "Skipped $($p.Data): $($_.Exception.Message)" } } [pscustomobject]@{ Drive = "${letter}:\" Sid = $folder.Name ItemsRemoved = $removed MBFreed = [math]::Round($freed / 1MB, 1) Status = if ($removed -eq $pairs.Count) { 'Emptied' } else { "Partial ($($pairs.Count - $removed) skipped)" } } } }