Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Answering the CIM Directory Challenge

Posted on January 8, 2021January 8, 2021
folder with document

The last Iron Scripter challenge of 2020 was a big one. If you didn't get a chance to work on it, see what you can come up with then come back to see my approach. As with many of the challenges, the goal isn't to produce a production-ready PowerShell tool, but rather to push the limits of your PowerShell scripting and tool-making ability. This is a terrific challenge in that regard.

Manage and Report Active Directory, Exchange and Microsoft 365 with
ManageEngine ADManager Plus - Download Free Trial

Exclusive offer on ADManager Plus for US and UK regions. Claim now!

The goal was to get a directory listing of files and folders using WMI/CIM . This could be a potential alternative to a traditional filesystem listing. As you'll see, it is possible to accomplish this task. However, not every file and/or folder is registered and I haven't discovered a consistent reason. This means there's no guarantee that when I run my code that I am getting a definitive answer. But the goal is to learn so I'm OK with that. Let's dig in.

Win32_Directory

The basis of a solution begins with the Win32_Directory WMI class. I'll filter for a single folder to get an idea of what it looks like.

Get-CimInstance win32_directory -Filter "Name='C:\\Work\\samples'" | Tee-Object -Variable d

You can also use Get-CimClass to examine the class properties. Remember that with WMI/CIM filters to use the legacy operators like the = sign. The \ is a special character and needs to be escaped.

win32_directory

Now for the fun part. In WMI, most everything is related. Use the Get-CimAssociatedInstance command to retrieve related objects. You can limit what type of results by filtering on a class name.

associated win32_directory

For my purpose, l only want to see immediate child directories, so I'll modify the code to filter out the parent.

$d | Get-CimAssociatedInstance -ResultClassName Win32_Directory | 
Where-Object { (Split-Path $_.name) -eq $d.name } |
Select-Object Hidden, Archive, System, Compressed, Writeable, Encrypted, LastModified, Name, Path

CIM_DataFile

Now that I have a way to get folders, I need to find files. This will use the CIM_DataFile class. I can use the same technique.

$d | Get-CimAssociatedInstance -ResultClassName CIM_DATAFile | Select-Object -First 1 -Property *
CIM_Datafile

Again, I can also use Get-CimClass to discover properties, but often it helps to see a real-world example.

Part of the challenge is to duplicate the output from Get-ChildItem so something like this is a good proof-of-concept.

$d | Get-CimAssociatedInstance -ResultClassName CIM_DATAFile |
Format-Table -GroupBy @{Name = "Path"; Expression = { Resolve-Path (Join-Path -Path $_.drive -ChildPath $_.Path) } } -Property  Hidden, Archive, System, LastModified, FileSize, Name

Creating a Custom Class

Now that I know how to retrieve files and folders, I want an easier way to display them, without having to rely on cumbersome Select-Object syntax. My answer is to create PowerShell classes.

Class cimFolder {
    [string]$FullName
    [string]$Name
    [bool]$Archive
    [bool]$Compressed
    [DateTime]$CreationDate
    [string]$Computername
    [string]$Drive
    [bool]$Encrypted
    [bool]$Hidden
    [DateTime]$LastAccessed
    [DateTime]$LastModified
    [string]$Path
    [bool]$Readable
    [bool]$System
    [bool]$Writeable
    [string]$Mode
}

Class cimFile {
    [string]$FullName
    [string]$Name
    [bool]$Archive
    [bool]$Compressed
    [DateTime]$CreationDate
    [string]$Computername
    [string]$Drive
    [bool]$Encrypted
    [bool]$Hidden
    [int64]$FileSize
    [DateTime]$LastAccessed
    [DateTime]$LastModified
    [string]$Path
    [bool]$Readable
    [bool]$System
    [bool]$Writeable
    [string]$Mode
}

Essentially, these are clones of the original WMI classes. Instead of defining a constructor, I'll write a PowerShell function that will "convert" the original WMI class into my custom class.

Function New-CimFile {
    [cmdletbinding()]
    Param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [object]$CimObject
    )
    Begin {
        $properties = [CimFile].DeclaredProperties.Name
    }
    Process {
        $file = [CimFile]::New()
        foreach ($item in $properties) {
            $file.$item = $CimObject.$item
        }

        $file.name = Split-Path -Path $CimObject.caption -Leaf
        $file.fullname = $CimObject.Caption
        $file.computername = $CimObject.CSName
        $file.mode = Get-Mode $CimObject

        $file
    }
    End {
        #not used
    }
}

I also wrote a helper function to get the file mode.

