Native Win32_Directory Scripting
I hope you've had an opportunity to try the code examples from the last few articles. I'm not implying that using CIM classes is necessarily the the best way to get file and folder information, but it might be a good alternative in some situations. Or if you have a very specific use case, it might be the best way to get the information you need. If nothing else, you should be able to learn from the code techniques in my examples.
There's nothing wrong with writing a function that uses Get-CimInstance
and Get-CimAssociatedInstance
. However, if you have the skills and information to take advantage of the native CIM classes, can eke out a little more performance.
The one thing to keep in mind is that if need to write Pester tests for your code, you can't mock a CIM class method. You would need to write a wrapper or helper function to abstract the CIM class method. That you could mock in a Pester test.
To recap, code like this should work.
[CimSession]$cs = 'thinkx1-jh'
$query = "Select * from win32_directory where name = 'C:\\temp'"
$d = $cs.QueryInstances('Root/cimv2', 'WQL', $query)
$d | Get-CimAssociatedInstance -ResultClassName Win32_Directory |
Where-Object { $_.Name -match "^$($d.name.replace('\','\\'))" } |
Select-Object Name
This gives me all the folders in the root of C:\Temp
.
Name
----
C:\temp\banana
C:\temp\contoso
C:\temp\docs
C:\temp\fonts
C:\temp\foo
C:\temp\ps
Let's assume we want to go native to get the associated instances. For that, we can use the EnumerateAssociatedInstances
method.
PS C:\> Get-TypeMember Microsoft.Management.Infrastructure.CimSession -Name EnumerateAssociatedInstances | Tee -Variable m
Type: Microsoft.Management.Infrastructure.CimSession
Name MemberType ResultType IsStatic IsEnum
---- ---------- ---------- -------- ------
EnumerateAssociatedInstances Method IEnumerable`1
PS C:\> $m.Syntax
$obj.EnumerateAssociatedInstances([String]namespaceName,[CimInstance]sourceInstance,[String]associationClassName,[String]resultClassName,[String]sourceRole,[String]resultRole)
$obj.EnumerateAssociatedInstances([String]namespaceName,[CimInstance]sourceInstance,[String]associationClassName,[String]resultClassName,[String]sourceRole,[String]resultRole,[CimOperationOptions]options)
The method has a few overloads. The first one should meet our needs. Here's an easier way to view the parameters.
PS C:\> $m.syntax[0].split(",")
$obj.EnumerateAssociatedInstances([String]namespaceName
[CimInstance]sourceInstance
[String]associationClassName
[String]resultClassName
[String]sourceRole
[String]resultRole)
You may be thinking, "How do I know what values to use?" Fortunately, we can use the Get-CimAssociatedInstance
cmdlet to get the information we need from Verbose output.

Based on this, you would try something like this.
$cs.enumerateAssociatedInstances('root/cimv2', $d, '', 'Win32_Directory', '', '')
This will fail with an error of MethodException: Cannot find an overload for "enumerateAssociatedInstances" and the argument count: "6".
That's not very helpful because the syntax calls for six parameters and that's what I did.
Let me show you how to get around this problem and then we'll look at how to create some tooling around CIM_DataFile
and Win32_Directory
.
Arrays vs IEnumerable
The problem is a subtle programming distinction that PowerShell scripters don't think about. Certainly not in the same way a .NET developer would. It comes down to output. What type of object is being returned when we use the QueryInstances
method?
PS C:\> Get-TypeMember Microsoft.Management.Infrastructure.CimSession -Name queryInstances
Type: Microsoft.Management.Infrastructure.CimSession
Name MemberType ResultType IsStatic IsEnum
---- ---------- ---------- -------- ------
QueryInstances Method IEnumerable`1
The result type shows IEnumerable
1`. This is a .NET interface that represents a collection of objects. But it isn't an array. It is a collection. I can verify this by running the following code.
PS C:\> $d = $cs.QueryInstances('Root/cimv2', 'WQL', $query)
PS C:\> $d.GetType().name
CimSyncInstanceEnumerable
This is the object that was causing the error when invoking the EnumerateAssociatedInstances
method.
PowerShell can work with enumerable collections, but sometimes you need to coerce the collection into an array. This is what is happening when you pipe the collection to ForEach-Object
.
PS C:\> $d = $cs.QueryInstances('Root/cimv2', 'WQL', $query) | foreach {$_}
PS C:\> $d.GetType().name
CimInstance
PS C:\> $d.count
1
Notice the type name difference? If you go back and look at the EnumerateAssociatedInstances
parameters, you'll see it is expecting [CimInstance]sourceInstance
. Now that I have a CimInstance
object, I can try the method again.
PS C:\> $cs.EnumerateAssociatedInstances('root/cimv2', $d, '', 'Win32_Directory', '', '') | Select -First 1 Name
Name
----
C:\temp\banana
Get-CimFolder
Now that I know how to properly get associated instances, I can build tooling around it.
Function Get-CimFolder {
[cmdletbinding()]
Param(
[Parameter(
Position = 0,
Mandatory,
HelpMessage = 'Enter a path like C:\temp without trailing slashes'
)]
[ValidateNotNullOrEmpty()]
[ValidateScript({ $_ -match '^[a-zA-Z]:\\\w+' })]
[string]$Path,
[Parameter(HelpMessage = 'Include hidden folders')]
[switch]$Hidden,
[Parameter(DontShow, HelpMessage = 'This parameter should be hidden from the user is used when calling the function recursively')]
[switch]$Child,
[Parameter(HelpMessage = 'Get top-level folders only')]
[switch]$NoRecurse,
[Parameter(ValueFromPipeline, HelpMessage = 'Specify a computer name or an existing CimSession object.')]
[ValidateNotNullOrEmpty()]
[Alias('CN')]
[CimSession]$CimSession = $ENV:COMPUTERNAME
)
Write-Verbose "Starting $($MyInvocation.MyCommand)"
#strip off trailing slashes from $Path
If ($Path -match '\\$') {
$Path = $Path.Remove($Path.length - 1, 1)
}
if ($Hidden) {
Write-Verbose 'Include hidden folders'
#need to escape possible regex characters in the path
$filter = { $_.Name -Match "$($top.name.replace('\','\\').replace('+','\+'))" }
}
else {
$filter = { (-Not $_.Hidden) -AND ($_.Name -Match "$($top.name.replace('\','\\').replace('+','\+'))") }
}
$Name = $Path.replace('\', '\\').Replace("'", "\'") #.Replace("+","``+")
$query = "Select * from win32_directory where name = '$Name'"
Write-Verbose $query
#cast the result to an array of objects
[object[]]$top = $CimSession.QueryInstances('root/cimv2', 'WQL', $query)
if (-Not $Child) {
#show the top folder
$top
}
Write-Verbose "EnumerateAssociatedInstances of Win32_Directory for $Name"
$CimSession.EnumerateAssociatedInstances('root/cimv2', $top[0], '', 'Win32_Directory', '', '') |
Where-Object $filter | ForEach-Object {
$_
if (-Not $NoRecurse) {
Get-CimFolder -Path $_.Name -Hidden:$Hidden -child -CimSession $CimSession
}
}
Write-Verbose "Ending $($MyInvocation.MyCommand)"
}
The function will recurse by calling itself, although I added a parameter if all I want are the top level folders.
PS C:\> Get-CimFolder c:\temp -NoRecurse | Format-Table Name, Hidden, LastModified -AutoSize
Name Hidden LastModified
---- ------ ------------
c:\temp False 6/11/2024 2:03:57 PM
c:\temp\briefcase False 5/30/2024 4:46:34 PM
c:\temp\buttondown False 2/1/2024 4:04:58 PM
c:\temp\dbox False 2/16/2024 8:39:56 AM
c:\temp\foo False 6/11/2024 2:03:57 PM
Otherwise, it will recurse.
Get-CimFolder -Path $_.Name -Hidden:$Hidden -child -CimSession $CimSession
In the function, the Child
parameter is used to control what gets displayed. The parameter is used by the code, not the user, so I am hiding it.
[Parameter(DontShow,HelpMessage = 'This parameter should be hidden from the user is used when calling the function recursively')]
[switch]$Child,
You'll still the parameter in auto-generated help, but tab-completion will ignore it. If you use the Platyps module to create external help, it too will be ignored and undocumented.
I'll show you a way to use this function in a few minutes.
Get-Win32Directory
I also decided to write a CIM-base alternative to Get-ChildItem.
This function takes a dependency on Get-CimFolder
.
. $PSScriptRoot\Get-CimFolder.ps1
Function Get-Win32Directory {
[cmdletbinding()]
Param(
[Parameter(
Position = 0,
Mandatory,
HelpMessage = "Enter a path like C:\temp without trailing slashes"
)]
[ValidateNotNullOrEmpty()]
[ValidateScript({ $_ -match '^[a-zA-Z]:\\\w+' })]
[string]$Path,
[switch]$Recurse,
[Parameter(
ValueFromPipeline,
HelpMessage = 'Specify a computer name or an existing CimSession object.'
)]
[ValidateNotNullOrEmpty()]
[Alias('CN')]
[CimSession]$CimSession = $ENV:COMPUTERNAME
)
Begin {
Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Starting $($MyInvocation.MyCommand)"
#initialize variables for the CimSession connection
New-Variable -Name ci
New-Variable -Name ce
} #begin
Process {
if ($CimSession.TestConnection([ref]$ci, [ref]$ce)) {
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Processing $Path"
if ($Recurse) {
Get-CimFolder -Path $Path | Foreach-Object { Get-Win32Directory -Path $_.Name -CimSession $CimSession }
}
else {
#Get top-level folder files only
$Name = $Path.replace('\', '\\')
$CimSession.QueryInstances('root/cimv2', 'WQL', "SELECT * FROM win32_directory WHERE Name='$Name'") |
ForEach-Object {
$CimSession.EnumerateAssociatedInstances('root/cimv2', $_[0], '', 'CIM_DataFile', '', '')
}
}
} #if TestConnection
else {
Write-Warning "Unable to connect to $($CimSession.ComputerName.ToUpper()). $($ce.Message)"
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeOfDay) END ] Ending $($MyInvocation.MyCommand)"
} #end
}
The function will write the full CIM_DataFile
object to the pipeline.
PS C:\> Get-Win32Directory -CimSession thinkx1-jh -Path c:\work | select -first 1 -Property *
Status : OK
Name : c:\work\01 2112.mp3
Caption : c:\work\01 2112.mp3
Description : c:\work\01 2112.mp3
InstallDate : 6/11/2024 1:17:14 PM
AccessMask : 18809343
Archive : True
Compressed : False
CompressionMethod :
CreationClassName : CIM_LogicalFile
CreationDate : 6/11/2024 1:17:14 PM
CSCreationClassName : Win32_ComputerSystem
CSName : THINKX1-JH
Drive : c:
EightDotThreeFileName : c:\work\012112~1.mp3
Encrypted : False
EncryptionMethod :
Extension : mp3
FileName : 01 2112
FileSize : 19766344
FileType : MP3 Format Sound
FSCreationClassName : Win32_FileSystem
FSName : NTFS
Hidden : False
InUseCount :
LastAccessed : 6/12/2024 11:10:21 AM
LastModified : 9/10/2023 5:53:57 PM
Path : \work\
Readable : True
System : False
Writeable : True
Manufacturer :
Version :
PSComputerName : thinkx1-jh
CimClass : root/cimv2:CIM_DataFile
CimInstanceProperties : {Caption, Description, InstallDate, Name…}
CimSystemProperties : Microsoft.Management.Infrastructure.CimSystemProperties
You could always create a custom object with a subset of properties. There are intriguing scenarios for this command. For one, it makes it easy to search remotely.
$r = Get-Win32Directory c:\temp -Recurse -CimSession thinkx1-jh
You can then play with the results.
$r | Format-Table -GroupBy @{Name = 'Folder'; Expression = { Split-Path $_.name -Parent } } -Property @{Name = 'FileName'; Expression = { Split-Path $_.Name -Leaf } },
FileSize, LastModified, FileType
$r | Group-Object FileType | Sort-Object count -Descending |
Select-Object Count, Name, @{Name = 'TotalSizeMB'; Expression = { ($_.Group | Measure-Object -Property FileSize -Sum).sum / 1KB } }

Get-CimFolderInfo
I also wrote a function to provide a summary for a given folder.
#requires -version 5.1
<#
Using CimSessions query a given folder recurse through all subfolders and files.
Return an object that shows the computer name, the top levelpath, the total number
of files, the total number of directories and the total size of all files.
#>
. $PSScriptRoot\Get-CimFolder
$PSDefaultParameterValues['Add-CimPath:Verbose'] = $False
Function Get-CimFolderInfo {
[cmdletbinding()]
[OutputType("cimFolderInfo")]
Param(
[Parameter(
Position = 0,
Mandatory,
ValueFromPipelineByPropertyName,
HelpMessage = 'Enter a path like C:\temp without trailing slashes'
)]
[Alias('FullName','Name')]
[ValidateNotNullOrEmpty()]
[ValidateScript({$_ -match '^[a-zA-Z]:\\\w+'})]
[string]$Path,
[Parameter(ValueFromPipeline, HelpMessage = 'Specify a computer name or an existing CimSession object.')]
[ValidateNotNullOrEmpty()]
[Alias('CN')]
[CimSession]$CimSession = $ENV:COMPUTERNAME
)
Begin {
Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Starting $($MyInvocation.MyCommand)"
Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Running in PowerShell version $($PSVersionTable.PSVersion.ToString())"
#initialize variables for the CimSession connection
New-Variable -Name ci
New-Variable -Name ce
$progParams = @{
Activity = $MyInvocation.MyCommand
Status = 'Processing'
CurrentOperation = 'Initializing'
PercentComplete = 0
}
} #begin
Process {
#strip off trailing slashes from $Path
If ($Path -match "\\$") {
$Path = $Path.Remove($Path.length-1,1)
}
#Save the top-level path for the output object
$TopPath = $Path
$progParams['CurrentOperation'] = "Getting all folders under $Path"
$FolderList = [System.Collections.Generic.List[object]]::new()
$FileList = [System.Collections.Generic.List[object]]::new()
If ($cimSession.TestConnection([ref]$ci, [ref]$ce)) {
$msg = "Processing $Path on $($CimSession.ComputerName.ToUpper())"
$progParams['Status'] = $msg
Write-Progress @progParams
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] $msg"
Try {
#save all folders in the top-level path so that I can calculate progress
$AllFolders = Get-CimFolder -Path $Path -CimSession $CimSession -ErrorAction Stop
$i = 0
ForEach ($folder in $AllFolders) {
$i++
$pct = ($i/$AllFolders.Count)*100
$progParams['PercentComplete'] = $pct
Write-Debug "Adding $($folder.Name)"
$FolderList.Add($folder)
$n =$folder.Name
$msg = "Getting files for $n"
$progParams['Status'] = $msg
Write-Progress @progParams
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] $msg"
$FolderPath = "$($n.split(":")[1].replace("\","\\").replace("'","\'"))\\"
$Drive = $n.Substring(0,2)
$query = "Select Name,FileSize from Cim_DataFile where Path = '$FolderPath' AND Drive = '$Drive'"
$progParams['Status'] = $query
Write-Progress @progParams
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] $query"
$CimSession.queryInstances("Root/cimv2", "WQL",$query) |
ForEach-Object {
$FileList.Add($_)
}
<#
This was too resource intensive on large folders and was failing
$CimSession.EnumerateAssociatedInstances('root/cimv2', ($_ | Add-CimPath), '', 'CIM_DataFile', '', '')
#>
}
}
Catch {
Write-Warning "Unable to process path $Path on $($CimSession.ComputerName.ToUpper()). $($_.Exception.Message)"
}
} #if test connection
else {
Write-Warning "Unable to connect to $($CimSession.ComputerName.ToUpper()). $($ce.Message)"
}
if ($FolderList.Count -gt 0) {
[PSCustomObject]@{
PSTypeName = 'cimFolderInfo'
Path = $TopPath
TotalFolders = $FolderList.Count - 1 #don't count the top folder
TotalFiles = $FileList.Count
TotalSize = ($FileList | Measure-Object -Property FileSize -Sum).Sum
ComputerName = $CimSession.ComputerName.ToUpper()
}
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeOfDay) END ] Ending $($MyInvocation.MyCommand)"
} #end
}
This works nicely and quickly for small folders.
PS C:\> Get-CimFolderInfo C:\temp\ -CimSession Thinkx1-jh
Path : C:\temp
TotalFolders : 9
TotalFiles : 302
TotalSize : 65230420717
ComputerName : THINKX1-JH
As I was developing this, I ran into problems enumerating associated instances on large folders. So I went the other way and get CIM_DataFile
instances for each enumerated folder.
PS C:\> Get-CimFolderInfo c:\scripts
Path : c:\scripts
TotalFolders : 1438
TotalFiles : 10606
TotalSize : 669937164
ComputerName : PROSPERO
Of course, I can get more granular.
Get-CimFolder c:\work -NoRecurse | Select-Object Name | Get-CimFolderInfo | Format-Table
I can get the top-level folders and report on each one.

Summary
That was a whirlwind of code at the end and I didn't discuss every design element. If you have questions on anything I did, please don't hesitate to leave a comment. If you have a question it is likely someone else does as well.
Again, using the CIM classes isn't always the best way to achieve a goal. The code samples I've provided should be considered models or starting points for your own projects. There is a lot of opportunity here, but you need to take some time to explore and try things out yourself.