Learn about Windows PowerShell
Summary: In today’s blog, we have an excerpt from an upcoming book by Don Jones, Richard Siddaway, and Jeffery Hicks.
Microsoft Scripting Guy, Ed Wilson, is here. Today and tomorrow we have a special treat for you. Candace Gillhoolley, from Manning Publications, sent me an excerpt from the upcoming book, PowerShell in Depth, which is written by Don Jones, Richard Siddaway, and Jeffery Hicks.
Here is Part 1...
There’s definitely a trick to creating reports with Windows PowerShell. Windows PowerShell isn’t at its best when it’s forced to work with text; objects are where it excels. This blog, based on Chapter 33 in PowerShell in Depth, focuses on a technique that can produce a nicely formatted HTML report, suitable for emailing to a boss or colleague.
Let us begin this blog with an example of what we think is a poor report-generating technique. We see code like this—sadly, more often than we would like. Most of the time, the IT Pro does not know any better and is simply perpetuating techniques from other languages such as VBScript. List 1, which we devoutly hope you will never run yourself, is a very common approach that you will see less-informed administrators take.
List 1: A poorly designed inventory report
param ($computername)
Write-Host '------- COMPUTER INFORMATION -------'
Write-Host "Computer Name: $computername"
$os = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $computername
Write-Host " OS Version: $($os.version)"
Write-Host " OS Build: $($os.buildnumber)"
Write-Host " Service Pack: $($os.servicepackmajorversion)"
$cs = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $computername
Write-Host " RAM: $($cs.totalphysicalmemory)"
Write-Host " Manufacturer: $($cs.manufacturer)"
Write-Host " Model: $($cd.model)"
Write-Host " Processors: $($cs.numberofprocessors)"
$bios = Get-WmiObject -Class Win32_BIOS -ComputerName $computername
Write-Host "BIOS Serial: $($bios.serialnumber)"
Write-Host ''
Write-Host '------- DISK INFORMATION -------'
Get-WmiObject -Class Win32_LogicalDisk -Comp $computername -Filt 'drivetype=3' |
Select-Object @{n='Drive';e={$_.DeviceID}},
@{n='Size(GB)';e={$_.Size / 1GB -as [int]}},
@{n='FreeSpace(GB)';e={$_.freespace / 1GB -as [int]}} |
Format-Table -AutoSize
This produces a text-based inventory report something like the one shown here:
It does the job, we suppose, but Don has a saying that involves angry deities and puppies, which he utters whenever he sees a script that outputs pure text like this. First of all, this script can only produce output on the screen because it’s using Write-Host. In most cases, if you find yourself using only Write-Host, you are probably doing it wrong. Wouldn’t it be nice to have the option of putting this information into a file or creating an HTML page? Of course, you could achieve that by changing all of the Write-Host commands to Write-Output, but you still wouldn’t be doing things the right way.
There are a lot of better ways that you could produce such a report and that’s what this blog is all about. First, we would suggest building a function for each block of output that you want to produce, and having that function produce a single object that contains all of the information you need. The more you can modularize, the more you can reuse those blocks of code. Doing so would make that data available for other purposes, not only for your report. In our example of a poorly written report, the first section, Computer Information, would be implemented by some function that you would write. The Disk Information section is only sharing information from one source, so it’s actually not that bad off, but all of those Write commands simply have to go.
The trick to our technique lies in the fact that the ConvertTo-HTML cmdlet in Windows PowerShell can be used in two ways, which you’ll see if you examine its Help file. The first way produces a complete HTML page, and the second only produces an HTML fragment. That fragment is a table with whatever data you have fed the cmdlet. We’re going to produce each section of our report as a fragment, and then use the cmdlet to produce a complete HTML page that contains all of those fragments.
We will start by ensuring that we can get whatever data we need formed into an object. We will need one kind of object for each section of our report, so if we’re sticking with Computer Information and Disk Information, that’s two objects.
Note For brevity and clarity, we are going to omit error handling and other niceties in this example. We would add those in a real-world environment.
Get-WmiObject by itself is capable of producing a single object that has all of the disk information we want, so we simply need to create a function to assemble the computer information. Here it is:
function Get-CSInfo {
param($computername)
$os = Get-WmiObject -Class Win32_OperatingSystem `
-ComputerName $computername
$cs = Get-WmiObject -Class Win32_ComputerSystem `
$bios = Get-WmiObject -Class Win32_BIOS `
$props = @{'ComputerName'=$computername
'OS Version'=$os.version
'OS Build'=$os.buildnumber
'Service Pack'=$os.sevicepackmajorversion
'RAM'=$cs.totalphysicalmemory
'Processors'=$cs.numberofprocessors
'BIOS Serial'=$bios.serialnumber}
$obj = New-Object -TypeName PSObject -Property $props
Write-Output $obj
}
The function uses the Get-WMIObject cmdlet to retrieve information from three WMI classes on the specified computer. We always want to write objects to the pipeline, so we’re using New-Object to write a custom object to the pipeline by using a hash table of properties culled from the three WMI classes. Normally, we prefer property names to not have any spaces, but, because we’re going to be using this in a larger reporting context, we’ll bend the rules a bit.
Now we can use our newly-created Get-CSInfo function to create an HTML fragment.
$frag1 = Get-CSInfo –computername SERVER2 |
ConvertTo-Html -As LIST -Fragment -PreContent '<h2>Computer Info</h2>' |
Out-String
This little trick took us a while to figure out, so it is worth examining.
The whole thing—and this was the tricky part—is piped to Out-String. You see, ConvertTo-HTML puts a bunch of things into the pipeline—strings, collections of strings, all kinds of wacky stuff. All of that will cause problems later when we try to assemble the final HTML page, so we’re using Out-String to resolve everything into plain old strings.
We can also produce the second fragment now. This is a bit easier because we don’t need to write our function first, but the HTML part will look substantially the same. In fact, the only real difference is that we are assembling our data in a table rather than as a list.
$frag2 = Get-WmiObject -Class Win32_LogicalDisk -Filter 'DriveType=3' `
-ComputerName SERVER2 |
Select-Object @{name='Drive';expression={$_.DeviceID}},
@{name='Size(GB)';expresssion={$_.Size / 1GB -as [int]}},
@{name='FreeSpace(GB)';expression={
$_.freespace / 1GB -as [int]}} |
ConvertTo-Html -Fragment -PreContent '<h2>Disk Info</h2>' |
We now have two HTML fragments, $frag1 and $frag2, so we are ready to assemble the final page. Join us tomorrow to see how this is accomplished.
~Don, Richard, and Jeffery
Thank-you Candace, Don, Richard, and Jeffery.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy
Good blog.
@Ashish Mahajan thank you for your comment. Today's posting was great; I am glad that you enjoyed it.
I'm getting the following, any suggestions?
Select-Object : Illegal key expresssion
At line:3 char:10
+ Select-Object @{name='Drive';expression={$_.DeviceID}},
NMayberry, you're welcome to post on http://bit.ly/AskDon - that's where my co-authors and I typically handle questions. However, I'm not seeing anything wrong with that syntax as-is, at least not when I'm running it in my Windows 8 virtual machine. It should work fine in PowerShell v2 as well. The entire command, copied from here and pasted right into the shell, ran fine for me.
There's a typo here:
Select-Object @{name='Drive';expression={$_.DeviceID}}
There is an extra 's' in 'expression'
Ah, good catch Jeff. I've got a little auto-corrector running that must have caught that when I pasted it. I ran it in my other VM and got the error.
We'll have to make sure we fix that in the chapter - thanks, NMayberry, for being a tech editor!!
Great blog. Cant wait to get the book.
Abe chutiye, laved kabhi tere baap ne script banaya tha