如何用 PowerShell 查明 Windows 磁碟空間去向:先作唯讀審查,再以 -WhatIf 預覽清理
Find what's filling your Windows drive with PowerShell: a read-only audit first, then a cleanup you can preview with -WhatIf
PowerShell 腳本先以唯讀方式檢查各磁碟可用空間、用戶設定檔內最大的資料夾及常見佔用位置,再提供有限度的清理操作。清理只刪除一星期未有變更的個人 TEMP 資料夾檔案,並支援 -WhatIf 預覽;其他位置只作檢查。程式碼尚未在 Windows 上實際執行。
AI disclosure: this post was written with AI assistance. The code was syntax-checked and linted, and its logic was tested with simulated drives and a throwaway test folder, but it has not yet been run on Windows: not on a real Windows 11 PC, and not on Windows PowerShell 5.1. Read it before you run it, and try the cleanup with
-WhatIffirst.
When the C: drive turns red, the first stop is Windows itself. Settings > System > Storage > Cleanup recommendations lists temporary files, large or unused files, files synced to the cloud and unused apps, and shows how much each would free. Storage Sense on the same page can clear temporary files and the Recycle Bin automatically. Use those first.
What Settings doesn't give you is a quick, scriptable answer to where did the space go? This post builds that answer in PowerShell, in two parts:
- A read-only audit. Free space per drive, the biggest folders under your profile, and the size of the places that usually hold junk. It changes nothing.
-
One narrow cleanup. Delete files from your own TEMP folder that haven't changed in a week. It asks first, supports
-WhatIf, and refuses to run if TEMP looks wrong.
Everything else (Downloads, the Recycle Bin, the Windows Update cache, Windows.old) is only measured. You decide what to do with those.
Step 1: free space per drive
Win32_LogicalDisk reports every logical disk. DriveType 3 means "Local Disk", and Size and FreeSpace are in bytes:
function Get-FixedDriveSpace {
[CmdletBinding()]
param()
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType = 3' -ErrorAction Stop |
ForEach-Object {
$pct = if ($_.Size -gt 0) { [math]::Round(100 * $_.FreeSpace / $_.Size, 1) } else { $null }
[pscustomobject]@{
Drive = $_.DeviceID
SizeGB = [math]::Round($_.Size / 1GB, 1)
FreeGB = [math]::Round($_.FreeSpace / 1GB, 1)
PercentFree = $pct
}
}
}
Get-FixedDriveSpace | Format-Table -AutoSize
Only DriveType = 3 disks come back. Removable and network drives have other DriveType values, so they're left out.
Step 2: measure a folder without following links
The obvious one-liner is Get-ChildItem -Recurse | Measure-Object Length -Sum. It has a catch: junctions and symbolic links. A link can point at a folder somewhere else, so it gets counted twice or sends you into a loop. The rules also differ between versions: in PowerShell 6 and later, -Recurse doesn't follow symbolic links to directories unless you add -FollowSymlink, and Windows PowerShell 5.1 doesn't have that switch. So walk the tree yourself, and skip anything with the ReparsePoint attribute:
function Get-FolderSize {
[CmdletBinding()]
param([Parameter(Mandatory)] [string] $Path)
$result = [pscustomobject]@{ Path = $Path; Bytes = [long]0; Files = 0; SkippedLinks = 0; Unreadable = 0 }
if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return $result }
$stack = New-Object System.Collections.Generic.Stack[string]
$stack.Push($Path)
while ($stack.Count -gt 0) {
$dir = $stack.Pop()
$errs = $null
$items = Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue -ErrorVariable errs
$result.Unreadable += @($errs).Count
foreach ($item in $items) {
$isLink = ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
if ($item.PSIsContainer) {
if ($isLink) { $result.SkippedLinks++ } else { $stack.Push($item.FullName) }
}
elseif ($isLink) { $result.SkippedLinks++ }
else { $result.Bytes += $item.Length; $result.Files++ }
}
}
$result
}
Two details:
-
-ErrorAction SilentlyContinue -ErrorVariable errskeeps going past folders you can't read (some system folders need admin rights) and counts them, instead of filling the screen with red. - The total is the sum of file lengths. That can differ from "size on disk" in File Explorer, for example for compressed files or OneDrive files that are only stored online.
Then list the biggest folders directly under your profile:
function Get-LargestFolder {
[CmdletBinding()]
param([string] $Path = $env:USERPROFILE, [int] $Top = 10)
Get-ChildItem -LiteralPath $Path -Directory -Force -ErrorAction Stop |
Where-Object { ($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0 } |
ForEach-Object {
$s = Get-FolderSize -Path $_.FullName
[pscustomobject]@{ Folder = $_.FullName; SizeGB = [math]::Round($s.Bytes / 1GB, 2); Files = $s.Files; Bytes = $s.Bytes }
} |
Sort-Object -Property Bytes -Descending |
Select-Object -First $Top
}
Get-LargestFolder -Top 10 | Format-Table Folder, SizeGB, Files -AutoSize
Run it again with -Path "$env:USERPROFILE\AppData\Local" (or any folder that stands out) to drill down. Measuring a big profile can take a few minutes.
Step 3: the usual suspects
These are the places Microsoft's own cleanup advice keeps coming back to:
| Location | How the script finds it | What to do about it |
|---|---|---|
| Your TEMP folder | $env:TEMP |
Cleanup recommendations > Temporary files, or Step 4 below |
| Windows Update download cache | %SystemRoot%\SoftwareDistribution\Download |
Leave it to Cleanup recommendations or Disk Cleanup |
| Recycle Bin | Shell's Recycle Bin folder (see below) | Look inside, then Clear-RecycleBin
|
| Downloads |
%USERPROFILE%\Downloads (the default location) |
Your files: sort by size and decide |
| Previous Windows install | %SystemDrive%\Windows.old |
Removing it means you can't go back to the previous version |
The Recycle Bin is a virtual folder, so there's no plain path to measure. The Shell object exposes it as special folder 10 (ssfBITBUCKET), and each item has a Size:
function Get-RecycleBinByteCount {
[CmdletBinding()]
[OutputType([long])]
param()
try {
$bin = (New-Object -ComObject Shell.Application).NameSpace(10)
$total = [long]0
foreach ($i in $bin.Items()) { $total += [long]$i.Size }
$total
}
catch {
Write-Warning "Could not read the Recycle Bin size: $($_.Exception.Message)"
$null
}
}
The snippets here are trimmed; the repo version adds parameter validation and more output fields. The full script wraps all of this in one Get-CleanupCandidate function that prints a row per location with its size and advice:
Get-CleanupCandidate | Format-Table Location, SizeGB, Advice -Wrap
A note on Downloads: %USERPROFILE%\Downloads is only the default. Microsoft notes that known folders can be redirected, so if yours has moved, pass the real path with -DownloadsPath.
Step 4: clear old TEMP files, with a preview
This is the only part that deletes anything, so it has the most guard rails:
function Assert-SafeTempPath {
[CmdletBinding()]
param([string] $TempPath)
if ([string]::IsNullOrWhiteSpace($TempPath)) { throw 'TEMP path is empty. Refusing to clean.' }
if (-not (Test-Path -LiteralPath $TempPath -PathType Container)) { throw "TEMP folder not found: $TempPath" }
$leaf = Split-Path -Path $TempPath.TrimEnd('\', '/') -Leaf
if ($leaf -notin @('Temp', 'Tmp')) {
throw "Refusing to clean '$TempPath': the folder is not named Temp or Tmp."
}
}
function Get-OldTempFile {
[CmdletBinding()]
param([string] $TempPath = $env:TEMP, [ValidateRange(0, 3650)] [int] $OlderThanDays = 7)
if ([string]::IsNullOrWhiteSpace($TempPath) -or -not (Test-Path -LiteralPath $TempPath -PathType Container)) { return }
$cutoff = (Get-Date).AddDays(-$OlderThanDays)
$stack = New-Object System.Collections.Generic.Stack[string]
$stack.Push($TempPath)
while ($stack.Count -gt 0) {
foreach ($item in (Get-ChildItem -LiteralPath $stack.Pop() -ErrorAction SilentlyContinue)) {
if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { continue }
if ($item.PSIsContainer) { $stack.Push($item.FullName) }
elseif ($item.LastWriteTime -lt $cutoff) { $item }
}
}
}
function Remove-OldTempFile {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param([ValidateRange(1, 3650)] [int] $OlderThanDays = 7, [string] $TempPath = $env:TEMP)
Assert-SafeTempPath -TempPath $TempPath
$files = @(Get-OldTempFile -TempPath $TempPath -OlderThanDays $OlderThanDays)
$mb = [math]::Round((($files | Measure-Object -Property Length -Sum).Sum) / 1MB, 1)
$deleted = 0; $skipped = 0
$target = "$($files.Count) file(s) ($mb MB) in $TempPath not changed in $OlderThanDays days"
if ($files.Count -gt 0 -and $PSCmdlet.ShouldProcess($target, 'Delete (no undo)')) {
foreach ($f in $files) {
try { Remove-Item -LiteralPath $f.FullName -ErrorAction Stop -Confirm:$false; $deleted++ }
catch { $skipped++ } # usually a file a running program still has open
}
}
[pscustomobject]@{ Found = $files.Count; Deleted = $deleted; Skipped = $skipped }
}
Why each piece is there:
-
SupportsShouldProcessgives the function-WhatIfand-Confirm. Nothing inside theif ($PSCmdlet.ShouldProcess(...))block runs under-WhatIf. -
ConfirmImpact = 'High'means PowerShell asks before deleting even when you don't pass-Confirm, because the default$ConfirmPreferenceis High. You confirm once for the whole batch.-Confirm:$falseonRemove-Itemstops it asking again for every file. -
The name check is there because .NET's
GetTempPathfalls back to your profile folder, or even the Windows folder, when the TMP and TEMP variables are missing. A cleanup that trusts the variable blindly could be pointed at the wrong place. This one only runs on a folder actually namedTemporTmp. - No link-following, for the same reason as Step 2. A junction inside TEMP can't lead the script out of TEMP.
-
No
-ForceonRemove-Item. Without-Force, PowerShell won't delete hidden or read-only files, and those are best left alone here.
Preview, then run:
Remove-OldTempFile -OlderThanDays 7 -WhatIf
Remove-OldTempFile -OlderThanDays 7
What about the Recycle Bin?
PowerShell already has Clear-RecycleBin, and it supports -WhatIf and -Confirm:
Clear-RecycleBin -DriveLetter C -WhatIf
Clear-RecycleBin -DriveLetter C
Open the Recycle Bin and look first: once it's emptied, the files are gone. Don't add -Force. Microsoft's documentation for Clear-RecycleBin says -Force also overrides -WhatIf and -Confirm, which defeats the whole point of previewing.
What not to touch by hand
-
C:\Windows\WinSxS. Microsoft says not to delete the WinSxS folder and to shrink it with tools built into Windows instead, for exampleDism.exe /online /Cleanup-Image /StartComponentCleanupfrom an elevated prompt. -
Windows.old. Remove it from Cleanup recommendations, and only once you're sure you won't roll back. - The Windows Update cache. Disk Cleanup and Cleanup recommendations handle system files. If Windows Update itself is stuck, follow Microsoft's reset steps rather than deleting the folder while updates are running.
Limits, honestly
- The code was tested with PowerShell 7 on Linux, using fake drives, a fake Recycle Bin and real files in a throwaway test folder. PSScriptAnalyzer reported nothing, and Pester tests of the size maths, the link skipping, the
-WhatIfpath and the refusal checks passed. It has not been run on a real Windows PC or on Windows PowerShell 5.1. Try it with-WhatIffirst. - Sizes are file lengths, not "size on disk".
- There's no undo for the TEMP cleanup. That's why it's limited to old files in a folder meant for temporary files.
- The full script, with its Pester tests, is at https://github.com/fecklesspleb/disk-space-audit (MIT).
Sources (Microsoft)
- Free up drive space in Windows (Cleanup recommendations, Storage Sense, Disk Cleanup, Windows.old, emptying the Recycle Bin): https://support.microsoft.com/en-us/windows/experience/storage-filemanagement/free-up-drive-space-in-windows
- Manage drive space with Storage Sense: https://support.microsoft.com/en-us/windows/experience/storage-filemanagement/manage-drive-space-with-storage-sense
- Win32_LogicalDisk class: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logicaldisk
- Get-ChildItem (
-FollowSymlink, links and junctions): https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-childitem - Path.GetTempPath and the GetTempPath fallback order: https://learn.microsoft.com/en-us/dotnet/api/system.io.path.gettemppath and https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw
- ShellSpecialFolderConstants (
ssfBITBUCKET): https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants - FolderItem.Size: https://learn.microsoft.com/en-us/windows/win32/shell/folderitem-size
- KNOWNFOLDERID (Downloads): https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
- Remove-Item: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/remove-item?view=powershell-7.5
- Clear-RecycleBin: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/clear-recyclebin?view=powershell-5.1
- Clean up the WinSxS folder: https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/clean-up-the-winsxs-folder
- Everything you wanted to know about ShouldProcess: https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-shouldprocess
- about_Preference_Variables (
$ConfirmPreferencedefaults to High): https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7.4
This article was created with the help of AI. Not affiliated with or endorsed by Microsoft.
If you want the rest of the fixes (slow startup, full drive, stuck updates) in one place, I sell a 40-page guide with the scripts: Windows 11 Fix-It Guide + PowerShell Toolkit, $19.
來源:DEV Community · dev.to