Skipping WMI System Properties in PowerShell

One of my favorite techniques when using WMI in PowerShell is to pipe an object to Select-Object and select all properties. Try this:

get-wmiobject win32_bios | select *

It works, but it also gets all of the system properties like __PATH which I rarely care about. I also get other properties like Site and Options which I typically don’t need. So here are some techniques you can use to view all WMI properties that probably matter most. Let’s begin with a typical command:

$os=get-wmiobject Win32_OperatingSystem

This object has a property called Properties which is a collection of WMI PropertyData objects. I could try something like:

PS C:\> $ | select name,value

Name Value
---- -----
BootDevice \Device\HarddiskVolume1
BuildNumber 7601
BuildType Multiprocessor Free
Caption Microsoft Windows 7 Ultimate
CodeSet 1252
CountryCode 1

Sure, I can see the properties and values but I don’t really have a good object to work with and this won’t help with multiple instances, at least not without a little extra work.

$d=Get-WmiObject win32_logicaldisk -filter "drivetype=3"
$d | foreach {$ | select name,value }

And maybe that’s all you need. If so, terrific. But I’m looking for an elegant solution. How about expanding the property names and saving them as an array of strings.

[string[]]$prop=$ | Select -expand name
$os | Select -Property $prop

Then I can use them with Select-Object

PS C:\> $os | Select -Property $prop

PS C:\> $os | Select -Property $prop

BootDevice : \Device\HarddiskVolume1
BuildNumber : 7601
BuildType : Multiprocessor Free
Caption : Microsoft Windows 7 Ultimate
CodeSet : 1252
CountryCode : 1

That has promise. I could even turn this into a one-line command, assuming I’ve already defined $os.

$os | Select -property ([string[]]($ | Select -expand name))

If I didn’t want to take the extra step, here’s a complex alternative:

