This is a reprint of an article published earlier this year in my premium PowerShell newsletter, Behind the PowerShell Pipeline. This is a sample of what my subscribers get 6-8 times a month.
I expect I will write several articles about PowerShell and its relationship with objects. I know that this is the biggest hurdle for PowerShell beginners to overcome. But once they grasp that PowerShell is about working with objects in the pipeline, they recognize the value and begin finding it easier to write PowerShell code and use it interactively at a console prompt.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
In a Console A Long, Long Time Ago
Don't forget that I am approaching PowerShell from the role of an IT pro. Many of these people come to PowerShell with other automation and scripting skills. It might be old-fashioned batch files. It might be extensive experience with VBScript. That was my PowerShell journey. Like many of you, my initial PowerShell experiences were colored by my past. I kept trying to make PowerShell write text as I did with VBScript. Here's an old example.
Dim objSet, wshell
On Error Resume Next
Set wshell=CreateObject("Wscript.Shell")
If wscript.arguments.count=0 Then
strSrv=InputBox("What server do you want to check? You must have admin rights on it. Do NOT use \\ " & _
"before the servername.","Disk Check","SERVERNAME")
If strSrv="" Then
wshell.popup "Nothing entered or you cancelled",4,"Disk Check",0+48
wscript.quit
End If
Else
strSrv=Trim(wscript.arguments(0))
End If
strQuery = "Select * from win32_logicaldisk where drivetype=3"
Set objSet=GetObject("winmgmts:\\" & strSrv).ExecQuery(strQuery)
if err.number<>0 then
wshell.popup "Oops! Error connecting to " & UCase(strSrv) & vbCrlf & "make sure you are using valid " & _
"credentials." & vbCrlf & "Error: " & err.number & " - " & err.description,5,"Disk Check Error",0+48
wscript.quit
end if
For Each item In objSet
PerFree=FormatPercent(item.FreeSpace/item.Size,2)
o=o & item.DeviceID & "\" & VBTAB
o=o & FormatNumber(item.Size/1048576,0) & Vbtab & FormatNumber(item.FreeSpace/1048576,0) & Vbtab & PerFree & Vbcrlf
next
WScript.Echo "Drive" & Vbtab & "Size (MB) Free (MB) %Free" & VbCrLf & o
set objSet=Nothing
set wshell=Nothing
wscript.quit
Don't panic. I'm not going to try and teach you VBScript. The script is getting disk information from WMI and displaying a summary. But to get the desired result, I spend a lot of time creating strings of text to display. In the For Each item
section, I am building a string for each drive showing the size and free space. Each drive gets concatenated to the variable o
. At the end of the script, I write the string of information to the screen.
WScript.Echo "Drive" & Vbtab & "Size (MB) Free (MB) %Free" & VbCrLf & o
Here's what I end up with.
Many people new to PowerShell are trying to force it to behave like this. They are used to a scripting language returning a string of text. This is especially true of people with a background in Linux bash scripting. Because Linux is a text-based operating system, its tools are based on the concept of parsing text. Someone new to PowerShell and still stuck in the text paradigm might write a PowerShell script like this.
$strSrv = Read-Host "Enter a computername"
$strQuery = "Select * from win32_logicaldisk where drivetype=3"
$objSet = Get-WmiObject -Query $strQuery -ComputerName $strSrv
Write-Host "Drive`t Size (MB) Free (MB) %Free"
foreach ($item in $objSet) {
$perFree = ($item.FreeSpace/$item.size)*100
$line = $item.DeviceID + "\ `t" + $item.Size/1048576 + " " + $item.FreeSpace/1048576 + " " + $perFree
Write-Host $line
}
Sadly, I have seen code like this in the real world. It will work. Sort of.
But this is a lot of work. One reason people get hung up on text is that running a command in PowerShell almost always produces easy-to-read text output. Think of the result you get from Get-Service
and Get-Process.
Objects Are Easy
The idea of an object shouldn't be that difficult. We deal with objects in the real world every day. The challenge is getting our heads wrapped around the concept of virtual objects. If you take the time to look at PowerShell, you'll see it is trying to help you. The command names imply objects. Get-Service
is going to get service objects. Here's a situation where using the full cmdlet name instead of the gsv alias can help a beginner. You don't want to learn PowerShell thinking, "When I run the gsv command, I get a list of services." You want to be thinking, "When I run the Get-Service command, I am getting a collection of service objects that I can pass on to another command."
Let's revisit the WMI code. The code says, "Get me a WMI object that is a Win32_Logicialdisk that meets specific criteria on the specified remote computer." Once those objects have been returned, the code tells PowerShell what object properties to display. The results are displayed, and PowerShell automatically handles all of the formatting.
$computername = Read-Host "Enter a computername"
$Query = "Select * from win32_logicaldisk where drivetype=3"
Get-WmiObject -Query $Query -ComputerName $computername |
Select-Object -property DeviceID,Size,Freespace,
@{Name="PercentFree";Expression = {$_.freespace/$_.size}}
One of the best features of PowerShell is that you can also define new object properties. The WMI win32_logicaldisk object doesn't have a PercentFree property. But Select-Object
can do that for me with the custom hashtable.
Once you start thinking about objects and not parsing text, you'll realize there is an entire world of possibilities. I tell beginning PowerShell scripters to say aloud what they want PowerShell to do.
Get all fixed logical disks from a remote computer. Select the computer name, drive letter, size in GB, free disk space, and percent free space and export to a CSV file.
Once you articulate what you want, you can find the PowerShell commands and parameters to make it happen.
Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ComputerName $computername |Select-Object -property DeviceID,@{Name="SizeGB";Expression = {$_.size/1GB -as [int32]}},Freespace,@{Name="PercentFree";Expression = {($_.freespace/$_.size)*100}},SystemName |Export-CSV c:\work\diskinfo.csv -Append
The biggest hurdle for beginners is discovering object properties with Get-Member
and the tricks to format values, such as using the 1GB shortcut and the -As
operator. I have found that the people who can visualize the concept of objects in the pipeline adopt PowerShell quicker and with less pain.
That's not to say we still don't need to mess with text or values. The percent free value I defined above is a good example. You might be inclined to use the -f
operator.
@{Name="PercentFree";Expression = {"{0:p2}" -f ($_.freespace/$_.size)}}
It sure looks nice.
If all I want to do is look at this result, I might be ok. But even here, we're back to text. If I pipe this output to Get-Member
I'll see that the PercentFree property is a System.String
. The problem arises when I want to do something with the value, such as sorting. The value will sort as a string, not a number, and I might not get the expected results.
I find a better approach is to treat the value as a [System.Double]
object using the [math] class to round the value to 2 decimal places.
@{Name="PercentFree";Expression = {[math]::round(($_.freespace/$_.size)*100,2)}}
With this, I get the formatted percentage I want and a value I can use.
I hope you can see the value here.
Functions Write Objects
The last part of the story I want to discuss briefly revolves around PowerShell functions. I have very strong opinions on how to write a PowerShell function properly and how it should behave. This is often at odds with people who come to PowerShell with a developer background or maybe used to writing VBScript subroutines.
While I recognize there are exceptions, a PowerShell function should write a series of objects, or maybe a single object, to the pipeline. It is not returning anything that looks like a string. Technically, there is nothing wrong with this simple PowerShell function.
Function Get-PercentFree {
Param($Drive = "C:", $Computername = $env:COMPUTERNAME)
$d = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$Drive'" -ComputerName $computername
$p = [math]::round(($d.freespace / $d.size) * 100, 2)
return $p
}
But it is of limited value. All I'm going to get is a result like 40.96. I would have written this type of code as a VBScript subroutine. But we're talking about PowerShell. I want an object in the pipeline.
Function Get-LogicalDisk {
[cmdletbinding()]
Param($Computername = $env:COMPUTERNAME)
$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ComputerName $computername
$disks | Select-Object @{Name = "Computername"; Expression = { $_.SystemName } },
DeviceID, Size, FreeSpace, @{Name = "PercentFree"; Expression = { [math]::round(($_.freespace / $_.size) * 100, 2) } },
@{Name = "Audit"; Expression = { Get-Date } }
}
The function is written to make a point, not necessarily something ready for production. Running the function gives this kind of output.
If all I need is percent free, that is easy enough to retrieve.
The function is flexible and re-usable because it writes a rich object to the pipeline.
If you noticed in the function, I also did not format the size and free space values. I left them in bytes. Getting or creating an object in PowerShell is separate from how it is formatted or presented. But that's a topic for another day.
If I need strings, say for a log file, I can still create them from objects.
$data = "prospero","thinkx1-jh","dom1","srv2" |
Foreach-Object { Get-LogicalDisk -Computername $_ }
foreach ($item in $data) {
$line = "[{0:d}] Drive {1} on {2} is {3}% free" -f $item.Audit,$item.DeviceID,$item.computername,$item.percentfree
$line
#$line | Out-File log.txt -append
}
Note that because the Audit property is a datetime object, I can format it using the d
qualifier.
Trying to accomplish this with a text-based paradigm is much more complicated and frustrating.
The more you think about the object you can get from PowerShell and how you can use it, the more you can accomplish, and most likely with better PowerShell code.
Thank you. Coming from VBScript to Powershell I’ve done exactly this. However, I’ll be working hard from now on to follow your guidance on working with objects, rather than trying to leverage everything into strings! Very well written.