Home > OS >  How can I get a count of directories and files in a base directory and each descendant directory?
How can I get a count of directories and files in a base directory and each descendant directory?

Time:03-15

I am counting files and folders inside a path, which has several files as well as folders.

I am using this:

dir -recurse |  ?{ $_.PSIsContainer } | %{ Write-Host $_.FullName (dir $_.FullName | Measure-Object).Count }

Problem is that I want to know if the count contains sub folders. Also, the above command is not giving me the file count of the base folder.

e.g.

  • powershell\
    • Copy_files.ps1
    • powershell.txt
    • sortbydate.ps1
    • linux\
      • New Text Document (2).txt
      • New Text Document.txt

For above file structure, I am getting :

linux 2

While I wanted:

Powershell 3 (   1 directory )
linux 2

CodePudding user response:

This is my second attempt - Kind of ugly, but appears to do the job.

Get-ChildItem -Recurse -Directory | ForEach-Object {
    [Object[]]$Dirs = (Get-ChildItem -Path $_.FullName -Directory)
    $DirText = if($Dirs.Length -gt 0) {" (   $($Dirs.Length) directory )"} else {''}
    [Object[]]$Files = (Get-ChildItem -Path $_.FullName -File)
    "$(' ' * ($_.FullName -split '\\').Length)$($_.Name) $($Files.Length)$DirText"
}

CodePudding user response:

dir is an alias for Get-ChildItem which, as the name implies, gets the children of the specified location but not an item representing that location itself; for that you'll need Get-Item. It would appear that you're executing that pipeline in the powershell directory and, once ?{ $_.PSIsContainer } filters out any child files, %{ ... } is executed on the sole child directory, linux.

Basic implementations

What you can do is...

  1. Starting from some base location (the current directory)...
  2. Enumerate the children of the current location, keeping track of the count of containers (directories) and leaves (files) along the way
  3. When a container is encountered, either start processing it immediately or store it for processing later
  4. When all children have been enumerated, output the results for the current container

With this approach each container and leaf is "touched" (enumerated) exactly once.

Iterative implementation

This implements the above steps using a [Queue[]] of containers to process; a [Stack[]] could also be used. Since traversal begins with one container to process (the current location), a do { } while () loop is used to continue executing if there are any more containers to traverse.

# Stores the current container being processed; initialize to the current location
$current = Get-Item -Path '.' -Force

# Stores containers that have been encountered but not yet processed
# Using a Queue[PSObject] with Enqueue()/Dequeue() results in breadth-first traversal
# Using a Stack[PSObject] with    Push()/Pop()     results in   depth-first traversal
$pendingContainers = New-Object -TypeName 'System.Collections.Generic.Queue[PSObject]'

do
{
    $containerCount = 0
    $leafCount = 0

    foreach ($child in Get-ChildItem -LiteralPath $current.PSPath -Force)
    {
        if ($child.PSIsContainer)
        {
            $containerCount  

            # Store the child container for later processing
            $pendingContainers.Enqueue($child)
        }
        else
        {
            $leafCount  
        }
    }

    # Write-Output is superfluous here, though makes it explicit that its input will be sent down the pipeline
    Write-Output -InputObject (
        [PSCustomObject] @{
            Name           = $current.PSChildName
            ContainerCount = $containerCount
            LeafCount      = $leafCount
            TotalCount     = $containerCount   $leafCount
        }
    )
}
# Assign the next container to $current, if available; otherwise, exit the loop
# The second operand to -and works because assigning a non-$null value evaluates to $true
while ($pendingContainers.Count -gt 0 -and ($current = $pendingContainers.Dequeue()))
# For PowerShell (Core) 6 : while ($pendingContainers.TryDequeue([Ref] $current))

Using Group-Object for counting

You can use the Group-Object cmdlet to build a [Hashtable] keyed on whether a child is a container ($true) or a leaf ($false). This simplifies the above code a little at the expense of decrease efficiency and increased memory usage. Note that this can be adapted similarly for use in the recursive implementation in the next section.

# Stores the current container being processed; initialize to the current location
$current = Get-Item -Path '.' -Force

# Stores containers that have been encountered but not yet processed
# Using a Queue[PSObject] with Enqueue()/Dequeue() results in breadth-first traversal
# Using a Stack[PSObject] with    Push()/Pop()     results in   depth-first traversal
$pendingContainers = New-Object -TypeName 'System.Collections.Generic.Queue[PSObject]'

