Jump to page sections

Recursively Remove Empty Folders, using Tail Recursion in PowerShell

Some time around 2011-2014, I wrote a script to recursively delete empty folders (if you visited the old link from some forum or blog, you have now been redirected to this updated article). I feel obligated to provide the visitors with a better alternative since the old, dead link still gets some poor visitors.

My solution is loosely based on Kirk Munroe's excellent answer on Stack Overflow. He explains that Get-ChildItem performs head recursion, while what we need is tail recursion.

Quote: "Since you want to remove empty folders, and also remove their parent if they are empty after you remove the empty folders, you need to use tail recursion instead, which processes the folders from the deepest child up to the root. By using tail recursion, there will be no need for repeated calls to the code that removes the empty folders -- one call will do it all for you."

It also handles hidden files and directories. A directory containing a hidden file will not be deleted, while an empty hidden directory will be deleted.

This code is compatible with PowerShell version 2, and there are also a few minor tweaks (PSv2 compatibility = not using the -Directory parameter for Get-ChildItem).

Function Code

Commented and mostly self-explanatory. For syntax highlighting or downloading a script file, visit GitHub here.

function Remove-EmptyFolders {
    <#
    .SYNOPSIS
        Remove empty folders recursively from a root directory.
        The root directory itself is not removed.

        Author: Joakim Borger Svendsen, Svendsen Tech, Copyright 2022.
        MIT License.
    .EXAMPLE
        . .\Remove-EmptyFolders.ps1
        Remove-EmptyFolders -Path E:\FileShareFolder
    .EXAMPLE
        Remove-EmptyFolders -Path \\server\share\data

        NB. You might have to change $ChildDirectory.FullName to
        $ChildDirectory.ProviderPath in the code for this to work.
        Untested with UNC paths as of 2022-01-28.
    
    #>
    [CmdletBinding()]
    Param(
        [String] $Path
    )
    Begin {
        [Int32] $Script:Counter = 0
        if (++$Counter -eq 1) {
            $RootPath = $Path
            Write-Verbose -Message "Saved root path as '$RootPath'."
        }
        # Avoid overflow. Overly cautious? ~2.15 million directories...
        if ($Counter -eq [Int32]::MaxValue) {
            $Counter = 1
        }
    }
    Process {
        # List directories.
        foreach ($ChildDirectory in Get-ChildItem -LiteralPath $Path -Force |
            Where-Object {$_.PSIsContainer}) {
            # Use .ProviderPath on Windows instead of .FullName,
            # in order to support UNC paths (untested).
            # Process each child directory recursively.
            Remove-EmptyFolders -Path $ChildDirectory.FullName
        }
        $CurrentChildren = Get-ChildItem -LiteralPath $Path -Force
        # If it's empty, the condition below evaluates to true. Get-ChildItem 
        # returns $null for empty folders.
        if ($null -eq $CurrentChildren) {
            # Do not delete the root folder itself.
            if ($Path -ne $RootPath) {
                Write-Verbose -Message "Removing empty folder '$Path'."
                Remove-Item -LiteralPath $Path -Force
            }
        }
    }
}

Test Run

We see that it removes all the folders except ./testdeldir/a/d, which contains a file. The recursion works, and nested empty directories are deleted up to the root path (but not including the root path).

PS /tmp> New-Item -ItemType Directory -Path ./testdeldir/a/x/g/d | Out-Null

PS /tmp> New-Item -ItemType Directory -Path ./testdeldir/a/b/c/e | Out-Null

PS /tmp> New-Item -ItemType Directory -Path ./testdeldir/a/c | Out-Null    

PS /tmp> New-Item -ItemType Directory -Path ./testdeldir/a/d/f | Out-Null

PS /tmp> 'testfile' | Set-Content ./testdeldir/a/d/file.txt

PS /tmp> . ./Remove-EmptyFolders.ps1 # dot-source to load the function

PS /tmp> Remove-EmptyFolders -Path ./testdeldir/ -Verbose                  

VERBOSE: Saved root path as './testdeldir/'.
VERBOSE: Removing empty folder '/tmp/testdeldir/a/b/c/e'.
VERBOSE: Removing empty folder '/tmp/testdeldir/a/b/c'.
VERBOSE: Removing empty folder '/tmp/testdeldir/a/b'.
VERBOSE: Removing empty folder '/tmp/testdeldir/a/c'.
VERBOSE: Removing empty folder '/tmp/testdeldir/a/d/f'.
VERBOSE: Removing empty folder '/tmp/testdeldir/a/x/g/d'.
VERBOSE: Removing empty folder '/tmp/testdeldir/a/x/g'.
VERBOSE: Removing empty folder '/tmp/testdeldir/a/x'.

PS /tmp> gci -rec ./testdeldir/


    Directory: /tmp/testdeldir

UnixMode   User             Group                 LastWriteTime           Size Name
--------   ----             -----                 -------------           ---- ----
drwxrwxr-x joakim           joakim              1/28/2022 23:14           4096 a

    Directory: /tmp/testdeldir/a

UnixMode   User             Group                 LastWriteTime           Size Name
--------   ----             -----                 -------------           ---- ----
drwxrwxr-x joakim           joakim              1/28/2022 23:14           4096 d

    Directory: /tmp/testdeldir/a/d

UnixMode   User             Group                 LastWriteTime           Size Name
--------   ----             -----                 -------------           ---- ----
-rw-rw-r-- joakim           joakim              1/28/2022 23:14              9 file.txt

Alternative

As mentioned on Stack Overflow, in this ingenius approach, the simple piece of code below also works. It sorts descending, ensuring children are always processed before parents. This makes it delete recursively when it tests the, depth-first, directories for children and deleted if the count of items inside is 0. Hidden files and folders are handled, by using the -Force parameter for Get-ChildItem.

Get-ChildItem -LiteralPath $FolderToProcess -Recurse -Force -Directory | 
    Sort-Object -Property FullName -Descending |
    Where-Object { ($_ | Get-ChildItem -Force | Select-Object -First 1).Count -eq 0 } |
    Remove-Item -Verbose

To make this compatible with PowerShell version 2, you can use this:

Get-ChildItem -LiteralPath $FolderToProcess -Recurse -Force | 
    Where-Object {$_.PSIsContainer}
    Sort-Object -Property FullName -Descending |
    Where-Object {-not ($_ | Get-ChildItem -Force | Select-Object -First 1)} |
    Remove-Item -Verbose
Powershell      Windows      Recursion      Function      Programming          All Categories

Google custom search of this website only

Minimum cookies is the standard setting. This website uses Google Analytics and Google Ads, and these products may set cookies. By continuing to use this website, you accept this.

If you want to reward my efforts