Capstone: Automating a Real Admin Task

PowerShell Fundamentals

Chapter 12 · Capstone: Automating a Real Admin Task

Dana just started as a junior sysadmin, and a shared logs folder is quietly filling up a server's disk. Before deleting anything, Dana wants a clean report of exactly what's old and large enough to matter — then, once it looks right, a safe way to actually clear it out, repeatable next time this happens without retyping everything by hand. Building that one real tool, Get-OldFileReport, touches every chapter in this course, in the order a genuine first attempt would actually hit them.

Step 1 — Checking the Environment First

Before writing anything, Dana checks how much room is actually at stake, using Chapter 2's own provider knowledge — Get-PSDrive works here exactly the way it did against the registry back then, just against the FileSystem provider this time:

Get-PSDrive C | Select-Object Used, Free

Unsure of the exact cmdlet for safely deleting a file, Dana reaches for Chapter 3's own discovery habit rather than guessing:

Get-Command -Verb Remove -Noun Item
Step 2 — Finding the Old, Large Files

The core of the whole tool is one object pipeline, straight from Chapters 1 and 4 — real properties (LastWriteTime, Length), no text parsing anywhere:

Get-ChildItem -Path $Path -Recurse -File | Where-Object { $_.LastWriteTime -lt $cutoff -and $_.Length -gt $sizeBytes } | Sort-Object Length -Descending
Step 3 — Typed, Hashtable-Backed Configuration

Rather than burying magic numbers in the pipeline itself, defaults live in an [ordered]@{} hashtable (Chapter 5), with the actual thresholds cast to real types before use — $cutoff a genuine [datetime], $sizeBytes a genuine [int64] of bytes, not megabytes:

$DefaultConfig = [ordered]@{ DaysOld = 30; SizeThresholdMB = 10 } [datetime]$cutoff = (Get-Date).AddDays(-$DaysOld) [int64]$sizeBytes = $SizeThresholdMB * 1MB
Step 4 — Classifying What Was Found

Chapter 6's switch -Wildcard sorts each match into a category, with break unnecessary here since each file only has one extension to match:

$category = switch -Wildcard ($file.Extension) { ".log" { "Log" } ".tmp" { "Temp" } ".bak" { "Backup" } default { "Other" } }
Step 5 — A Well-Behaved Function

Everything gets wrapped in a real function with a proper param() block (Chapter 7) — and Chapter 7's own biggest lesson is respected deliberately: every progress message uses Write-Verbose, never a bare or Write-Output line, so nothing pollutes the one clean value the function actually returns.

Step 6 — A Clean Report, Not a Screen Dump

Chapter 8's warning is respected directly: the report is built with Export-Csv and ConvertTo-Json, never Format-Table, since this data needs to survive being read back in later:

$results | Export-Csv -Path "$ReportPath.csv" -NoTypeInformation $results | ConvertTo-Json -Depth 3 | Set-Content "$ReportPath.json"
Step 7 — Deleting Safely

Only once the report looks right does Dana add real deletion — wrapped in Chapter 9's own try/catch/finally, with -ErrorAction Stop so a locked or permission-denied file is actually caught rather than silently skipped:

foreach ($file in $results) { try { Remove-Item -Path $file.FullPath -ErrorAction Stop Write-Verbose "Deleted $($file.FullPath)" } catch { Write-Warning "Couldn't delete $($file.FullPath): $($_.Exception.Message)" } finally { Write-Verbose "Finished handling $($file.Name)" } }
Step 8 — Running It Safely, and Keeping It Around

A colleague emails Dana the finished .ps1 for a second opinion. Running it back fails outright — Chapter 10's own Mark of the Web, since it arrived as an email attachment. Unblock-File .\Get-OldFileReport.ps1 clears it, confirmed safe by review first. Finally, Chapter 11's own lesson closes the loop: Dana dot-sources the script into $PROFILE, so Get-OldFileReport is simply available every time a new session opens, without retyping or re-navigating to the file — the exact same durability Chapter 11 gave the ll function now applied to a real production tool.

The Complete Script

