Working with HTML Fragments and Files

Doctor Scripto

Summary: Windows PowerShell MVPs, Don Jones, Richard Siddaway, and Jeffrey Hicks share excerpts from their new book.

Microsoft Scripting Guy, Ed Wilson, is here. This week we will not have our usual PowerTip. Instead we have excerpts from seven books from Manning Press. In addition, each blog will have a special code for 50% off the book being excerpted that day. Remember that the code is valid only for the day the excerpt is posted. The coupon code is also valid for a second book from the Manning collection.

Today, the excerpt is from PowerShell in Depth
     By Don Jones, Richard Siddaway, and Jeffery Hicks

Photo of book cover

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 from PowerShell in Depth, focuses on a technique that can produce a nicely formatted HTML report, suitable for emailing to a boss or colleague.

Let’s 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 doesn’t know any better, and is simply perpetuating techniques from other languages, such as VBScript. Listing 1, which we devoutly hope you will never run, is a very common approach that you’ll see less informed administrators use.

Listing 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 report something like the one shown here.

Image of command output

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 ever produce output on the screen because it’s using Write-Host. In most cases, if you find yourself using only Write-Host, you’re 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 just 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’d 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 your report.

In our example of a poorly written report, the first section, Computer Information, would be implemented by some function you’d write. The Disk Information section is only sharing information from one source, so it’s actually not that bad—but all of those Write commands just have to go.

The trick to our technique lays in the fact that Windows PowerShell’s ConvertTo-HTML cmdlet 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 produces only an HTML fragment. That fragment is a table with whatever data you’ve 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.

Getting the information

We’ll start by ensuring that we can get whatever data we need formed into an object. We’ll need one type 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’re 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 `

  -ComputerName $computername

 

  $bios = Get-WmiObject -Class Win32_BIOS `

  -ComputerName $computername

 

  $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, and using a hash table of properties culled from the three WMI classes. Normally, we prefer that property names do 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.

Producing an HTML fragment

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’s worth examining.

  1. We’re saving the final HTML fragment into a variable named $frag1. That’ll let us capture the HTML content and later insert it into the final file.
  2. We’re running Get-CSInfo and giving it the computer name we want to inventory. For right now, we’re hardcoding the SERVER2 computer name. We’ll change that to a parameter a bit later.
  3. We’re asking ConvertTo-HTML to display this information in a vertical list, rather than in a horizontal table (which is what it would do by default). The list will mimic the layout from the old “bad way of doing things” report.
  4. We used the PreContent switch to add a heading to this section of the report. We added the <h2> HTML tags so that the heading will stand out a bit.
  5. The whole thing—and this was the tricky part—is piped to Out-String. You see, ConvertTo-HTML puts strings, collections of strings…all kinds of wacky stuff into the pipeline. All of that will cause problems later when we try to assemble the final HTML page. So we’re getting Out-String to resolve everything into plain old strings.

We can also produce the second fragment. This is a bit easier because we don’t need to write our own function first, but the HTML part will look substantially the same. In fact, the only real difference is that we’re letting our data be assembled into 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>’ |

Out-String

We now have two HTML fragments, $frag1 and $frag2, so we’re ready to assemble the final page.

Assembling the final HTML page

Assembling the final page simply involves adding our two existing fragments—although, we’re also going to embed a style sheet. Using cascading style sheet (CSS) language is a bit beyond the scope of this blog, but this example will give you a basic idea of what it can do. This embedded style sheet lets us control the formatting of the HTML page, so that it looks a little nicer. If you’d like a good tutorial and reference for CSS, check out CSS Tutorial at w3schools.com.

$head = @’

<style>

body { background-color:#dddddd;

       font-family:Tahoma;

       font-size:12pt; }

td, th { border:1px solid black;

         border-collapse:collapse; }

th { color:white;

     background-color:black; }

table, tr, td, th { padding: 2px; margin: 0px }

table { margin-left:50px; }

</style>

‘@

 

