Every Windows estate has a folder of batch files nobody wants to touch. They work, sort of, and the person who wrote them left in 2016. Rewriting them in PowerShell is worthwhile, but only for the ones where it buys you something.
This covers what maps to what, the traps that catch people, and how to decide which ones to leave alone.
Quick Facts
- Don’t migrate everything. A three-line batch file that copies a folder is fine as it is.
- Migrate when you need error handling, structured data or remote execution. That’s where batch genuinely can’t compete.
%ERRORLEVEL%and$LASTEXITCODEare not the same thing. This causes real bugs.- Quoting rules differ significantly. Batch quoting habits produce subtle PowerShell errors.
- Run both in parallel for a cycle before retiring the batch version.
What maps to what
| Batch | PowerShell | Notes |
echo | Write-Output | Use Write-Host only for genuinely console-only text |
set VAR=value | $var = 'value' | PowerShell variables are typed |
if exist | Test-Path | Works for files, folders, registry keys |
for /f | foreach / ForEach-Object | No more token parsing |
xcopy / robocopy | Copy-Item | Keep robocopy for large mirrored transfers |
%ERRORLEVEL% | $LASTEXITCODE or $? | Different meanings. See below |
call | Dot-source or a function | Functions are the better pattern |
findstr | Select-String | Returns objects, not text |
The exit code trap
This is the one that produces bugs people chase for hours.
$LASTEXITCODEholds the exit code of the last native executable. It is not set by PowerShell cmdlets.$?is a boolean. Did the last operation succeed. It’s set by cmdlets, and doesn’t give you a code.- A failing cmdlet doesn’t set
$LASTEXITCODEat all, so it retains a stale value from whatever ran before.
# Native executable - use $LASTEXITCODE
robocopy C:\Source D:\Dest /MIR
if ($LASTEXITCODE -ge 8) { Write-Error "Robocopy failed: $LASTEXITCODE" }
# Cmdlet - use try/catch with -ErrorAction Stop
try {
Copy-Item C:\Source D:\Dest -Recurse -ErrorAction Stop
}
catch {
Write-Error "Copy failed: $($_.Exception.Message)"
exit 1
}
Note the robocopy check uses -ge 8. Robocopy returns 1 for “files copied successfully”. Treating non-zero as failure, which is the batch habit, reports success as an error.
Quoting, which trips everyone up
In batch, quoting is mostly about spaces. In PowerShell, quote type changes meaning:
$name = 'World'
Write-Output "Hello $name" # Hello World - double quotes expand
Write-Output 'Hello $name' # Hello $name - single quotes are literal
# Passing arguments with spaces to a native command
$path = 'C:\Program Files\App'
& 'C:\Tools\thing.exe' --path $path # PowerShell handles the quoting
Use single quotes by default and double quotes only when you need to interpolate. It avoids a whole class of surprises, particularly with strings containing $.
A worked migration
A typical old batch file:
:: backup.bat
@echo off
set SOURCE=C:\Data
set DEST=D:\Backup
if not exist %DEST% mkdir %DEST%
xcopy %SOURCE% %DEST% /E /Y
if %ERRORLEVEL% NEQ 0 echo Backup failed >> C:\Logs\backup.log
And the PowerShell version. Same job, but it can now be scheduled with parameters, logs properly and fails loudly:
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidateScript({ Test-Path $_ })]
[string]$Source = 'C:\Data',
[string]$Destination = 'D:\Backup',
[string]$LogPath = 'C:\Logs\backup.log'
)
if (-not (Test-Path $Destination)) {
New-Item -Path $Destination -ItemType Directory -Force | Out-Null
}
try {
Copy-Item -Path $Source -Destination $Destination -Recurse -Force -ErrorAction Stop
"$(Get-Date -f 'yyyy-MM-dd HH:mm') Backup completed" | Add-Content $LogPath
}
catch {
"$(Get-Date -f 'yyyy-MM-dd HH:mm') Backup FAILED: $($_.Exception.Message)" | Add-Content $LogPath
exit 1
}
The gains: input validation, real error handling, a timestamped log, parameters instead of hard-coded paths, and -WhatIf support for free.
Which ones to leave alone
- Short and working. A five-line file that maps a drive doesn’t need rewriting.
- Called by something you can’t change. A legacy application expecting
.bat. - Run in contexts where PowerShell isn’t guaranteed, like some pre-boot or imaging environments.
- Nobody understands what it does. Work that out first. A faithful rewrite of a misunderstood script just moves the mystery.
And whatever you migrate, run both versions in parallel for a cycle before retiring the original. Compare the outputs. It’s the cheapest insurance there is.
Glossary
| Term | What it means |
$LASTEXITCODE | Exit code of the last native executable. Not set by cmdlets. |
$? | Boolean indicating whether the last operation succeeded. |
| Dot-sourcing | Running a script in the current scope with . .\script.ps1 so its functions persist. |
Call operator & | Runs a command whose name or path is in a variable or quoted string. |
| Splatting | Passing parameters via a hashtable, keeping long commands readable. |
| Idempotent | Safe to run repeatedly with the same result. |
Frequently asked questions
Should I rewrite all my batch files?
No. Migrate the ones where error handling, logging, structured data or remote execution would genuinely help. A short working script that does one simple thing is fine as it is.
Why doesn’t my error checking work after migrating?
Almost certainly $LASTEXITCODE versus $?. Cmdlets don’t set $LASTEXITCODE, so it holds a stale value. Use try/catch with -ErrorAction Stop for cmdlets.
Can I call batch files from PowerShell during migration?
Yes. & 'C:\path\script.bat'. It’s a sensible transitional step: wrap the batch file in PowerShell for logging and scheduling, then replace its internals gradually.
Is robocopy still better than Copy-Item?
For large mirrored transfers, yes. It’s faster, resumable and handles long paths better. Call it from PowerShell rather than replacing it, and check $LASTEXITCODE -ge 8 for real failures.
Do I need to change scheduled tasks after migrating?
Yes. Point the action at pwsh.exe with -NoProfile -ExecutionPolicy Bypass -File and the script path, rather than calling the .bat directly.
Gear We Recommend
Testing scripts is easier with a dedicated admin machine set up right. Here’s the kit we use.
Browse our Admin Machine picks on AmazonAs an Amazon Associate, TechyGeeksHome earns from qualifying purchases
Disclosure: this post may contain affiliate links. If you buy through one of them, we may earn a small commission at no extra cost to you. We only recommend products we’ve tested or genuinely rate.
Discover more from TechyGeeksHome
Subscribe to get the latest posts sent to your email.