Every Downloads folder ends up the same way: 400 files, three of them named invoice (7).pdf, a dozen half-unzipped archives, and screenshots from a meeting nobody remembers. Dragging them into folders by hand takes twenty minutes. One Move-Item script does it in a second, and it can show you a full preview before it touches anything.
This post is that script, built up in small steps, plus the three Move-Item behaviours that bite people the first time they automate it.
The 10-second refresher
Move-Item (aliases move, mv, mi) moves a file or folder to a new location. If the destination is an existing folder, the item lands inside it with its name unchanged:
Move-Item -Path "$HOME\Downloads\report.pdf" -Destination "$HOME\Documents"
Two switches make it safe to automate:
| Switch | What it does |
|---|---|
-WhatIf |
Prints what would move and moves nothing |
-PassThru |
Outputs the moved item at its new location, so you can log it |
Full parameter list is in the official Move-Item documentation on Microsoft Learn.
Step 1: see what you are dealing with
Before sorting anything, group the files by extension so you know which folders you will get:
Get-ChildItem "$HOME\Downloads" -File |
Group-Object Extension |
Sort-Object Count -Descending |
Select-Object Count, Name
Typical output:
Count Name
----- ----
142 .pdf
61 .png
38 .zip
17 .xlsx
9 .msi
3
That last row with no name is files that have no extension at all. The script needs to handle those too.
Step 2: the sort script, in dry-run mode
$src = "$HOME\Downloads"
$dryRun = $true # flip to $false when the preview looks right
$files = Get-ChildItem -Path $src -File
foreach ($file in $files) {
$ext = $file.Extension.TrimStart('.').ToLower()
if (-not $ext) { $ext = 'no-extension' }
$dest = Join-Path $src $ext
if (-not (Test-Path -LiteralPath $dest)) {
New-Item -ItemType Directory -Path $dest -WhatIf:$dryRun | Out-Null
}
Move-Item -LiteralPath $file.FullName -Destination $dest -WhatIf:$dryRun
}
Run it as is and you get one What if: line per file and nothing changes on disk. Read the list. When it looks right, set $dryRun = $false and run it again.
A few choices in there are deliberate:
-
$files = Get-ChildItem ...first, then loop. Collecting the list up front means the script works from a fixed snapshot instead of reading the folder while it is changing underneath it. -
-WhatIf:$dryRunonNew-Itemtoo. If you only put-WhatIfonMove-Item, the dry run still creates a pile of emptypdf,png,zipfolders. Passing the same switch to both keeps the preview truly side-effect free. -
.ToLower(). Otherwise.PDFand.pdfwould compete for the same folder name. Windows treats them as one folder, but the output looks messy and confusing. -
-LiteralPath, not-Path. More on that in the gotchas below.
If you want the helpers explained in depth, I covered building folder paths with Join-Path and checking a path exists with Test-Path in earlier tips.
Step 3: do not lose invoice.pdf number two
Here is the first real-world failure. If pdf\invoice.pdf already exists and you move another invoice.pdf into the same folder, Move-Item stops with:
Move-Item: Cannot create a file when that file already exists.
-Force makes the error go away by overwriting the existing file, which is almost never what you want in a Downloads cleanup. Instead, pick a free name the way File Explorer does:
$target = Join-Path $dest $file.Name
$n = 1
while (Test-Path -LiteralPath $target) {
$target = Join-Path $dest ('{0} ({1}){2}' -f $file.BaseName, $n, $file.Extension)
$n++
}
Move-Item -LiteralPath $file.FullName -Destination $target -WhatIf:$dryRun
Swap that block in for the single Move-Item line from Step 2. Now the second invoice becomes invoice (1).pdf and nothing gets silently replaced.
Step 4: keep a log of what moved
When someone asks "where did my installer go?", you want an answer. -PassThru returns the item at its new location, so you can write it straight to a log file:
Move-Item -LiteralPath $file.FullName -Destination $target -PassThru |
ForEach-Object { '{0} {1} -> {2}' -f (Get-Date -Format s), $file.FullName, $_.FullName } |
Add-Content -Path "$src\_moves.log"
Run this only after the dry run, because nothing is passed through while -WhatIf is on. The log gives you a timestamped line for every move, which is also everything you need to undo the cleanup by hand.
The three Move-Item gotchas
1. A missing destination folder turns your move into a rename.
# C:\Reports\Archive does NOT exist yet
Get-ChildItem C:\Reports -File | Move-Item -Destination C:\Reports\Archive
The first file is moved and renamed to a file called Archive with no extension. The second file then fails, because Archive now exists as a file. You end up with one mystery file and a screen of red errors. Always create the destination folder first, which is exactly why Step 2 runs Test-Path and New-Item before moving.
2. Square brackets in file names break -Path.
-Path treats [ and ] as wildcard characters, so this finds nothing:
Move-Item -Path "C:\Temp\report [final].pdf" -Destination C:\Archive
Downloads are full of names like photo [1].jpg. Use -LiteralPath whenever the name comes from real files rather than a pattern you typed. Piping Get-ChildItem output into Move-Item is safe as well, because the pipeline binds the exact path for you.
3. Moving folders to another drive depends on your PowerShell version.
Moving a file from C: to D: works everywhere. Moving a folder across drives in Windows PowerShell 5.1 fails with Source and destination path must have identical roots. Current PowerShell 7 releases handle it by copying and then deleting the source. On a locked work laptop stuck on 5.1, use Copy-Item -Recurse, check the copy, and only then remove the original (with a dry run first, as in the Remove-Item -WhatIf tip).
Bonus: move by age instead of type
The same pattern archives old files from a reports folder. This one moves anything not touched in 30 days:
$archive = 'C:\Reports\Archive'
$cutoff = (Get-Date).AddDays(-30)
if (-not (Test-Path -LiteralPath $archive)) {
New-Item -ItemType Directory -Path $archive | Out-Null
}
Get-ChildItem C:\Reports -File |
Where-Object LastWriteTime -lt $cutoff |
Move-Item -Destination $archive -WhatIf
Notice the folder gets created before the pipeline runs, so gotcha number one never gets a chance.
Practice the moves before you run them on real files
Typing these out on your actual Downloads folder for the first time is a bit nerve-wracking. In CMD Master's free interactive PowerShell lessons you can practise Move-Item, -WhatIf, and folder creation in a live browser terminal with instant feedback, with no install and nothing real to lose. The same hands-on Windows command line course covers CMD and Bash too, if you switch between shells at work.
Your turn: what does your Downloads folder look like right now? Run the Step 1 command and share your top extension in the comments. I am betting on .pdf.
Top comments (0)