Dealing with high levels of recursion with Get-ChildItem PowerShell cmdlet

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.

Get-ChildItem (Microsoft.PowerShell.Management) - PowerShell
The Get-ChildItem cmdlet gets the items in one or more specified locations. If the item is a container, it gets the items inside the container, known as child items. You can use the Recurse parameter to get items in all child containers and use the Depth parameter to limit the number of levels to re…

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.

Queue<T> Class (System.Collections.Generic)
Represents a first-in, first-out collection of objects.

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.

This is part of the Toolbox Tuesday series

A range of tools and tricks for your tech toolbox, from PowerShell scripts to document templates!

View more articles