do
{
    $childrenByIsContainer = Get-ChildItem -LiteralPath $current.PSPath -Force |
        Group-Object -AsHashTable -Property 'PSIsContainer'
    $containerCount = $childrenByIsContainer[$true].Count
    $leafCount = $childrenByIsContainer[$false].Count

    foreach ($childContainer in $childrenByIsContainer[$true])
    {
        # Store the child container for later processing
        $pendingContainers.Enqueue($childContainer)
    }

    # Write-Output is superfluous here, though makes it explicit that its input will be sent down the pipeline
    Write-Output -InputObject (
        [PSCustomObject] @{
            Name           = $current.PSChildName
            ContainerCount = $containerCount
            LeafCount      = $leafCount
            TotalCount     = $containerCount   $leafCount
        }
    )
}
# Assign the next container to $current, if available; otherwise, exit the loop
# The second operand to -and works because assigning a non-$null value evaluates to $true
while ($pendingContainers.Count -gt 0 -and ($current = $pendingContainers.Dequeue()))
# For PowerShell (Core) 6 : while ($pendingContainers.TryDequeue([Ref] $current))

Recursive implementation

The above iterative implementation is perhaps more naturally written as a recursive function. The code is a bit shorter and perhaps easier to follow, although one disadvantage is that no results will be output for a given container until all of its descendants have been enumerated, which can cause a noticeable delay in large hierarchies.

function MeasureContainer($container)
{
    $containerCount = 0
    $leafCount = 0

    foreach ($child in Get-ChildItem -LiteralPath $container.PSPath -Force)
    {
        if ($child.PSIsContainer)
        {
            $containerCount  

            Write-Output -InputObject (
                MeasureContainer $child
            )
        }
        else
        {
            $leafCount  
        }
    }

    Write-Output -InputObject (
        [PSCustomObject] @{
            Name           = $container.PSChildName
            ContainerCount = $containerCount
            LeafCount      = $leafCount
            TotalCount     = $containerCount   $leafCount
        }
    )
}

MeasureContainer (Get-Item -Path '.' -Force)

To start the recursion, MeasureContainer is called at the end and passed the base container which, as before, is the current location.

Output

Executing any of the above code blocks will produce output objects like this...

Name ContainerCount LeafCount TotalCount
powershell 1 3 4
linux 0 2 2

...although the order in which they are output depends on the algorithm. You can then manipulate and view them with standard cmdlets such as Select-Object, Sort-Object, Where-Object, etc.

Also, since the code above is written in a provider-agnostic manner ("containers" and "leaves" vs. "directories" and "files") it will work on others types of PSDrives as well; for example, try running...

cd 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\'

...before the above code to see it enumerate and count registry keys (though not values, which are not items but item properties).

Cmdlet implementation

Here's a simple cmdlet that enhances the above iterative code by including each container's absolute path, relative path, and depth in the output object and allows limiting the maximum depth of traversal.

[CmdletBinding()]
param(
    [String] $Path = '.',                  # Default to current location
    [ValidateRange(-1, [Int32]::MaxValue)]
    [Int32] $MaxDepth = -1                 # Default to unlimited depth
)

# Stores the current container being processed; initialize to the base container from parameters
[PSObject] $current = [PSCustomObject] @{
    Container = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
    Depth = 0
}

