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

Getting CIMInstance by Path

Posted on August 20, 2021August 20, 2021

I am a member of the PowerShell Cmdlet Working Group. We've been looking into this issue and it is an intriguing one. Enough so that I spent some time looking into it and writing up some test code. If you work with WMI/CIM this might be of interest to you. Personally, I never have had a need to approach WMI/CIM with this approach, but clearly, other IT Pros do.

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!

Get WMI by Path

Most people use the WMI/CIM cmdlets to get a bunch of things, using filtering to narrow down the selection. However, since the days of VBScript, you can also get a WMI object directly by referencing its path. This is a system property you can see using Get-WMIObject.

The format isn't too difficult to figure out: Computername\namespace:classname.filter. Think of this property as the address of the object in the CIM repository. Using it allows you to go directly to the object which has a performance benefit. It is not difficult to use traditional syntax like this:

Get-WmiObject -Class win32_service -filter "name='spooler'"

This takes 241ms on my laptop. However, I can also use the [wmi] type accelerator using the path.

[wmi]"root\cimv2:win32_service.Name='spooler'"

This expression only took 130ms to retrieve the exact object I got with filtering. If you already have the path to a WMI object you want to manage this certainly seems like a smart way to go.

CIM Limitations

Everything I just demonstrated works fine in Windows PowerShell using Get-WMIObject. However, this approach is considered deprecated and today we use the CIM cmdlets. These commands query the same CIM repository, except they do so using the WSMan protocol instead of RPC and DCOM. Which is a good thing. Unfortunately, Get-CimInstance doesn't return the __Path property.

This has been the case since PowerShell v3. Whether it is a bug or design decision is irrelevant to me. However, I have enough information to construct the path All I need is the key property for the class which I can get with Get-CimClass.

I then wrote a PowerShell function based on this idea.

Function Get-CimKeyProperty {
    [cmdletbinding()]
    [Outputtype("cimKeyProperty")]
    Param(
        [Parameter(Position = 0, Mandatory)]
        [string]$Classname,
        [ValidateNotNullOrEmpty()]
        [string]$Namespace = "root\cimv2",
        [ValidateNotNullorEmpty()]
        [string]$Computername = $env:Computername
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Starting $($myinvocation.mycommand)"
    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $namespace\$classname"
        Try {
            $cim = Get-CimClass @PSBoundParameters -ErrorAction stop
            [pscustomobject]@{
                PSTypename   = "cimKeyProperty"
                Namespace    = $Namespace
                Classname    = $cim.CimClassName
                Name         = $cim.cimclassproperties.where( { $_.qualifiers.name -contains 'key' }).Name
                Computername = $cim.CimSystemProperties.ServerName.toUpper()
            }
        }
        Catch {
            Throw $_
        }
    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"

    } #end

}

The function writes a rich object to the pipeline. It also takes into account the different types of paths, which I won't get into here.

With this information, I can create the __Path.

Function New-CimInstancePath {
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string]$Classname,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]]$Name,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]]$Value,
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Namespace = "root\cimv2",
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullorEmpty()]
        [string]$Computername = $env:Computername
    )

    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Starting $($myinvocation.mycommand)"
    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $Namespace.$Class"
        if ($Name -is [array]) {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using a compound selector"
            $selectors = @()
            for ($i = 0; $i -lt $name.count; $i++) {
                $selectors += "{0}=""{1}""" -f $name[$i], $value[$i]
            }
            "\\{0}\{1}:{2}.{3}" -f $Computername, $Namespace, $classname, ($selectors -join ",")
        }
        elseif ($Name -AND $Value) {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using $name property"
            "\\{0}\{1}:{2}.{3}='{4}'" -f $Computername, $Namespace, $classname, $Name, $Value
        }
        else {
            #singleton
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using singleton path"
            "\\{0}\{1}:{2}=@" -f $Computername, $Namespace, $classname
        }
    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"

    } #end
}

I wrote this function to work together with Get-CimKeyProperty.

The last step is to create a command that can update a CimInstance.

Function Add-CimInstancePath {
    [cmdletbinding()]
    Param(
        [Parameter(Position = 0, ValueFromPipeline)]
        [ciminstance]$Instance
    )

    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Starting $($myinvocation.mycommand)"
    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $($Instance.CimSystemProperties.ClassName)"
        $system = $Instance.CimSystemProperties
        $get = @{
            Classname    = $system.ClassName
            Namespace    = $system.Namespace.replace("/", "\")
            Computername = $system.ServerName
        }
        $kp = Get-CimKeyProperty @get
        if ($kp.Name) {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Defining a path using property $($kp.name)"
            $newpath = $kp | New-CimInstancePath -value $instance.CimInstanceProperties[$($kp.name)].value
        }
        else {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Defining a singleton path"
            $newpath = $kp | New-CimInstancePath
        }

        if ($newpath) {
            $Instance.CimSystemProperties |
            Add-Member -MemberType NoteProperty -Name __Path -Value $newpath -Force
            #write the updated instance to the pipeline
            $instance
        }
    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"
    } #end
}

This function is designed to take a CImInstance and add a __Path property to CimSystemproperties.

I can use this value with the [WMI] type accelerator.

Using Winrm

However, this doesn't really address the underlying issue. In a world without Get-WmiObject, what am I going to do with this path information? For now, PowerShell 7 still supports [wmi], even though Get-WmiObject is gone. This is fine for local queries. But if you are getting an instance on a remote server, I'm pretty sure [wmi] is using legacy protocols, which we want to avoid.

