Recursion in PowerShell : Getting the Size of Folders on a Hard Disk

Following on from the “What is Recursion?” post it’s time for a concrete example with PowerShell;  a script to calculate the sizes of all sub-folders within a folder.  The script (and description) follows.

You can get all the items within a folder quite easily in PowerShell;

Get-ChildItem C:\Source

and walking through all the sub-folders is dead easy too;

Get-ChildItem C:\Source -Recurse

Each file returned has a “Length” attribute which is the file’s size in bytes.  Unfortunately, this attribute doesn’t exist on folders.  What the GUI does (when you look at a folder’s size) is automatically add the sizes of it’s children together.  We need to do something similar.

We could probably then add all the sizes together and do some grouping with Measure-Object and Group-Object.  This would give us the size of each folder but wouldn’t automatically add the folders size to their parent folder’s size.  In order to do that we need a script.

Voila!

function Get-FolderSize
{
    <#
        .SYNOPSIS
        Orders the sub-folders in a given folder by their size on the disk.
        .DESCRIPTION
        When passed a path to a folder this function recurses through each sub-folder
        totalling the size of all the files.  The results are then sorted in descending
        order and outputted to the screen.
        .PARAMETER Path
        The target folder to enumerate.
        .PARAMETER First
        If specified this restricts the output to the first number of results given
        by this parameter.
        .EXAMPLE
        Get-FolderSize C:\Source
        This will total and output the size of all the folders within the folder C:\Source.
        .EXAMPLE
        Get-FolderSize C:\Windows -First 5
        This will total and output the size of all the folders within the folder C:\Windows
        and will only show the 5 largest.
    #>
    param
    (
        [Parameter(Mandatory=$True)]
        [ValidateScript(
            {
                if(!(Test-Path -Path $_ -PathType Container))
                {
                    Throw ("Source path ($_) does not exist or is not a folder.")

                }else
                {
                    $True
                }
            }
        )]
        [string]$Path,
        [int]$First=0
    )
    function Get-WorkingFolderSize
    {
        param
        (
            [Parameter(Mandatory=$True)]
            [string]$Path
        )
        $FolderList=@()
        #Recurse through subfolders in the path and add the size of any child items that aren't folders.
        Write-Debug "Entering Get-FolderSize function"
        Write-Debug "Parameter : Path = $Path"
        [int64]$Size=0;
        foreach ($ChildItem in Get-ChildItem -Path $Path)
        {
            #If the child item is a folder and is NOT a symbolic link, call Get-WorkingFolderSize
            #on it.
            if (!($ChildItem.Attributes -like "*ReparsePoint*"))
            {
                if ($ChildItem.PSIsContainer)
                {
                    $SubFolders=(Get-WorkingFolderSize -Path $ChildItem.FullName)
                    $Size=$Size+($Subfolders | Measure-Object Size -Sum).Sum
                    #Build an array of all the folders we've found with their sizes.
                    $FolderList=$FolderList+$SubFolders
                }else
                {
                    $Size=$Size+$ChildItem.Length

                }
            }
        }
        #The result of getting the folder size should be added to a custom PSObject, which is then added
        #to the array of folders.
        $CurrentFolderObject=New-Object PSObject -Property @{Path=$Path;Size=$Size}
        $FolderList=$FolderList+$CurrentFolderObject
        Write-Debug "Leaving Get-FolderSize function"
        return $FolderList
    }
    #These actually call the worker function and format output.
    if ($First -gt 0)
    {
        Get-WorkingFolderSize -Path $Path | Sort-Object -Property Size -Descending |
             Select-Object -Property @{Name="SizeInKB";Expression={("{0:N2}" -f ($_.Size / 1KB))}},
                Path -First $First | Format-Table -Autosize -Wrap
    }else
    {
        Get-WorkingFolderSize -Path $Path | Sort-Object -Property Size -Descending |
             Select-Object -Property @{Name="SizeInKB";Expression={("{0:N2}" -f ($_.Size / 1KB))}},
                Path | Format-Table -Autosize -Wrap
    }
}

It’s a bit lengthy but the crucial bit (the recursion) is pretty simple.

Lets break it down;