if ($null -eq $current.Container)
{
    Write-Error -Message "The object referred to by the base path ""$Path"" could not be found."
}
elseif (-not $current.Container.PSIsContainer)
{
    Write-Error -Message "The object referred to by the base path ""$Path"" is not a container."
}
else
{
    # Stores containers that have been encountered but not yet processed
    # Using a Queue[PSObject] with Enqueue()/Dequeue() results in breadth-first traversal
    # Using a Stack[PSObject] with    Push()/Pop()     results in   depth-first traversal
    [System.Collections.Generic.Queue[PSObject]] $pendingContainers = 
        New-Object -TypeName 'System.Collections.Generic.Queue[PSObject]'

    do
    {
        [Int32] $containerCount = 0
        [Int32] $leafCount = 0
    
        #TODO: Handle errors due to inaccessible children
        foreach ($child in Get-ChildItem -LiteralPath $current.Container.PSPath -Force)
        {
            if ($child.PSIsContainer)
            {
                #TODO: Detect and exit directory cycles caused by junctions or symbolic links
                #      See [System.IO.FileAttributes]::ReparsePoint

                $containerCount  
    
                # If the current depth is within the depth limit, or there is no depth limit...
                if ($current.Depth -lt $MaxDepth -or $MaxDepth -eq -1)
                {
                    # Store the child container for later processing
                    $pendingContainers.Enqueue(
                        [PSCustomObject] @{
                            Container = $child
                            Depth = $current.Depth   1
                        }
                    )
                }
            }
            else
            {
                $leafCount  
            }
        }

        # Write-Output is superfluous here, though makes it explicit that its input will be sent down the pipeline
        Write-Output -InputObject (
            [PSCustomObject] @{
                                 # Display a "friendly" provider-specific path instead
                AbsolutePath   = Convert-Path -LiteralPath $current.Container.PSPath
                RelativePath   = if ($current.Depth -eq 0) {
                                     # Resolve-Path ... -Relative returns a path prefixed with ".." when
                                     # passed the current location; substitute a less roundabout value instead
                                     '.'
                                 } else {
                                     # Resolve-Path ... -Relative returns a path relative to the current
                                     # location and doesn't allow another base location to be specified, so
                                     # the location must changed before and reverted after resolution.  Bleh.
                                     Push-Location -LiteralPath $Path
                                     try
                                     {
                                         Resolve-Path -LiteralPath $current.Container.PSPath -Relative
                                     }
                                     finally
                                     {
                                         Pop-Location
                                     }
                                  }
                Name           = $current.Container.PSChildName
                Depth          = $current.Depth
                ContainerCount = $containerCount
                LeafCount      = $leafCount
                TotalCount     = $containerCount   $leafCount
            }
        )
    }
    # Assign the next container to $current, if available; otherwise, exit the loop
    # The second operand to -and works because assigning a non-$null value evaluates to $true
    while ($pendingContainers.Count -gt 0 -and ($current = $pendingContainers.Dequeue()))
    # For PowerShell (Core) 6 : while ($pendingContainers.TryDequeue([Ref] $current))
}

Output

When running...

.\SO71470092.ps1 -Path $PSHOME | Select-Object -First 25

...I get these results on Windows Powershell 5.1.19041.1320...

AbsolutePath RelativePath Name Depth ContainerCount LeafCount TotalCount
C:\Windows\System32\WindowsPowerShell\v1.0 . v1.0 0 6 29 35
C:\Windows\System32\WindowsPowerShell\v1.0\en .\en en 1 0 1 1
C:\Windows\System32\WindowsPowerShell\v1.0\en-US .\en-US en-US 1 0 271 271
C:\Windows\System32\WindowsPowerShell\v1.0\Examples .\Examples Examples 1 0 1 1
C:\Windows\System32\WindowsPowerShell\v1.0\Modules .\Modules Modules 1 75 0 75
C:\Windows\System32\WindowsPowerShell\v1.0\Schemas .\Schemas Schemas 1 1 0 1
C:\Windows\System32\WindowsPowerShell\v1.0\SessionConfig .\SessionConfig SessionConfig 1 0 0 0
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\AppBackgroundTask .\Modules\AppBackgroundTask AppBackgroundTask 2 1 5 6
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\AppLocker .\Modules\AppLocker AppLocker 2 1 2 3
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\AppvClient .\Modules\AppvClient AppvClient 2 2 7 9
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Appx .\Modules\Appx Appx 2 1 4 5
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\AssignedAccess .\Modules\AssignedAccess AssignedAccess 2 1 3 4
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\BitLocker .\Modules\BitLocker BitLocker 2 1 5 6
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\BitsTransfer .\Modules\BitsTransfer BitsTransfer 2 1 4 5
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\BranchCache .\Modules\BranchCache BranchCache 2 1 13 14
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\CimCmdlets .\Modules\CimCmdlets CimCmdlets 2 1 2 3
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\ConfigCI .\Modules\ConfigCI ConfigCI 2 1 2 3
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Defender .\Modules\Defender Defender 2 1 10 11
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\DeliveryOptimization .\Modules\DeliveryOptimization DeliveryOptimization 2 0 4 4
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\DirectAccessClientComponents .\Modules\DirectAccessClientComponents DirectAccessClientComponents 2 1 8 9
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Dism .\Modules\Dism Dism 2 2 6 8
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\DnsClient .\Modules\DnsClient DnsClient 2 1 16 17
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\EventTracingManagement .\Modules\EventTracingManagement EventTracingManagement 2 1 10 11
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\International .\Modules\International International 2 1 2 3
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\iSCSI .\Modules\iSCSI iSCSI 2 1 6 7
  • Related