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...
- Starting from some base location (the current directory)...
- Enumerate the children of the current location, keeping track of the count of containers (directories) and leaves (files) along the way
- When a container is encountered, either start processing it immediately or store it for processing later
- 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 PSDrive
s 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 |