Get-WmiObject win32_OperatingSystem | foreach {
Select -InputObject $_ -Property ([string[]](Select -InputObject $_ -expand Properties | Select -expand Name))

But this leads to yet another option, using the [WMIClass] type accelerator. When used, PowerShell creates an empty object of the specified WMI class which includes a property list.

PS C:\> [wmiclass]"win32_operatingsystem" | Select -expand Properties | Select name


This can be further simplified:

([wmiclass]"win32_operatingsystem").Properties | Select -expand Name

In fact, I can use this in an earlier expression to get the same non-System class results.

get-wmiobject win32_operatingsystem | select -property ([string[]](([wmiclass]"Win32_Operatingsystem").properties | select -expand name))

I’ll admit that’s a lot to type. So I need a shortcut. What about turning this into a scriptblock?

$p={[string[]](([wmiclass]"Win32_Operatingsystem").properties | select -expand name)}

When I invoke the scriptblock, I’ll get the array of property names. I can now use this in my Get-WMIObject expression:

get-wmiobject win32_operatingsystem | select -property (&$p)

Definitely easier to type, but limited. I need the scriptblock to be more flexible so it can accommodate other WMI classes.

$p={Param([string]$class) [string[]](([wmiclass]$class).properties | select -expand name) }

I’ll test it in the shell.

PS C:\> &$p "win32_bios"

Excellent! Now for the real deal:

PS C:\> get-wmiobject win32_bios | select -property (&$p "win32_bios")

BiosCharacteristics : {4, 7, 8, 9...}
BIOSVersion : {TOSQCI - 6040000, Ver 1.00PARTTBL}
BuildNumber :
Caption : Ver 1.00PARTTBL
CodeSet :
CurrentLanguage :
Description : Ver 1.00PARTTBL

That’s exactly what I wanted with minimal effort. All I need is to have that scriptblock loaded in my PowerShell session and remember to specify the class name.

get-wmiobject win32_logicaldisk -filter "drivetype=3" | select -property (&$p "win32_logicaldisk")

Another option would be to turn the scriptblock into a function, but I’ll leave that to you.

Naturally this isn’t perfect. If I need to query a remote computer with classes that aren’t on my computer, this won’t work; at least not without some revisions. But since I’d say 90% or more of WMI commands in PowerShell are with the Win32 classes, I think this is a handy trick to stick in your PowerShell toolbox.

If you’d like to try some of my code samples, you can download demo-wmiproperties.

Friday Fun: 13 More Scriptblocks

In celebration of Friday the 13th I thought I would offer up a menu of 13 more script blocks. If you missed the first course, you can find the original 13 scrptblocks here. I’m not going to spend a lot of time going over these. Many of them are simple one liners. Some of them take parameters just like functions and scripts. The easiest way to execute any of the scriptblocks is to use the & operator. But these might also come in handy with any cmdlet that takes a scriptblock as a parameter value such as Invoke-Command.

I think of scriptblocks as “quick and dirty” blocks of re-usable code. If you find something very useful, you might expand it into a full-blown function complete with error handling and verbose output. Or you might find a handy technique in one of these examples.

#1 Get top problem source from the last 500 event log entries
$topprob={Param($log="System") Get-EventLog -LogName $log -newest 500 -entrytype Error |
Group Source -NoElement | Sort Count | Select -last 1}

#2 Get folder usage by owner in MB
$usage={Param($path=".") dir $path -recurse | Where {-Not $_.PSIsContainer} |
Select Fullname,Length,@{N='Owner';E={($_ | Get-ACL).Owner}} |
Group Owner | Sort Count -descending|
Select Count,Name,@{N='SizeMB';E={(($_.Group | Measure length -sum).sum)/1MB}}

#3 get empty event logs
$emptylog={get-eventlog -list | where {$_.entries.count -eq 0}}

#4 Get OS Install date
$install={Param($Computername=$env:computername) Get-WmiObject win32_operatingsystem -comp $computername |
select CSName,Caption, @{N="Install";E={$_.ConvertToDateTime($_.InstallDate)}}}

#5 Test if running Windows 8
(Get-WmiObject win32_operatingsystem -comp $computername).Caption -match "Windows 8"
#&$test8 MyWin8

#6 Test if running PowerShell v3
(test-wsman -ComputerName $computername).Productversion -match "Stack: 3.0"}
#&$testPS3 MyWin8

#7 Get code snippets from help examples
$excode={Param($command="get-service") (get-help $command).examples.example | select code}
#&$excode get-process

#8 Count by 13
$countup={Param($count=5) $x=0; For ($i=0;$i -lt $Count; $i++) {$x+=13;$x}}
#&$countup 13

#9 get a 13 character random password
$randpass={ Param($length=13) $chars=[char[]](33..126) ; -join ($chars | get-random -count $length)}

#10 Test if profile scripts exist
$profile | Select @{N="Type";E={"AllUsersAllHosts"}},@{N="Path";E={$_.AllUsersAllHosts}},@{N="Exists";E={Test-Path $_.AllUsersAllHosts}}
$profile | Select @{N="Type";E={"AllUsersCurrentHost"}},@{N="Path";E={$_.AllUsersCurrentHost}},@{N="Exists";E={Test-Path $_.AllUsersCurrentHost}}
$profile | Select @{N="Type";E={"CurrentUsersAllHosts"}},@{N="Path";E={$_.CurrentUserAllHosts}},@{N="Exists";E={Test-Path $_.CurrentUserAllHosts}}
$profile | Select @{N="Type";E={"CurrentUserCurrentHost"}},@{N="Path";E={$_.CurrentUserCurrentHost}},@{N="Exists";E={Test-Path $_.CurrentUserCurrentHost}}
#&$profileCheck | format-list

#11 get default printer
$defaultPrint={get-wmiobject win32_printer -filter "Default='True'"}

#12 get timezone
$tz={Param($computername=$env:computername) Get-WmiObject win32_timezone -computername $computername |
Select @{N="Computername";E={$_.__SERVER}},Description}

#13 find expired certificates
$expired={dir cert:\ -recurse | where {$_.NotAfter -AND $_.NotAfter -lt (Get-date)}}
#Invoke-command $expired -comp server01

Download the script file and watch out for black cats under ladders!

PowerShell Scripting with [ValidateScript]

The last few days we’ve been looking at parameter validation attributes you might use in a script of function. Yesterday I wrote about [ValidateRange] and demonstrated how you might use it. That attribute works fine for any values that can be evaluated as numbers. But dates are a different story. I got a comment with a suggestion for validating dates and at first glance it appears to work. For a very small range it might, but here’s the code I was testing with.

Param (
[Parameter(Position=0,Mandatory=$True,HelpMessage="Enter a date",ValueFromPipeline=$True)]

Process {
write-host $date -ForegroundColor Green

I’m expecting date between 1/1/2012 and 4/1/2012. Anything else should throw an exception. Now watch what happens as I pipe some values to this:

PS S:\> "2/12/2012","5/1/2012","12/1/2011","13/2/2012" | .\Demo-ValidateRange-Da
C:\scripts\Demo-ValidateRange-Date.ps1 : Cannot validate argument on parameter
'Date'. The 5/1/2012 argument is greater than the maximum allowed range of 4/1/
2012. Supply an argument that is less than 4/1/2012 and then try the command ag
At line:1 char:79
+ "2/12/2012","5/1/2012","12/1/2011","13/2/2012" | .\
1 <<<< + CategoryInfo : InvalidData: (5/1/2012:String) [Demo-ValidateRan ge-Date.ps1], ParameterBindingValidationException + FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateRa nge-Date.ps1 12/1/2011 13/2/2012

Values that are outside the range and even those that are bogus still are accepted. If I cast $date to [DateTime] which is what I would normally do, then nothing works at all. But that's fine because there is another attribute that is better suited to this task: [ValidateScript()].

To use this attribute we'll insert a scriptblock inside the parentheses. The scriptblock can be as complicated as you need it to be, but it must evaluate to either True or False. Use $_ to indicate the parameter value. So using my datetime example, I might use a validation script like this:

Param (
[Parameter(Position=0,Mandatory=$True,HelpMessage="Enter a date",ValueFromPipeline=$True)]
[ValidateScript( {
($_ -ge $start) -AND ($_ -le $end)

Process {
write-host $date -ForegroundColor Green

With a validation script I have much more flexibility. Now look at the results:

PS S:\> "2/12/2012","5/1/2012","3/15/2012","12/1/2011","13/2/2012" | .\Demo-Vali
dateScript-Date.ps1 | clip
2/12/2012 12:00:00 AM
C:\scripts\Demo-ValidateScript-Date.ps1 : Cannot validate argument on parameter
'Date'. The "
($_ -ge $start) -AND ($_ -le $end)
" validation script for the argument with value "5/1/2012 12:00:00 AM" did not
return true. Determine why the validation script failed and then try the comma
nd again.
At line:1 char:92
+ "2/12/2012","5/1/2012","3/15/2012","12/1/2011","13/2/2012" | .\Demo-ValidateS
cript-Date.ps1 <<<< | clip + CategoryInfo : InvalidData: (5/1/2012:String) [Demo-ValidateScr ipt-Date.ps1], ParameterBindingValidationException + FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateSc ript-Date.ps1 3/15/2012 12:00:00 AM C:\scripts\Demo-ValidateScript-Date.ps1 : Cannot validate argument on parameter 'Date'. The " [datetime]$start="1/1/2012" $end=Get-Date ($_ -ge $start) -AND ($_ -le $end) " validation script for the argument with value "12/1/2011 12:00:00 AM" did no t return true. Determine why the validation script failed and then try the comm and again. At line:1 char:92 + "2/12/2012","5/1/2012","3/15/2012","12/1/2011","13/2/2012" | .\Demo-ValidateS cript-Date.ps1 <<<< | clip + CategoryInfo : InvalidData: (12/1/2011:String) [Demo-ValidateSc ript-Date.ps1], ParameterBindingValidationException + FullyQualifiedErrorId : ParameterArgumentValidationError,Demo-ValidateSc ript-Date.ps1 C:\scripts\Demo-ValidateScript-Date.ps1 : The input object cannot be bound to a ny parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input. At line:1 char:92 + "2/12/2012","5/1/2012","3/15/2012","12/1/2011","13/2/2012" | .\Demo-ValidateS cript-Date.ps1 <<<< + CategoryInfo : InvalidArgument: (13/2/2012:String) [Demo-Valida teScript-Date.ps1], ParameterBindingException + FullyQualifiedErrorId : InputObjectNotBound,Demo-ValidateScript-Date.ps1

The valid dates pass, dates outside the range fail the validation test and the last value which isn't a legal date also fails but with a slightly different error message. As with all of the validation attributes I could have inserted this code into the body of my script and thrown my own errors. That choice is up to you. [ValidateScript()] isn't difficult to use. Just remember to insert your commands into a scriptblock, use $_ for the parameter value, and make sure the scriptblock writes either $True or $False.

Scripting with PSCredential

I see this question often: how can I pass a parameter value for a PSCredential that might be a credential object or it might be a user name? In the past I’ve used code like this:

begin {
Write-Verbose -Message "Starting $($myinvocation.mycommand)"
write-verbose -Message "Using volume $($volume.toUpper())"
#convert credential to a PSCredential if a string was passed.
if ( $credential -is []) {
Write-Verbose "Using PSCredential for $($credential.username)"
ElseIf ($Credential) {
Write-Verbose "Getting PSCredential for $credential"
$Credential=Get-Credential $credential
} #Begin

This assumes $Credential is a parameter. But then I realized, why not take advantage of parameter validation? I could use the [ValidateScript()] parameter attribute and insert some code to test the incoming value. If it is already a PSCredential, don’t do anything. But if it is a string, call Get-Credential and use the result.

Param (
if ($_ -is [System.Management.Automation.PSCredential]) {
elseif ($_ -is [string]) {
$Script:Credential=Get-Credential -Credential $_
else {
Write-Error "You passed an unexpected object type for the credential."

When using ValidateScript your code has to return True or False. Or you can also Write and error if you want to customize the exception message a bit. That’s what I’ve done here. With this code I can either use -Credential with a value like jdhitsolutions\administrator or a saved PSCredential object. Let me show you a simple script with this in action, plus I’ll address another common question about using credentials with WMI-based scripts and functions.

#requires -version 2.0

<# This function demonstrates how you might pass a credential object as a parameter #>

Function Get-OSName {

Param (
if ($_ -is [System.Management.Automation.PSCredential]) {
elseif ($_ -is [string]) {
$Script:Credential=Get-Credential -Credential $_
else {
Write-Error "You passed an unexpected object type for the credential."


#Never write the same line of code more than once if you can avoid it
$wmiCommand="Get-WmiObject -Class Win32_Operatingsystem -Property Caption,CSName -ComputerName $Computername"
Write-Verbose $wmiCommand

if ($Credential) {
#add the credential to the command string
Write-Verbose "Adding credential"
#escape the $ sign so that the command uses the variable name
$wmiCommand+=" -credential $Script:Credential"

Write-Verbose "Creating a scriptblock from the command string"

Try {
Write-Verbose "Invoking the command"
Invoke-Command -ScriptBlock $sb -errorAction Stop |
Select @{Name="Computername";Expression={$_.CSName}},
Catch {
Write-Warning $_.Exception.Message
Finally {
Write-Verbose "Finished"

} #close function

So the challenge is if I have a credential I need to use a Get-Wmiobject expression that uses it, otherwise run an expression without it. I'm a big believer in avoiding writing the same line of code more than once so I'll create a command string with my basic WMI command.

$wmiCommand="Get-WmiObject -Class Win32_Operatingsystem -Property Caption,CSName -ComputerName $Computername"

In this example the value for $Computername will be expanded and inserted into the string. If no credential is passed then this is the command I'll run. But if a credential is passed, then all I need to do is append it to my command string.

$wmiCommand+=" -credential

You must pay attention to a very subtle detail: I am escaping the $ sign in the variable name. I do not want PowerShell to expand the variable. I want the command string to use the variable as variable. That is, if using a credential I need the command to be: Get-WmiObject -Class Win32_Operatingsystem -Property Caption,CSName -ComputerName SERVER01 -credential $Script:Credential”

The last step is to turn this command string into a script block so it can be executed.


Armed with a scriptblock I can use Invoke-Command.

Invoke-Command -ScriptBlock $sb -errorAction Stop |
Select @{Name="Computername";Expression={$_.CSName}},

The end result is a function that I can run with no credentials. If I use a credential value like jdhitsolutions\administrator, I’ll get prompted for the password from Get-Credential. Or if I pass a saved credential, the function will use it.

These techniques are by no means the only solution but I find them simple to follow and effective.

Re-Use PowerShell Scriptblocks

//Re-Use PowerShell Scriptblocks//

I commented on a blog post today that showed how to use a hash table with Select-Object to format file sizes say as KB.

dir $env:temp -rec | select fullname,@{Name="KB";Expression={$_.length/1kb}}

Since you most likely will want to use something similar for other directories, don't feel you have to re-invent the wheel. Save the hashtable as a scriptblock. Here are some examples:

[email protected]{Name="SizeKB";Expression={$_.length/1kb}}
[email protected]{Name="SizeMB";Expression={$_.length/1mb}}
[email protected]{Name="SizeGB";Expression={$_.length/1gb}}

Now, you can get formatted sizes

dir ~documents -recurse | select fullname,$mb
dir e:vboxdisks -recurse | select name,$gb | format-table -auto

Or if you want to format the number, just update the hash table:

[email protected]{Name="SizeMB";Expression={"{0:F2}"-f ($_.length/1mb)}}

If you define these ahead of time, you can reuse them as needed depending on how you want to format the data.

What other custom scriptblock suggestions do you have?