ConvertTo-HTML -head $head -PostContent $frag1,$frag2 `

-PreContent “<h1>Hardware Inventory for SERVER2</h1>”

We’ve put that style sheet into the variable $head, using a Here-String to type the entire CSS syntax we wanted. That gets passed to the Head parameter, our HTML fragments to the PostContent parameter, and we couldn’t resist adding a header for the whole page, where we’ve again hardcoded a computer name (SERVER2).

We saved the entire script as C:\Good.ps1, and ran it like this:

./good > Report.htm

That directs the output HTML to Report.htm, which is incredibly beautiful and shown here.

Image of command output

Okay, maybe it’s no work of art, but it’s highly functional; and frankly, it looks better than the on-screen-only report we started with in this blog. Listing 2 shows the completed script, where we’ve swapped out the hardcoded computer name for a script-wide parameter that defaults to the local host. Notice that we’ve also included the [CmdletBinding()] declaration at the top of the script, enabling the Verbose parameter. We’ve used Write-Verbose to document what each step of the script is doing.

Listing 2: An HTML inventory report script

<#

.DESCRIPTION

Retrieves inventory information and produces HTML

.EXAMPLE

./Good > Report.htm

.PARAMETER

The name of a computer to query. The default is the local computer.

#>

 

[CmdletBinding()]

param([string]$computername=$env:computername)

 

# function to get computer system info

function Get-CSInfo {

  param($computername)

  $os = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $computername

  $cs = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $computername

  $bios = Get-WmiObject -Class Win32_BIOS -ComputerName $computername

  $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

}

 

Write-Verbose ‘Producing computer system info fragment’

$frag1 = Get-CSInfo -computername $computername |

ConvertTo-Html -As LIST -Fragment -PreContent ‘<h2>Computer Info</h2>’ |

Out-String

 

Write-Verbose ‘Producing disk info fragment’

$frag2 = Get-WmiObject -Class Win32_LogicalDisk -Filter ‘DriveType=3’ `

         -ComputerName $computername |

Select-Object @{name=’Drive’;expression={$_.DeviceID}},

              @{name=’Size(GB)’;expression={$_.Size / 1GB -as [int]}},

        @{name=’FreeSpace(GB)’;expression={$_.freespace / 1GB -as [int]}} |

ConvertTo-Html -Fragment -PreContent ‘<h2>Disk Info</h2>’ |

Out-String

 

Write-Verbose ‘Defining CSS’

$head = @’

<style>

body { background-color:#dddddd;

       font-family:Tahoma;

       font-size:12pt; }

td, th { border:1px solid black;

         border-collapse:collapse; }

th { color:white;

     background-color:black; }

table, tr, td, th { padding: 2px; margin: 0px }

table { margin-left:50px; }

</style>

‘@

 

Write-Verbose ‘Producing final HTML’

Write-Verbose ‘Pipe this output to a file to save it’

ConvertTo-HTML -head $head -PostContent $frag1,$frag2 `

-PreContent “<h1>Hardware Inventory for $ComputerName</h1>”

 

Now that’s a script you can build upon! And the script is very easy to use.

PS C:\> $computer = SERVER01

PS C:\> C:\Scripts\good.ps1 -computername $computer |

>> Out-File “$computer.html”

>> 

PS C:\> Invoke-Item “$computer.html”

The script runs, produces an output file for future reference, and displays the report. Keep in mind that our work in building the Get-CSInfo function is reusable. Because that function outputs an object and not only pure text, you could repurpose it in a variety of places where you might need the same information.

To add to this report, you’d simply:

  1. Write a command or function that generates a single object that contains all the information you need for a new report section.
  2. Use that object to produce an HTML fragment, and store it in a variable.
  3. Add that new variable to the list of variables in the script’s last command, thus adding the new HTML fragment to the final report.
  4. Sit back and relax.

Yes, this report is text. Ultimately, every report will be, because text is what we humans read. The point of this one is that everything stays as Windows PowerShell-friendly objects until the last possible instance. We let Windows PowerShell, rather than our own fingers, format everything for us. The actual working bits of this script, which retrieve the information we need, could easily be copied and pasted and used elsewhere for other purposes. That wasn’t as easy to do with our original pure-text report, because the actual working code was so embedded with all of that formatted text.

Building reports is certainly a common need for administrators, and Windows PowerShell is well suited to the task. The trick, we feel, is to produce reports in a way that makes the reports’ functional code (the bits that retrieve information and so forth) somewhat distinct from the formatting- and output-creation code. In fact, Windows PowerShell is generally capable of delivering great formatting with very little work on your part, as long as you work the way it needs you to.

Here is the code for the discount offer today at www.manning.com: scriptw1
Valid for 50% off PowerShell in Depth and SQL Server DMVs in Action
Offer valid from April 1, 2013 12:01 AM until April 2 midnight (EST)

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 

0 comments

Discussion is closed.

Feedback usabilla icon