function Get-OldFileReport { param( [Parameter(Mandatory)] [string]$Path, [int]$DaysOld = 30, [double]$SizeThresholdMB = 10, [string]$ReportPath = ".\old-file-report", [switch]$DeleteAfterReport ) [datetime]$cutoff = (Get-Date).AddDays(-$DaysOld) [int64]$sizeBytes = $SizeThresholdMB * 1MB Write-Verbose "Scanning $Path for files older than $DaysOld days, larger than $SizeThresholdMB MB" $matches = Get-ChildItem -Path $Path -Recurse -File | Where-Object { $_.LastWriteTime -lt $cutoff -and $_.Length -gt $sizeBytes } | Sort-Object Length -Descending $results = foreach ($file in $matches) { $category = switch -Wildcard ($file.Extension) { ".log" { "Log" } ".tmp" { "Temp" } ".bak" { "Backup" } default { "Other" } } [PSCustomObject]@{ Name = $file.Name FullPath = $file.FullName Category = $category SizeMB = [math]::Round($file.Length / 1MB, 2) LastWriteTime = $file.LastWriteTime } } $results | Export-Csv -Path "$ReportPath.csv" -NoTypeInformation $results | ConvertTo-Json -Depth 3 | Set-Content "$ReportPath.json" Write-Verbose "Report written: $ReportPath.csv / $ReportPath.json ($($results.Count) files found)" if ($DeleteAfterReport) { foreach ($file in $results) { try { Remove-Item -Path $file.FullPath -ErrorAction Stop Write-Verbose "Deleted $($file.FullPath)" } catch { Write-Warning "Couldn't delete $($file.FullPath): $($_.Exception.Message)" } finally { Write-Verbose "Finished handling $($file.Name)" } } } $results } # A dry-run report only — no deletion, exactly one clean value returned Get-OldFileReport -Path "D:\Logs" -DaysOld 30 -SizeThresholdMB 10 -Verbose # Once the report looks right — report AND delete Get-OldFileReport -Path "D:\Logs" -DaysOld 30 -SizeThresholdMB 10 -DeleteAfterReport

Chapter Attribution

StepChapter(s) applied
1 — Checking the environmentChapter 2 (Get-PSDrive), Chapter 3 (Get-Command discovery)
2 — The core pipelineChapter 1 (object pipeline), Chapter 4 (Where-Object/Sort-Object)
3 — ConfigurationChapter 5 ([ordered]@{}, type casting)
4 — ClassificationChapter 6 (switch -Wildcard)
5 — The function itselfChapter 7 (param(), Write-Verbose over uncaptured output)
6 — The reportChapter 8 (Export-Csv/ConvertTo-Json, never Format-Table)
7 — Safe deletionChapter 9 (try/catch/finally, -ErrorAction Stop)
8 — Running & keeping itChapter 10 (Unblock-File), Chapter 11 ($PROFILE)
What this whole course was really about
From Chapter 1's first Get-ChildItem to this capstone's last Remove-Item, not one step ever fell back to parsing text. Discovery (Chapter 3), filtering (Chapter 4), configuration (Chapter 5), classification (Chapter 6), a well-behaved function (Chapter 7), and a clean exported report (Chapter 8) all stayed real, typed objects the entire way through — the exact claim Chapter 1 opened this course with, now carrying a genuine production script rather than a single example line.
Honest scope note
This capstone deliberately stops short of full automation. It doesn't cover scheduling Get-OldFileReport to run unattended (Task Scheduler is OS-level territory, not a PowerShell Fundamentals topic), running it against several remote servers at once (PowerShell Remoting is PowerShell Intermediate/Advanced's own Chapter 3), or delivering the report anywhere automatically (email/Teams integration is a separate API concern). What's here is a genuinely real, safe, reusable tool — not yet an unattended one.

Hands-On Exercises

Exercise 1

Explain why Get-OldFileReport uses Write-Verbose for every progress message instead of a bare string or Write-Output. What would go wrong with the function's return value if a stray Write-Output line were added inside the foreach loop, referencing Chapter 7's own explanation?

📄 View solution
Exercise 2

Explain why the deletion step (Step 7) uses -ErrorAction Stop on Remove-Item inside its try block, referencing Chapter 9's own explanation of terminating vs. non-terminating errors. What would happen to a locked file if -ErrorAction Stop were left off?

📄 View solution
Exercise 3

Dana's colleague emails the finished script, and running it fails until Unblock-File is used. Explain exactly why, referencing Chapter 10's own explanation of what actually makes a file count as "remote."

📄 View solution

Chapter 12 Quick Reference — Course Complete

  • 8 build steps, 11 prior chapters — one real, reusable admin tool, built in the order a genuine first attempt would actually hit each concept
  • This course's own throughline, closed out: real, typed .NET objects flowing cleanly from discovery through filtering, configuration, classification, execution, reporting, and safe deletion — never once falling back to parsing text
  • Honest scope note: no unattended scheduling, no multi-server remoting, no automated delivery — a real tool, not yet a fully unattended one
  • The PowerShell Fundamentals course is now complete — 12/12 chapters