Function Get-Mode {
    [cmdletbinding()]
    param([object]$CimObject)

    # use the ternary operator to simplify the code,
    # although this will require PowerShell 7.x
    $dir = $CimObject.CimClass.Cimclassname -match 'Directory' ? "d" : "-"
    $archive = $CimObject.archive ? "a" : "-"
    $ro = $CimObject.writeable ? "-" : "r"
    $system = $CimObject.System ? "s" : "-"
    $hidden = $CimObject.Hidden ? "h" : "-"

    "{0}{1}{2}{3}{4}" -f $Dir, $Archive, $RO, $Hidden, $System
}

Note that this function uses the ternary operator so it will only run in PowerShell 7. There's no real reason to use the operator other than I wanted an excuse to try it out. With these elements loaded into my PowerShell session, I can verify the concept.

$d | Get-CimAssociatedInstance -ResultClassName CIM_DATAFile | New-CimFile

I can do a similar process for directory objects. Finally, I can put it all together in a single command.

Function Get-CimFolder {
    [cmdletbinding(DefaultParameterSetName = "computer")]
    [alias("cdir")]
    [OutputType("cimFolder", "cimFile")]
    Param(
        [Parameter(Position = 0, HelpMessage = "Enter the folder path. Don't include the trailing \.")]
        [ValidateNotNullorEmpty()]
        [string]$Path = ".",
        [switch]$Recurse,
        [Parameter(ParameterSetName = "computer")]
        [ValidateNotNullOrEmpty()]
        [alias("cn")]
        [string]$Computername = $ENV:Computername,
        [Parameter(ParameterSetName = "session")]
        [ValidateNotNullOrEmpty()]
        [Microsoft.Management.Infrastructure.CimSession]$CimSession
    )
    Begin {
        Write-Verbose "Starting $($myinvocation.MyCommand)"
        $cimParams = @{
            Classname  = "win32_directory"
            Filter     = ""
            CimSession = ""
        }

    } #begin
    Process {
        #convert Path to a file system path
        if ($path -match '\\$') {
            Write-Verbose "Stripping off a trailing slash"
            $path = $path -replace "\\$", ""
        }
        Try {
            $cpath = Convert-Path -Path $path -ErrorAction Stop
            #escape any \ in the path
            $rPath = $cpath.replace("\", "\\")
            $cimParams.Filter = "Name='$rpath'"
            Write-Verbose "Using query $($cimparams.filter)"
        }
        Catch {
            Write-Warning "Can't validate the path $path. $($_.Exception.Message)"
            #bail out
            return
        }
        if ($pscmdlet.ParameterSetName -eq 'computer') {
            Try {
                $cimSession = New-CimSession -ComputerName $computername -ErrorAction Stop
                $tmpSession = $True
            }
            Catch {
                Throw $_
            }
        }
        Else {
            Write-Verbose "Using an existing CIMSession to $($cimsession.computername)"
            $tmpSession = $False
        }
        $cimParams.Cimsession = $CimSession
        Write-Verbose "Getting $cpath on $($cimsession.computername)"
        $main = Get-CimInstance @cimParams

        #filter out the parent folder
        $main | Get-CimAssociatedInstance -ResultClassName Win32_Directory |
            Where-Object { (Split-Path $_.name) -eq $main.name } | New-CimFolder -outvariable cf

        $main | Get-CimAssociatedInstance -ResultClassName CIM_DATAFile | New-Cimfile

        if ($cf -AND $recurse) {
            Write-Verbose "Recursing..."
            foreach ($fldr in $cf.fullname) {
                Write-Verbose $fldr
                Get-CimFolder -path $Fldr -CimSession $cimSession
            }
        }

        if ($cimSession -AND $tmpSession) {
            Write-Verbose "Remove the temporary session"
            Remove-CimSession $cimSession
        }
    } #Process
    End {
        Write-Verbose "Ending $($myinvocation.MyCommand)"
    }
}

Because I'm using the CIM cmdlets, I have an option to query a folder on a remote machine, which was also part of the challenge. My function uses parameter sets to support connecting by computer name and by CIMSession.

Customizing the Results

If you try the code, you'll see that the results are the full object with all the properties. Not necessarily easy to read and certainly not meeting the challenge objectives. One of the reasons for creating my own unique class is that I can define custom type extensions, such as a set of default properties.

Update-TypeData -TypeName cimFolder -DefaultDisplayPropertySet Mode, LastModified, Size, Name -Force
Update-TypeData -TypeName cimFile -MemberType AliasProperty -MemberName Size -Value FileSize -Force
Update-TypeData -TypeName cimFile -DefaultDisplayPropertySet Mode, LastModified, Size, Name -Force

This cleans up the output considerably.

But defined objects can also have custom formatting files. I created one using my trusty New-PSFormatXML command.

<?xml version="1.0" encoding="UTF-8"?>
<!--
Format type data generated 10/21/2020 17:19:32 by PROSPERO\Jeff
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools

This file will only work properly in PowerShell 7
-->
<Configuration>
  <SelectionSets>
    <SelectionSet>
      <Name>cimFileTypes</Name>
      <Types>
        <TypeName>cimFolder</TypeName>
        <TypeName>cimFile</TypeName>
      </Types>
    </SelectionSet>
  </SelectionSets>
  <ViewDefinitions>
    <View>
      <Name>default</Name>
      <ViewSelectedBy>
        <SelectionSetName>cimFileTypes</SelectionSetName>
      </ViewSelectedBy>
      <GroupBy>
        <!--
            You can also use a scriptblock to define a custom property name.
            You must have a Label tag.
            <ScriptBlock>$_.machinename.toUpper()</ScriptBlock>
            <Label>Computername</Label>

            Use <Label> to set the displayed value.
-->
        <Label>Path</Label>
        <ScriptBlock>Join-Path -path $_.drive -childpath $_.path  </ScriptBlock>
      </GroupBy>
      <TableControl>
        <!--Delete the AutoSize node if you want to use the defined widths.
        <AutoSize />-->
        <TableHeaders>
          <TableColumnHeader>
            <Label>Mode</Label>
            <Width>6</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>LastModified</Label>
            <Width>24</Width>
            <Alignment>right</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Size</Label>
            <Width>14</Width>
            <Alignment>right</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Name</Label>
            <!--  <Width>10</Width> -->
            <Alignment>left</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <!--
            By default the entries use property names, but you can replace them with scriptblocks.
            <ScriptBlock>$_.foo /1mb -as [int]</ScriptBlock>
-->
              <TableColumnItem>
                <PropertyName>Mode</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>LastModified</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Size</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <ScriptBlock>
                  <!-- show Directories, hidden, compressed or encrypted files in color using ANSI-->
                  if ($_.GetType().Name -eq 'cimFolder') {
                    "`e[38;5;228m$($_.name)`e[0m"
                  }
                  elseif ($_.Encrypted -OR $_.compressed) {
                      "`e[38;5;201m$($_.name)`e[0m"
                  }
                  elseif ($_.Hidden) {
                    "`e[38;5;105m$($_.name)`e[0m"
                    }
                  else {
                    $_.name
                  }
                </ScriptBlock>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

The file uses scriptblocks to display certain files in color, which was yet another challenge task. My code uses ANSI escape sequences for PowerShell 7. Once I load the file with Update-FormatData, look at the amazing difference.

formatted output

Creating a Module

As you can see, there are a lot of moving parts to this solution. I decided to take the extra step and pull everything together into a module called CimFolder. You can find the module on GitHub at https://github.com/jdhitsolutions/CimFolder. The module is a proof-of-concept and not necessarily production-ready code so I have no plans to publish it to the PowerShell Gallery.

There is also a potential for incomplete results. Not every file and folder is registered with WMI. In my testing I have come across folders that my command fails to enumerate, yet Get-ChildItem works just fine. I have yet to discover a consistent explanation or fix which is why I wouldn't rely on my code for production. However, I hope how and why I put things together has been educational.

The module has the latest updates to the code samples in this blog post. But I have to say, I am quite happy with the results.

I hope you'll browse through the code for ideas and inspiration.

Expand Your Skills

I hope you'll keep an eye on the Iron Scripter site and try your hand at the challenges. The challenges are open-ended so I encourage you to browse the site and tackle a few of them.

If you like learning from challenges, I suggest getting a copy of The PowerShell Practice Primer. This book contains 100 practice problems that begin on a very easy level and increase in difficulty. Answers should be no more than a few PowerShell expressions you would run in the console. These are not scripting challenges.

Or to take your scripting skills to the next level, take a look at The PowerShell Scripting and Toolmaking Book which is also on LeanPub.

In any event, the best way to improve your PowerShell skills is to simply continue to use them every day.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

3 thoughts on “Answering the CIM Directory Challenge”

  1. Jeffery Hicks says:
    January 8, 2021 at 2:10 pm

    Since I originally posted this, I have pushed a new version of the module to GitHub which fixes a critical recursion bug. The bug explains why I wasn’t seeing files and folders I expected. If you are testing out the module, be sure you have at least version 0.5.0.

  2. Pingback: ICYMI: PowerShell Week of 08-January-2021 | PowerShell.org
  3. Pingback: ICYMI: PowerShell Week of 08-January-2021 – 247 TECH

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d