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.
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.
MS have deprecated the wmic tool as of 21H1.
https://docs.microsoft.com/en-us/windows/deployment/planning/windows-10-deprecated-features
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.
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…
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.
A lot of useful information as always, thank you and best regards!