But here's the interesting part. I can use the command-line tool winrm.cmd to query WMI over WSMan, just as Get-CimInstance is doing and I can give it an instance path.

The catch is that the path syntax is slightly different from what we get with Get-WMIObject. In other words, I need to transform this: \\THINKP1\root\cimv2:Win32_Service.Name="Spooler" into this: wmi/root/cimv2/win32_service?name=spooler. The computername is handled separately.

I already have code to get the __Path value using Get-CimInstance. Assuming I'm saving this value for later use, I wrote this function which in essence transforms the WMI path into something compatible with the winrm command.

Function Get-CimInstancePath {
    #this function uses winrm.cmd. It may not properly process non-Cimv2 paths
    [cmdletbinding()]
    [alias("gcip")]
    [OutputType("CimInstance")]
    Param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$InstancePath
    )

    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN] Starting $($myinvocation.mycommand)"
        [regex]$rx = '((\\\\(?<computername>\w+)\\)?(?<namespace>(ROOT|root|Root)\\.*(?=:)):)?(?<class>\w+(?=\.|\=))(\.(?<filter>.*))?'
    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $instancepath"
        if ($rx.IsMatch($InstancePath)) {
            $groups = $rx.match($InstancePath).groups
            $computername = $groups['computername'].Value
            $filter = $groups['filter'].Value.replace(",", "+")
            $class = $groups['class'].Value

            $namespace = $groups['namespace'].value

            $ns = "wmi/$($namespace.replace('\','/'))"

            [string]$get = "$ns/$class"
            if ($filter) {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Appending filter $filter"
                $get += "?$filter"
            }
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Getting $get"
            #Write-Host "winrm get $get" -ForegroundColor cyan
            #winrm get $get.trim() >d:\temp\r.txt
            #$global:g = $get
            if ($computername) {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Querying remote computer $computername"
                <#
                        Normally I would never use Invoke-Expression, but PowerShell is doing
                        something odd with multi-selector paths like Win32_Bios and failing.
                        This seems the best option.
                    #>
                [xml]$raw = Invoke-Expression "winrm get $get -r:$Computername -f:xml 2>$env:temp\e.txt"
            }
            else {
                [xml]$raw = Invoke-Expression "winrm get $get -f:xml 2>$env:temp\e.txt"
            }

            if ($raw) {

                #insert the corresponding CIM type name
                $cimtype = "Microsoft.Management.Infrastructure.CimInstance#$($namespace.replace("\","/"))/$class"
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Inserting typename $cimtype"
                $result = $raw.$class
                $result.psobject.typenames.insert(0, $cimtype)
                #write the result to the pipeline
                $result
            }
            elseif (Test-Path $env:temp\e.txt) {
                $m = '(?<=f:Message\>)(.|\n)*(?=\n\<\/f:Message)'
                #'Message(.*\n.*)*'
                $msg = [System.Text.RegularExpressions.Regex]::Match((Get-Content $env:temp\e.txt -Raw), $m).value.trim()
                Write-Warning "Failed to get CIM Instance. `n$msg"
            }

            if (Test-Path $env:temp\e.txt) {
                # Remove-Item $env:temp\e.txt
            }
        }
        else {
            Write-Warning "Failed to parse instance $instancepath"
        }
    } #process

    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"
    } #end
}

This function not only takes the path and gets the information using winrm, it also creates an object and inserts the corresponding CIM property name, which lets PowerShell format the output just as it would using Get-CimInstance.

$p = (get-ciminstance win32_service -filter "name='bits'" | Add-CimInstancePath).cimsystemproperties.__Path
Get-CimInstancePath -InstancePath $p

Now I have a command that uses CIM and retrieves a WMI object by the instance path. There's no guarantee my code will work with all WMI classes, but I expect it should with the default win32 classes in Root\Cimv2.

Next Steps

I might publish these functions in a module to the PowerShell Gallery. As for PowerShell 7 and the open issue, I'm not sure what approach Microsoft should take. They could add a parameter to Get-CimInstance to get an instance by its path. Although they would also need to fix the missing system Path property. Another option would be to introduce a [cim] type accelerator that would work the same as [wmi]. The hypothetical [cim] would work the same as my Get-CimInstancePath.

In the meantime, I hope you'll try these things out. And if your work leverages getting WMI objects by path, I'd love to hear about it.


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

6 thoughts on “Getting CIMInstance by Path”

  1. Doug says:
    August 31, 2021 at 9:12 pm

    MS have deprecated the wmic tool as of 21H1.
    https://docs.microsoft.com/en-us/windows/deployment/planning/windows-10-deprecated-features

    1. Jeffery Hicks says:
      August 31, 2021 at 9:26 pm

      I’m not using wmic. I’m using the command-line tool winrm.cmd which is something completely different and isn’t going away any time soon.

  2. Doug says:
    August 31, 2021 at 9:39 pm

    Apologies, the comment was cut short.
    The intent was to point out that there is now no supported way to perform CIM queries apart from the PS CIM cmdlets. wmic is going away *as well* as Get-WMI…

    1. Jeffery Hicks says:
      September 1, 2021 at 7:56 am

      That’s right, which is why if you need to get a WMI instance by path, the CIM cmdlets have a limitation. Until, or if, Microsoft addresses this issue my workarounds will have to suffice. Although I have to think that getting an instance by path is an edge use case.

  3. GordonRem says:
    September 16, 2021 at 4:06 pm

    A lot of useful information as always, thank you and best regards!

  4. Pingback: 5 PowerShell Hacks – October Community Round-up - SquaredUp

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