function Get-FolderSize
{
    param
    (
        [Parameter(Mandatory=$True)]
        [ValidateScript(
            {
                if(!(Test-Path -Path $_ -PathType Container))
                {
                    Throw ("Source path ($_) does not exist or is not a folder.")
 
                }else
                {
                    $True
                }
            }
        )]
        [string]$Path,
        [int]$First=0
    )

The parameters for the script are straight-forward; we need a path to a folder so that has to be mandatory. By default the script returns all the sub-folders under the path but we can make it only display a set number of the largest folders with the First parameter (i.e., –First 5 to get the 5 largest folders).

    function Get-WorkingFolderSize
    {
        param
        (
            [Parameter(Mandatory=$True)]
            [string]$Path
        )
        $FolderList=@()

The actual worker function is defined in the main body of the Get-FolderSize function. This is just so the Get-FolderSize function can include a few lines that format the output (so running the command outputs a formatted table of results). If you just want to output the raw objects you can just use the Get-WorkingFolderSize function and strip out the Get-FolderSize code from around it.

$Path is the path to the folder to process and is mandatory. Get-WorkingFolderSize will return an array including the folder $Path (and the size of its contents) and all the sub-folders inside it.

$FolderList is defined as an empty array. This will hold an array of objects defining each folder we find.

        [int64]$Size=0;
        foreach ($ChildItem in Get-ChildItem -Path $Path)
        {
            #If the child item is a folder and is NOT a symbolic link, call Get-WorkingFolderSize
            #on it.
            if (!($ChildItem.Attributes -like "*ReparsePoint*"))
            {
                if ($ChildItem.PSIsContainer)
                {
                    $SubFolders=(Get-WorkingFolderSize -Path $ChildItem.FullName)
                    $Size=$Size+($Subfolders | Measure-Object Size -Sum).Sum
                    #Build an array of all the folders we've found with their sizes.
                    $FolderList=$FolderList+$SubFolders
                }else
                {
                    $Size=$Size+$ChildItem.Length

                }
            }
        }

This is the meat of the function (and the script). It maintains a running total of the size of all files in the folder passed by $Path and this is stored in $Size.  The basic logic is;

  • Take $Path and check each item within it.
  • If the item is a file, add it to the running total of the size of the folder in $Path.
  • If it’s a folder, call the function recursively and pass it the path to the sub-folder.
  • Return an array of the folder passed with $Path and all the folders within it, with the sizes of their content.s

For each child item in the folder we first make sure it isn’t a symbolic link (if (!($ChildItem.Attributes -like “*ReparsePoint*”))). If it is we skip the item as it has no size in practical terms.

If the child item is a folder we recursively call the Get-WorkingFolderSize function again, passing the child-item path. The Get-WorkingFolderSize function returns an array of folders (each with their size) so we sum the size of all the items in the returned array and add that to our $Size variable. Finally we add the array that was returned to $FolderList (we’re going to return this at the end of the function).

Things are a lot easier if the child-item we’re processing is a file; we just get its length and add it to the running total of the size all items in the current folder ($Size).

        $CurrentFolderObject=New-Object PSObject -Property @{Path=$Path;Size=$Size}
        $FolderList=$FolderList+$CurrentFolderObject
        Write-Debug "Leaving Get-FolderSize function"
        return $FolderList

Once we’ve finished totalling the sizes of all the items in the folder pointed to by $Path we create a custom PSObject containing the current folder’s size and path and add it to the list of sub-folders we’ve already created ($FolderList). Then we return the array of PSObjects as the result of the function.

    if ($First -gt 0)
    {
        Get-WorkingFolderSize -Path $Path | Sort-Object -Property Size -Descending |
             Select-Object -Property @{Name="SizeInKB";Expression={("{0:N2}" -f ($_.Size / 1KB))}},
                Path -First $First | Format-Table -Autosize -Wrap
    }else
    {
        Get-WorkingFolderSize -Path $Path | Sort-Object -Property Size -Descending |
             Select-Object -Property @{Name="SizeInKB";Expression={("{0:N2}" -f ($_.Size / 1KB))}},
                Path | Format-Table -Autosize -Wrap
    }

Here is the main body of the Get-FolderSize function. As I mentioned earlier, this is purely to format the output so isn’t strictly necessary. But if you just want a ‘fire and forget’ script it’s useful.

There are two command pipelines and which gets used depends on whether the $First parameter has be specified. In both cases the output of Get-WorkingFolderSize is sorted (in descending order of Size) and the Size attribute is formatted in KB for legibility (Select-Object -Property @{Name=”SizeInKB”;Expression={(“{0:N2}” -f ($_.Size / 1KB))}}). The Path attribute is also selected and optionally the output is filtered to the first $First items. Finally the information is displayed as a formatted table.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s