Avoid Wildcard Pitfalls in PowerShell: Use -LiteralPath for Safe, Predictable File Operations
Square brackets, asterisks, and question marks look harmless in file names—until PowerShell treats them as wildcards. If you copy a file named report[final].csv with -Path, you might accidentally match multiple files or none at all. The fix is simple and reliable: default to -LiteralPath for any operation that targets an exact file system path.
In this post, you'll learn why wildcards sneak in, how -LiteralPath prevents surprising matches, and a safe pattern for validation, logging, and error handling. You'll walk away with copy-and-paste snippets you can drop into automation scripts and CI/CD pipelines today.
Why [] and other characters bite—and how -LiteralPath saves you
Know your wildcards
PowerShell (and underlying providers) interpret certain characters in -Path as wildcards:
*matches zero or more characters?matches a single character[]defines a character class (e.g.,[0-9])
That means a path like C:\drop\report[final].csv can be interpreted as a pattern rather than a literal file name, potentially matching other files in the directory or failing unexpectedly.
Prefer -LiteralPath for exact matches
When you use -LiteralPath, PowerShell bypasses wildcard expansion completely. The string you pass is treated as an exact path—even if it contains [, ], *, or ?. This is the safest and most predictable way to target specific files and directories in scripts, automation tasks, and production runbooks.
Example: safe copy with literal paths
$src = 'C:\drop\report[final].csv'
$dstDir = 'C:\safe\backup'
$dst = Join-Path -Path $dstDir -ChildPath 'report[final].csv'
New-Item -ItemType Directory -Path $dstDir -Force | Out-Null
if (-not (Test-Path -LiteralPath $src)) {
Write-Warning ('Missing file: {0}' -f $src)
return
}
Copy-Item -LiteralPath $src -Destination $dst -Force -ErrorAction Stop
$full = Resolve-Path -LiteralPath $dst
Write-Host ('Copied -> {0}' -f $full)
# Using -Path here would treat [] as wildcardsThis pattern ensures PowerShell never treats the name as a wildcard, avoids accidental multi-file operations, and gives you a clear, validated flow.
A safe, repeatable copy pattern
Make the following steps a habit whenever you touch the file system:
- Build paths safely with
Join-Pathinstead of concatenation. - Validate existence with
Test-Path -LiteralPathbefore acting. - Log the action you are about to take (where, what, why) for auditability.
- Stop on errors so you don’t continue in a broken state (
-ErrorAction Stopandtry/catch). - Prefer literal operations for all file manipulations (
Copy-Item,Remove-Item,Move-Item,Get-Item).
Hardened function with validation, logging, and WhatIf
Here’s a reusable function that demonstrates a safe pattern. It validates inputs, logs intent, supports dry runs with -WhatIf, and uses -LiteralPath everywhere.
function Copy-LiteralSafe {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string]$Source,
[Parameter(Mandatory)]
[string]$DestinationDir,
[string]$LogFile,
[switch]$VerboseLogging
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path -LiteralPath $Source)) {
throw "Source not found: $Source"
}
if (-not (Test-Path -LiteralPath $DestinationDir)) {
New-Item -ItemType Directory -Path $DestinationDir -Force | Out-Null
}
$leaf = Split-Path -Leaf -Path $Source
$dest = Join-Path -Path $DestinationDir -ChildPath $leaf
$msg = "Copy $Source -> $dest"
if ($VerboseLogging) { Write-Host $msg }
if ($LogFile) { Add-Content -LiteralPath $LogFile -Value ("{0:u} {1}" -f (Get-Date), $msg) }
if ($PSCmdlet.ShouldProcess($dest, "Copy from literal source")) {
Copy-Item -LiteralPath $Source -Destination $dest -Force
$resolved = Resolve-Path -LiteralPath $dest
Write-Verbose ("Copied to {0}" -f $resolved)
return $resolved
}
}
# Dry-run example
Copy-LiteralSafe -Source 'C:\drop\report[final].csv' -DestinationDir 'C:\safe\backup' -VerboseLogging -WhatIf
# Commit example
Copy-LiteralSafe -Source 'C:\drop\report[final].csv' -DestinationDir 'C:\safe\backup' -LogFile 'C:\logs\file-ops.log' -VerboseLoggingNotice the pattern: validate with Test-Path -LiteralPath, build destinations with Join-Path, then commit with Copy-Item -LiteralPath. Logging captures intent and result, while -WhatIf gives you a safe dry run.
Safer deletes, moves, and checks
- Delete:
Remove-Item -LiteralPath 'C:\temp\weird[file].tmp' -Force - Move:
Move-Item -LiteralPath 'C:\in\report[final].csv' -Destination 'C:\archive\report[final].csv' -Force - Existence check:
Test-Path -LiteralPath 'C:\in\report[final].csv' - Inspect:
Get-Item -LiteralPath 'C:\in\report[final].csv'
Operational tips: validation, logging, and dry runs
1) Validate with Test-Path and fail fast
Checking existence before acting protects you from surprises and unclear error messages. When you do act, set -ErrorAction Stop so exceptions bubble up to a try/catch block:
try {
if (-not (Test-Path -LiteralPath $src)) {
throw "Missing file: $src"
}
Copy-Item -LiteralPath $src -Destination $dst -Force -ErrorAction Stop
}
catch {
Write-Error ("Copy failed: {0}" -f $_)
# Optionally: exit 1 for CI/CD
}2) Log the action before you commit
Log intent first, then execute. This makes reviews and audits clearer:
$intent = "Copy $src -> $dst"
Write-Information $intent
Add-Content -LiteralPath 'C:\logs\ops.log' -Value ("{0:u} {1}" -f (Get-Date), $intent)
Copy-Item -LiteralPath $src -Destination $dst -Force -ErrorAction StopPrefer Write-Information for structured, controllable output (tied to $InformationPreference) instead of Write-Host for logs.
3) Use -WhatIf and -Confirm via ShouldProcess
For scripts and modules, opt into SupportsShouldProcess. This lets callers use -WhatIf and -Confirm without you changing logic:
My-Command -WhatIfprints intended changes.My-Command -Confirmprompts before destructive operations.
4) When you must use -Path (and wildcards are intended)
Sometimes you really do want pattern matching. Use -Path, but escape special characters in literal segments with the backtick:
$src = 'C:\drop\report`[final`].csv' # Escapes [ and ]
Copy-Item -Path $src -Destination 'C:\safe\backup\report`[final`].csv' -ForceAs soon as you need exact matching again, switch back to -LiteralPath.
5) CI/CD and production pipelines
- Artifacts and releases: Treat build artifacts as literal paths; fail fast if missing; log SHA256 and file sizes.
- Backups and rotations: Move/Copy with
-LiteralPathto avoid wildcard expansions that could sweep whole directories. - Permissions and security: Combine
-LiteralPathwith explicit ACL updates to prevent accidental broad changes.
Performance and reliability notes
- Provider-level filtering: If you need filtering in large directories, use
-Filterwhere supported for performance, but keep literal segments literal (e.g., literal parent dir + filter). - Avoid string concatenation: Use
Join-Pathto handle separators and readability. - Consistency: Make
-LiteralPathyour default mental model; reach for-Pathonly when you truly need wildcards.
Security best practices
- Principle of least surprise: Literal paths reduce accidental data exposure or destructive operations.
- Auditability: Log intent before commit; include user, timestamp, and working directory.
- Guard deletes and overwrites: Pair
-LiteralPathwith-WhatIfand/or-Confirmfor high-risk commands likeRemove-Itemorrobocopymirroring.
Quick checklist
- Use
-LiteralPathfor all direct file references. - Validate with
Test-Path -LiteralPathbefore action. - Use
-ErrorAction Stopandtry/catchto fail fast. - Log the action before you commit it.
- Enable
-WhatIf/-Confirmwhere possible.
What you get:
- Fewer surprises
- Safer copies and deletes
- Predictable, testable results
Make exact path handling a habit. 📘 Read the PowerShell Advanced CookBook → https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/