Dealing with high levels of recursion with Get-ChildItem PowerShell cmdlet
A bit of context
Recently, I encountered an intriguing challenge. As part of our migration sprint, a customer is trying to extract permissions from a file share path. The plot thickened when the script they ran consistently failed, offering little diagnostic information. Even the Windows Server Event Viewer could only provide a little insight.
The script used the Get-ChildItem
command with the recurse
flag, allowing it to navigate through every item in the share. The root of the issue began to emerge when we were met with call stack depth errors after the script ran for an extended period and delved deeper into the file structure.
A call stack depth error typically indicates that a function's self-call limit has been reached. In using Get-ChildItem -Recurse
, there's a ceiling to how deep it can traverse. The exact limit varies based on the PowerShell version in use. It can range from 100 to 1,000 calls; in specific scenarios, it might be bound only by available system resources.
Solving the depth errors
The crux of the problem lies in the limited depth of the recurse
flag, necessitating a rethink of our script's approach. After some Googling and consulting with GPT, I decided to leverage the queuing feature available through PowerShell's broader .NET capabilities.
The goal is to queue items discovered in the folder structure for subsequent processing. This approach permits the use of Get-ChildItem
without the recurse
flag. While there are still some inherent limitations, they are significantly reduced compared to the previous method.
The PowerShell script
$path = 'E:\'
$errors = @{
'UnauthorizedAccess' = @()
'ItemNotFound' = @()
'IOError' = @()
'OtherErrors' = @()
}
# Create a function to handle the error reporting
function HandleError {
param($item)
switch ($_.Exception.GetType().FullName) {
'System.UnauthorizedAccessException' {
$script:errors['UnauthorizedAccess'] += $item
# Write-Host -Message "You do not have permission: $item" -ForegroundColor "Red"
}
'System.Management.Automation.ItemNotFoundException' {
$script:errors['ItemNotFound'] += $item
# Write-Host -Message "Path not found: $item" -ForegroundColor "Red"
}
'System.IO.IOException' {
$script:errors['IOError'] += $item
Write-Host -Message "IO error with path: $item" -ForegroundColor "Red"
}
default {
$script:errors['OtherErrors'] += $item
# Write-Host -Message "Error details: $_" -ForegroundColor "Red"
}
}
}
# Start with the root path
$queue = [System.Collections.Generic.Queue[string]]::new()
$queue.Enqueue($path)
$processedCount = 0
# Process each item individually
while ($queue.Count -gt 0) {
$currentPath = $queue.Dequeue()
$processedCount++
try {
$items = Get-ChildItem $currentPath -Force -ErrorAction Stop
# Enqueue directories for further processing
foreach ($dir in $items | Where-Object { $_.PSIsContainer }) {
$queue.Enqueue($dir.FullName)
}
} catch {
HandleError $currentPath
}
# Display processed count for every 1000 items to avoid flooding the console
if ($processedCount % 1000 -eq 0) {
Write-Host "Processed $processedCount paths."
}
}
Write-Host "Processing complete. Total paths processed: $processedCount."
Before you hit run
A final point to consider: the script employs the force
flag to ensure a comprehensive scan of the entire file structure. However, depending on the content stored and the path configuration, this might yield more information than necessary.