Sunteți pe pagina 1din 42

http://PowerShellBooks.com http://PowerShell.

org
Creating
HTML Reports
in PowerShell
by Don Jones
Copyright 2012 by Don Jones
Licensed under Creative Commons Attribution-NoDerivs 3.0 Unported
Creating HTML Reports in PowerShell
1
http://PowerShellBooks.com http://PowerShell.org
Foreword
I set out to create this guide because, as more and more people jump into PowerShell, they seem to be jumping into
reporting. And, unfortunately, theyre making a lot of beginner mistakes that end up making the job tougher, and the
results less attractive. Its quite understandable, though: creating HTML reports requires an unholy blend of skills.
You have to be an administrator to retrieve the information, a Web designer to make it look good, and quite frankly a
hacker to get everything you want into the right place. So with that in mind, I set out to create a book thatll help a
bit.
Since Im not making any money from this book, I do hope youll take a moment to consider the other books Ive
written and co-authored, all of which can be found at http://PowerShellBooks.com. Your purchases there help pay
the mortgage and the utility bills, and that gives me the time to work on projects like this one.
If you have something youd like to add to this book, or you find an error, or you just need additional help, please
drop a message into the PowerShell Q&A forum at http://PowerShell.org. That site also features an impressive
array of other free resources related to PowerShell, and I think youll enjoy the time you spend there.
Don Jones
Copyright This guide is released under the Creative Commons Attribution-NoDerivs 3.0 Unported License.
The authors encourage you to redistribute this file as widely as possible, but ask that you do not modify the
document. However, you are encouraged to submit changes to the authors directly (http://
ConcentratedTech.com/contact) so that your suggestions can be incorporated into the master document and
published in a future revision.
http://PowerShellBooks.com http://PowerShell.org
Contents
.................................................................................................................. HTML Report Basics 4
.......................................................................................................... Gathering the Information 6
.................................................................................................................... Building the HTML 9
................................................................................ An Aside: The ConvertTo-HTML Cmdlet 15
........................................................................................................... Beautification of HTML 16
.............................................................................................................. Making Tables Prettier 22
...................................................................................................................... The Final Product 25
.................................................................................................... Creating an Awesome Report 32
......................................................................................................................... How to Do it 33
............................................................................................ Formatting Individual Table Cells 40
.................................................................. Combining HTML Reports and a GUI Application 42
........................................................................................................................... Contacting Me 45
Creating HTML Reports in PowerShell
3
http://PowerShellBooks.com http://PowerShell.org
HTML Report Basics
First, understand that PowerShell isnt limited to creating reports in HTML. But I like HTML because its flexible,
can be easily e-mailed, and can be more easily made to look pretty than a plain-text report. But before you dive in,
you do need to know a bit about how HTML works.
An HTML page is just a plain text file, looking something like this:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/
xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>HTML TABLE</title>
</head><body>
<table>
<colgroup><col/><col/><col/><col/><col/></colgroup>
<tr><th>ComputerName</th><th>Drive</th><th>Free(GB)</th><th>Free(%)</th><th>Size(GB)</th></tr>
<tr><td>CLIENT</td><td>C:</td><td>49</td><td>82</td><td>60</td></tr>
</table>
</body></html>
When read by a browser, this file is rendered into the display you see within the browsers window. The same
applies to e-mail clients capable of displaying HTML content. While you, as a person, can obviously put anything
you want into the file, if you want the output to look right you need to follow the rules that browsers expect.
One of those rules is that each file should contain one, and only one, HTML document. Thats all of the content
between the <HTML> tag and the </HTML> tag (tag names arent case-sensitive, and its common to see them in
all-lowercase as in the example above). I mention this because one of the most common things Ill see folks do in
PowerShell looks something like this:
Get-WmiObject -class Win32_OperatingSystem | ConvertToHTML | Out-File report.html
Get-WmiObject -class Win32_BIOS | ConvertTo-HTML | Out-File report.html -append
Get-WmiObject -class Win32_Service | ConvertTo-HTML | Out-File report.html -append
Aaarrrggh, says my colon everytime I see that. Youre basically telling PowerShell to create three complete
HTML documents and jam them into a single file. While some browsers (Internet Explorer, notable) will figure that
out and display something, its just wrong. Once you start getting fancy with reports, youll figure out pretty quickly
that this approach is painful. It isnt PowerShells fault; youre just not following the rules. Hence this guide!
Youll notice that the HTML consists of a lot of other tags, too: <TABLE>, <TD>, <HEAD>, and so on. Most of
these are paired, meaning they come in an opening tag like <TD> and a closing tag like </TD>. The <TD> tag
represents a table cell, and everything between those tags is considered the contents of that cell.
The <HEAD> section is important. Whats inside there isnt normally visible in the browser; instead, the browser
focuses on whats in the <BODY> section. The <HEAD> section provides additional meta-data, like what the title
of the page will be (as displayed in the browsers window title bar, not in the page itself), any style sheets or scripts
that are attached to the page, and so on. Were going to do some pretty awesome stuff with the <HEAD> section,
trust me.
Youll also notice that this HTML is pretty clean, as opposed to, say, the HTML output by Microsoft Word. This
HTML doesnt have a lot of visual information embedded in it, like colors or fonts. Thats good, because it follows
correct HTML practices of separating formatting information from the document structure. Its disappointing at first,
because your HTML pages look really, really boring. But were going to fix that, also.
http://PowerShellBooks.com http://PowerShell.org
In order to help the narrative in this book stay focused, Im going to start with a single example. In that example,
were going to retrieve multiple bits of information about a remote computer, and format it all into a pretty, dynamic
HTML report. Hopefully, youll be able to focus on the techniques Im showing you, and adapt those to your own
specific needs.
In my example, I want the report to have five sections, each with the following information:
Computer Information
The computers operating system version, build number, and service pack version.
Hardware info: the amount of installed RAM and number of processes, along with the
manufacturer and model.
An list of all processes running on the machine.
A list of all services which are set to start automatically, but which arent running.
Information about all physical network adapters in the computer. Not IP addresses, necessarily -
hardware information like MAC address.
I realize this isnt a universally-interesting set of information, but these sections will allow be to demonstrate some
specific techniques. Again, Im hoping that you can adapt these to your precise needs.
Creating HTML Reports in PowerShell
5
http://PowerShellBooks.com http://PowerShell.org
Gathering the Information
Im a big fan of modular programming. Big, big fan. With that in mind, I tend to write functions that gather the
information I want to be in my report - and Ill usually do one function per major section of my report. Youll see in
a bit how thats beneficial. By writing each function individually, I make it easier to use that same information in
other tasks, and I make it easier to debug each one. The trick is to have each function output a single type of object
that combines all of the information for that report section. Ive created five functions, which Ive pasted into a
single script file. Ill give you each of those functions one at a time, with a brief commentary for each. Heres the
first:
function Get-InfoOS {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$os = Get-WmiObject -class Win32_OperatingSystem -ComputerName $ComputerName
$props = @{'OSVersion'=$os.version;
'SPVersion'=$os.servicepackmajorversion;
'OSBuild'=$os.buildnumber}
New-Object -TypeName PSObject -Property $props
}
This is a straightforward function, and the main reason I bothered to even make it a function - as opposed to just
using Get-WmiObject directly - is that I want different property names, like OSVersion instead of just Version.
That said, I tend to follow this exact same programming pattern for all info-retrieval functions, just to keep them
consistent.
function Get-InfoCompSystem {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$cs = Get-WmiObject -class Win32_ComputerSystem -ComputerName $ComputerName
$props = @{'Model'=$cs.model;
'Manufacturer'=$cs.manufacturer;
'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalmemory / 1GB);
'Sockets'=$cs.numberofprocessors;
'Cores'=$cs.numberoflogicalprocessors}
New-Object -TypeName PSObject -Property $props
}
Very similar to the last one. Youll notice here that Im using the -f formatting operator with the RAM property, so
that I get a value in gigabytes with 2 decimal places. The native value is in bytes, which isnt useful for me.
function Get-InfoBadService {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$svcs = Get-WmiObject -class Win32_Service -ComputerName $ComputerName `
-Filter "StartMode='Auto' AND State<>'Running'"
foreach ($svc in $svcs) {
$props = @{'ServiceName'=$svc.name;
'LogonAccount'=$svc.startname;
'DisplayName'=$svc.displayname}
New-Object -TypeName PSObject -Property $props
}
}
http://PowerShellBooks.com http://PowerShell.org
Here, Ive had to recognize that Ill be getting back more than one object from WMI, so I have to enumerate through
them using a ForEach construct. Again, Im primarily just renaming properties. I absolutely could have done that
with a Select-Object command, but I like to keep the overall function structure similar to my other functions. Just a
personal preference that helps me include fewer bugs, since Im used to doing things this way.
function Get-InfoProc {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$procs = Get-WmiObject -class Win32_Process -ComputerName $ComputerName
foreach ($proc in $procs) {
$props = @{'ProcName'=$proc.name;
'Executable'=$proc.ExecutablePath}
New-Object -TypeName PSObject -Property $props
}
}
Very similar to the function for services. You can probably start to see how using this same structure makes a certain
amount of copy-and-paste pretty effective when I create a new function.
function Get-InfoNIC {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$nics = Get-WmiObject -class Win32_NetworkAdapter -ComputerName $ComputerName `
-Filter "PhysicalAdapter=True"
foreach ($nic in $nics) {
$props = @{'NICName'=$nic.servicename;
'Speed'=$nic.speed / 1MB -as [int];
'Manufacturer'=$nic.manufacturer;
'MACAddress'=$nic.macaddress}
New-Object -TypeName PSObject -Property $props
}
}
The main thing of note here is how Ive converted the speed property, which is natively in bytes, to megabytes.
Because I dont care about decimal places here (I want a whole number), casting the value as an integer, by using the
-as operator, is easier for me than the -f formatting operator. Also, it gives me a chance to show you this technique!
Note that, for the purposes of this book, Im going to be putting these functions into the same script file as the rest of
my code, which actually generates the HTML. I dont normally do that. Normally, info-retrieval functions go into a
script module, and I then write my HTML-generation script to load that module. Having the functions in a module
makes them easier to use elsewhere, if I want to. Im skipping the module this time just to keep things simpler for
this demonstration. If you want to learn more about script modules, pick up Learn PowerShell Toolmaking in a
Month of Lunches or PowerShell in Depth, both of which are linked at http://PowerShellBooks.com.
Creating HTML Reports in PowerShell
7
http://PowerShellBooks.com http://PowerShell.org
Building the HTML
Once the information is retrieved, I can start creating the HTML. For each section of my report, Im going to
generate an HTML fragment. This is not a complete HTML page; its just the HTML needed to display that
particular report section. But first, Im going to add a little bit of code to my script. To make sure youre keeping up,
heres the entire thing:
<#
.SYNOPSIS
Generates an HTML-based system report for one or more computers.
Each computer specified will result in a separate HTML file;
specify the -Path as a folder where you want the files written.
Note that existing files will be overwritten.
.PARAMETER ComputerName
One or more computer names or IP addresses to query.
.PARAMETER Path
The path of the folder where the files should be written.
.EXAMPLE
.\New-HTMLSystemReport -ComputerName ONE,TWO -Path C:\Reports\
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[string[]]$ComputerName,
[Parameter(Mandatory=$True)]
[string]$Path
)
PROCESS {
function Get-InfoOS {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$os = Get-WmiObject -class Win32_OperatingSystem -ComputerName $ComputerName
$props = @{'OSVersion'=$os.version;
'SPVersion'=$os.servicepackmajorversion;
'OSBuild'=$os.buildnumber}
New-Object -TypeName PSObject -Property $props
}
function Get-InfoCompSystem {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$cs = Get-WmiObject -class Win32_ComputerSystem -ComputerName $ComputerName
$props = @{'Model'=$cs.model;
'Manufacturer'=$cs.manufacturer;
'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalmemory / 1GB);
'Sockets'=$cs.numberofprocessors;
'Cores'=$cs.numberoflogicalprocessors}
New-Object -TypeName PSObject -Property $props
}
function Get-InfoBadService {
[CmdletBinding()]
Creating HTML Reports in PowerShell
9
http://PowerShellBooks.com http://PowerShell.org
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$svcs = Get-WmiObject -class Win32_Service -ComputerName $ComputerName `
-Filter "StartMode='Auto' AND State<>'Running'"
foreach ($svc in $svcs) {
$props = @{'ServiceName'=$svc.name;
'LogonAccount'=$svc.startname;
'DisplayName'=$svc.displayname}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoProc {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$procs = Get-WmiObject -class Win32_Process -ComputerName $ComputerName
foreach ($proc in $procs) {
$props = @{'ProcName'=$proc.name;
'Executable'=$proc.ExecutablePath}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoNIC {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$nics = Get-WmiObject -class Win32_NetworkAdapter -ComputerName $ComputerName `
-Filter "PhysicalAdapter=True"
foreach ($nic in $nics) {
$props = @{'NICName'=$nic.servicename;
'Speed'=$nic.speed / 1MB -as [int];
'Manufacturer'=$nic.manufacturer;
'MACAddress'=$nic.macaddress}
New-Object -TypeName PSObject -Property $props
}
}
foreach ($computer in $computername) {
try {
$everything_ok = $true
Write-Verbose "Checking connectivity to $computer"
Get-WmiObject -class Win32_BIOS -ComputerName $Computer -EA Stop | Out-Null
} catch {
Write-Warning "$computer failed"
$everything_ok = $false
}
if ($everything_ok) {
}
}
}
Basically, Ive turned this script - which Im calling New-HTMLSystemReport - into an Advanced Script, accepting
its own parameters and even including comment-based help. I want to acknowledge that the contents of the
PROCESS script block are not properly indented; thats a function of the page width of this book and not because
Im sloppy!
http://PowerShellBooks.com http://PowerShell.org
Youll notice that, near the end, Im grabbing Win32_BIOS and piping it to Out-Null. Thats basically a ping for
me, although its specifically testing my ability to get to WMI on the remote machine. I dont care about the result I
get back; I just want to see if it works. If it doesnt, Ill display a warning and skip that machine.
The rest of my code is going to go inside the If construct:
if ($everything_ok) {
}
When we get to the end of the book, Ill give you the entire script again.
So lets start working on the HTML. At a very basic level, the following commands will do what I want:
if ($everything_ok) {
$filepath = Join-Path -Path $Path -ChildPath "$computer.html"
$html_os = Get-InfoOS -ComputerName $computer |
ConvertTo-HTML -As List -Fragment -PreContent "<h2>OS</h2>" |
Out-String
$html_cs = Get-InfoCompSystem -ComputerName $computer |
ConvertTo-HTML -As List -Fragment -PreContent "<h2>Hardware</h2>" |
Out-String
$html_pr = Get-InfoProc -ComputerName $computer |
ConvertTo-HTML -Fragment -PreContent "<h2>Processes</h2>" |
Out-String
$html_sv = Get-InfoBadService -ComputerName $computer |
ConvertTo-HTML -Fragment -PreContent "<h2>Check Services</h2>" |
Out-String
$html_na = Get-InfoNIC -ComputerName $Computer |
ConvertTo-HTML -Fragment -PreContent "<h2>NICs</h2>" |
Out-String
$params = @{'Title'="Report for $computer";
'PreContent'="<h1>System Report for $computer</h1>";
'PostContent'=$html_os,$html_cs,$html_pr,$html_sv,$html_na}
ConvertTo-HTML @params | Out-File -FilePath $filepath
}
Notice that Im basically just running all five of my functions, and piping them to ConvertTo-HTML. Each time, Im
giving ConvertTo-HTML a -PreContent parameter, which will become the section heading for that section of the
report. Im using an <H2> HTML tag in those headings, to make them stand apart. You can see that each of the five
is producing not a full HTML page, but an HTML fragment. The first two are producing a list, which I tend to prefer
if I have a single object to display; the last three are using a table, which is the default (you could also put -As Table
if you wanted to explicitly list that).
Also pay close attention to where Im piping that HTML: Im sending it to Out-String. Thats because ConvertTo-
HTML natively produces an array of strings, which will be problematic for me to use later. Out-String collapses all
those down into an object that will play nicely with what I want to do last.
My last trick is to run ConvertTo-HTML one last time, but without the -Fragment switch. In other words, Im
producing the final, completed HTML page. Im giving it a title for the browsers window bar, and Im using
-PreContent to insert an overall report header. My HTML fragments get fed to -PostContent, and then piped out to
my file. You can see at the top of this code snippet where I appended the computername.html filename to the
folder path that was specified.
Heres a portion of the resulting HTML, shown in Internet Explorer:
http://PowerShellBooks.com http://PowerShell.org
Not beautiful, but all of the information is there. Beautification is our next step. But before we do that, lets pause for
a moment and consider what weve produced: a complete, multi-section report, in HTML, which is suitable for
placing onto a Web server, attaching to an e-mail (using Send-MailMessage, you could even make the HTML
contents the -Body of the message if you also add the -BodyAsHTML switch). You could even frame the results.
Well, maybe not that - lets make it prettier first.
Creating HTML Reports in PowerShell
13
http://PowerShellBooks.com http://PowerShell.org
An Aside:
The ConvertTo-HTML Cmdlet
Obviously, one of the keys to all of this magic is the ConvertTo-HTML cmdlet. It has several parameters which are
worth examining:
-InputObject accepts input from the pipeline, and this input is used to construct either a list or a table.
A table is used by default, specify -As List if you want a list instead. I prefer lists whenever theres
only a single object being displayed, but thats just a personal preference.
-Body specifies the contents of the HTML <BODY> tag. This content will appear before anything else,
and the parameter accepts multiple values.
-PreContent displays next, followed by whatever table or list was generated from -InputObject.
-PostContent wraps up the display.
-Title specifies the contents of the <TITLE> tag, which appears in the <HEAD> section and specifies
what to display in the browsers window title bar.
-Head lets you specify contents for the <HEAD> section; this overrides -Title. Whatever you pass to
the -Head parameter needs to be properly formatted HTML, and must be legal for inclusion in the
<HEAD> section.
-CssUri lets you specify the name (or URL) of a Cascading Style Sheet.
Youd think that last one would be the key to making our report prettier, and youd be almost right.
Creating HTML Reports in PowerShell
15
http://PowerShellBooks.com http://PowerShell.org
Beautification of HTML
The HTML weve produced so far is exactly as it should be: plain. HTML is meant to describe the structure of a
document, such as where the headings are, what tables look like, and so on. A style sheet, or more properly a
Cascading Style Sheet (CSS) is designed to apply visual formatting to that structure.
CSS can be attached to HTML as a separate file, but I dont like that approach for management reports, mainly
because it means you have to mail two reports all over the place, instead of just one. I tend to prefer the other
approach, which is to embed the style information right into the main HTML page. Web developers are cringing
right now, but relax. Reports like this are meant to be more standalone.
Im going to start by creating the following CSS file, and saving it as C:\style.css. Now, in practice, youd probably
put this on a file share someplace. Were not going to link HTML pages directly to it, but will rather copy the
contents into each HTML page we create.
body {
color:#333333;
font-family:Calibri,Tahoma;
font-size: 10pt;
}
h1 {
text-align:center;
}
h2 {
border-top:1px solid #666666;
}
th {
font-weight:bold;
color:#000000;
background-color:#eeeeee;
}
Now Im going to modify a portion of my script, as follows, to utilize that:
foreach ($computer in $computername) {
$style = "<style>$(get-content C:\style.css)</style>"
try {
$everything_ok = $true
Write-Verbose "Checking connectivity to $computer"
Get-WmiObject -class Win32_BIOS -ComputerName $Computer -EA Stop | Out-Null
} catch {
Write-Warning "$computer failed"
$everything_ok = $false
}
if ($everything_ok) {
$filepath = Join-Path -Path $Path -ChildPath "$computer.html"
$html_os = Get-InfoOS -ComputerName $computer |
ConvertTo-HTML -As List -Fragment -PreContent "<h2>OS</h2>" |
Out-String
$html_cs = Get-InfoCompSystem -ComputerName $computer |
ConvertTo-HTML -As List -Fragment -PreContent "<h2>Hardware</h2>" |
Out-String
$html_pr = Get-InfoProc -ComputerName $computer |
ConvertTo-HTML -Fragment -PreContent "<h2>Processes</h2>" |
Out-String
http://PowerShellBooks.com http://PowerShell.org
$html_sv = Get-InfoBadService -ComputerName $computer |
ConvertTo-HTML -Fragment -PreContent "<h2>Check Services</h2>" |
Out-String
$html_na = Get-InfoNIC -ComputerName $Computer |
ConvertTo-HTML -Fragment -PreContent "<h2>NICs</h2>" |
Out-String
$params = @{'Head'="<title>Report for $computer</title>$style";
'PreContent'="<h1>System Report for $computer</h1>";
'PostContent'=$html_os,$html_cs,$html_pr,$html_sv,$html_na}
ConvertTo-HTML @params | Out-File -FilePath $filepath
}
}
Ive highlighted the two changes I made. The first one reads in the contents of that CSS file, inserting it between two
HTML <STYLE> tags. The second one changes from using the -Title parameter of ConvertTo-HTML, and instead
uses the more-flexible -Head parameter, specifying an entire <HEAD> section. Im inserting both a <TITLE> tag
(so that I still get a title in the browser window title bar), and my inline, or embedded, style sheet. Heres the new
result in Internet Explorer:
Awesome, no? All I did is create some style for four HTML tags: <BODY>, which sets the default for the page,
<H1> and <H2>, and the <TH> tag used for table header rows. Of course, you need to dig into CSS to learn more
about what styles you can set and it can get pretty complex. But the results can be pretty astounding.
Note If youd like a complete reference and tutorial on CSS, you can get one online for free. Just head to
http://w3schools.com/css3/default.asp, where youll find links to both examples, tutorials, and references.
Creating HTML Reports in PowerShell
17
http://PowerShellBooks.com http://PowerShell.org
Lets quickly review: Suppose you followed my naming convention and named your script New-
HTMLSystemReport. Also suppose you have an organizational unit in Active Directory named Servers, and that you
want to generate a report for each server in that OU. Using Microsofts ActiveDirectory PowerShell module, you
could run this (Im using COMPANY.COM as my AD domain name; youd obviously adjust that):
Get-ADComputer -filter * -searchBase "ou=servers,dc=company,dc=com" |
Select-Object -ExpandProperty Name |
C:\MyScripts\New-HTMLSystemReport -Path C:\HTMLReports
After a bit of waiting, youd have your HTML reports. Although, to be honest, the way Ive hardcoded the location
of the CSS template file bugs me. So heres a final script, with that parameterized:
<#
.SYNOPSIS
Generates an HTML-based system report for one or more computers.
Each computer specified will result in a separate HTML file;
specify the -Path as a folder where you want the files written.
Note that existing files will be overwritten.
.PARAMETER ComputerName
One or more computer names or IP addresses to query.
.PARAMETER Path
The path of the folder where the files should be written.
.PARAMETER CssPath
The path and filename of the CSS template to use.
.EXAMPLE
.\New-HTMLSystemReport -ComputerName ONE,TWO `
-Path C:\Reports\ `
-CssPath c:\style.css
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[string[]]$ComputerName,
[Parameter(Mandatory=$True)]
[string]$Path,
[Parameter(Mandatory=$True)]
[string]$CssPath
)
PROCESS {
function Get-InfoOS {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$os = Get-WmiObject -class Win32_OperatingSystem -ComputerName $ComputerName
$props = @{'OSVersion'=$os.version;
'SPVersion'=$os.servicepackmajorversion;
'OSBuild'=$os.buildnumber}
New-Object -TypeName PSObject -Property $props
}
function Get-InfoCompSystem {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$cs = Get-WmiObject -class Win32_ComputerSystem -ComputerName $ComputerName
$props = @{'Model'=$cs.model;
Creating HTML Reports in PowerShell
19
http://PowerShellBooks.com http://PowerShell.org
'Manufacturer'=$cs.manufacturer;
'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalmemory / 1GB);
'Sockets'=$cs.numberofprocessors;
'Cores'=$cs.numberoflogicalprocessors}
New-Object -TypeName PSObject -Property $props
}
function Get-InfoBadService {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$svcs = Get-WmiObject -class Win32_Service -ComputerName $ComputerName `
-Filter "StartMode='Auto' AND State<>'Running'"
foreach ($svc in $svcs) {
$props = @{'ServiceName'=$svc.name;
'LogonAccount'=$svc.startname;
'DisplayName'=$svc.displayname}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoProc {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$procs = Get-WmiObject -class Win32_Process -ComputerName $ComputerName
foreach ($proc in $procs) {
$props = @{'ProcName'=$proc.name;
'Executable'=$proc.ExecutablePath}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoNIC {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$nics = Get-WmiObject -class Win32_NetworkAdapter -ComputerName $ComputerName `
-Filter "PhysicalAdapter=True"
foreach ($nic in $nics) {
$props = @{'NICName'=$nic.servicename;
'Speed'=$nic.speed / 1MB -as [int];
'Manufacturer'=$nic.manufacturer;
'MACAddress'=$nic.macaddress}
New-Object -TypeName PSObject -Property $props
}
}
foreach ($computer in $computername) {
$style = "<style>$(get-content $csspath)</style>"
try {
$everything_ok = $true
Write-Verbose "Checking connectivity to $computer"
Get-WmiObject -class Win32_BIOS -ComputerName $Computer -EA Stop | Out-Null
} catch {
Write-Warning "$computer failed"
$everything_ok = $false
}
http://PowerShellBooks.com http://PowerShell.org
if ($everything_ok) {
$filepath = Join-Path -Path $Path -ChildPath "$computer.html"
$html_os = Get-InfoOS -ComputerName $computer |
ConvertTo-HTML -As List -Fragment -PreContent "<h2>OS</h2>" |
Out-String
$html_cs = Get-InfoCompSystem -ComputerName $computer |
ConvertTo-HTML -As List -Fragment -PreContent "<h2>Hardware</h2>" |
Out-String
$html_pr = Get-InfoProc -ComputerName $computer |
ConvertTo-HTML -Fragment -PreContent "<h2>Processes</h2>" |
Out-String
$html_sv = Get-InfoBadService -ComputerName $computer |
ConvertTo-HTML -Fragment -PreContent "<h2>Check Services</h2>" |
Out-String
$html_na = Get-InfoNIC -ComputerName $Computer |
ConvertTo-HTML -Fragment -PreContent "<h2>NICs</h2>" |
Out-String
$params = @{'Head'="<title>Report for $computer</title>$style";
'PreContent'="<h1>System Report for $computer</h1>";
'PostContent'=$html_os,$html_cs,$html_pr,$html_sv,$html_na}
ConvertTo-HTML @params | Out-File -FilePath $filepath
}
}
}
Now, youd run something like this for the same example:
Get-ADComputer -filter * -searchBase "ou=servers,dc=company,dc=com" |
Select-Object -ExpandProperty Name |
C:\MyScripts\New-HTMLSystemReport -Path C:\HTMLReports -CssPath C:\style.css
Obviously, youd adjust the file paths to suit your system.
We could stop right here and youd have a marvelous HTML report. But lets keep going.

Creating HTML Reports in PowerShell
21
http://PowerShellBooks.com http://PowerShell.org
Making Tables Prettier
One area where I think CSS is a bit weak is in formatting tables. For example, in order to make alternating rows
have a different background color, you have to have a table cell tag that includes a class name, like <TD
class="typeA">. PowerShell doesnt emit class names into its HTML, though, which would seem to present an
insurmountable hurdle.
It isnt surmountable, but its pretty darn difficult. For this approach, Im going to take advantage of the fact that
PowerShell produces such well-formed HTML, meaning it is essentially XHTML, which is XML, which
PowerShell can manipulate quite handily. Im going to do this with my individual HTML fragments, before I
assemble the final page, because its a lot easier that way.
This does require an understanding of some HTML/XML basics. For example, consider this HTML table:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/
xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>HTML TABLE</title>
</head><body>
<table>
<colgroup><col/><col/><col/><col/><col/></colgroup>
<tr><th>ComputerName</th><th>Drive</th><th>Free(GB)</th><th>Free(%)</th><th>Size(GB)</th></tr>
<tr><td>CLIENT</td><td>C:</td><td>49</td><td>82</td><td>60</td></tr>
</table>
</body></html>
First, it says right there that this is XHTML, which is awesome. Second, notice the <TABLE> tag. Everything
within it - all of the <COLGROUP> and <TR> tags - are children of the <TABLE>. Within each <TR>, you have
the children of that tag, which are either <TH> or <TD>. Were going to rely on this hierarchical relationship. Check
out this function, which Im adding to my script:
function Set-AlternatingCSSClasses {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,ValueFromPipeline=$True)]
[string]$HTMLFragment,

[Parameter(Mandatory=$True)]
[string]$CSSEvenClass,

[Parameter(Mandatory=$True)]
[string]$CssOddClass
)
[xml]$xml = $HTMLFragment
$table = $xml.SelectSingleNode('table')
$classname = $CSSOddClass
foreach ($tr in $table.tr) {
if ($classname -eq $CSSEvenClass) {
$classname = $CssOddClass
} else {
$classname = $CSSEvenClass
}
$class = $xml.CreateAttribute('class')
$class.value = $classname
$tr.attributes.append($class) | Out-null
}
$xml.innerxml | out-string
http://PowerShellBooks.com http://PowerShell.org
}
This will accept an HTML fragment, provided it is valid XML and that it contains a <TABLE> tag. Itll go through
that table, adding the specified CSS class names to the <TR> tags. Now, theres a trick to this: The old HTML
fragments I was generating contain a top-level <H2> tag from my -PreContent switch, as well as a top-level
<TABLE> tag. Having two top-level elements isnt valid XML, so Im going to have to modify the rest of my script
to work with this:
if ($everything_ok) {
$filepath = Join-Path -Path $Path -ChildPath "$computer.html"
$html_os = Get-InfoOS -ComputerName $computer |
ConvertTo-HTML -As List -Fragment -PreContent "<h2>OS</h2>" |
Out-String
$html_cs = Get-InfoCompSystem -ComputerName $computer |
ConvertTo-HTML -As List -Fragment -PreContent "<h2>Hardware</h2>" |
Out-String
$html_pr = Get-InfoProc -ComputerName $computer |
ConvertTo-HTML -Fragment |
Out-String |
Set-AlternatingCSSClasses -CSSEvenClass 'even' -CssOddClass 'odd'
$html_pr = "<h2>Processes</h2>$html_pr"
$html_sv = Get-InfoBadService -ComputerName $computer |
ConvertTo-HTML -Fragment |
Out-String |
Set-AlternatingCSSClasses -CSSEvenClass 'even' -CssOddClass 'odd'
$html_sv = "<h2>Check Services</h2>$html_sv"
$html_na = Get-InfoNIC -ComputerName $Computer |
ConvertTo-HTML -Fragment |
Out-String |
Set-AlternatingCSSClasses -CSSEvenClass 'even' -CssOddClass 'odd'
$html_na = "<h2>NICs</h2>$html_na"
$params = @{'Head'="<title>Report for $computer</title>$style";
'PreContent'="<h1>System Report for $computer</h1>";
'PostContent'=$html_os,$html_cs,$html_pr,$html_sv,$html_na}
ConvertTo-HTML @params | Out-File -FilePath $filepath
}
So Im taking the HTML fragment without the -PreContent, piping it to Out-String, and then piping it to my new
Set-AlternatingCSSClasses function. Im telling it to use the CSS class names even and odd. When I get the
result back, Im manually prepending the <H2> heading to each fragment.
Ive also added a bit to my CSS file. Heres the whole thing:
body {
color:#333333;
font-family:Calibri,Tahoma;
font-size: 10pt;
}
h1 {
text-align:center;
}
h2 {
border-top:1px solid #666666;
}
th {
font-weight:bold;
color:#eeeeee;
background-color:#333333;
Creating HTML Reports in PowerShell
23
http://PowerShellBooks.com http://PowerShell.org
}
.odd { background-color:#ffffff; }
.even { background-color:#dddddd; }
Notice that, while HTML tags like <BODY> and <H1> are referred to by just the tag name in the CSS, my class
names are preceded by a period. The final result looks like this:
So now I have awesome alternating colors on my table rows. This is cool. I made the table header rows a bit darker
to make them stand out even more.
http://PowerShellBooks.com http://PowerShell.org
The Final Product
Heres my final script, which I saved as New-HTMLSystemReport.ps1:
<#
.SYNOPSIS
Generates an HTML-based system report for one or more computers.
Each computer specified will result in a separate HTML file;
specify the -Path as a folder where you want the files written.
Note that existing files will be overwritten.
.PARAMETER ComputerName
One or more computer names or IP addresses to query.
.PARAMETER Path
The path of the folder where the files should be written.
.PARAMETER CssPath
The path and filename of the CSS template to use.
.EXAMPLE
.\New-HTMLSystemReport -ComputerName ONE,TWO `
-Path C:\Reports\ `
-CssPath c:\style.css
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[string[]]$ComputerName,
[Parameter(Mandatory=$True)]
[string]$Path,
[Parameter(Mandatory=$True)]
[string]$CssPath
)
PROCESS {
function Get-InfoOS {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$os = Get-WmiObject -class Win32_OperatingSystem -ComputerName $ComputerName
$props = @{'OSVersion'=$os.version;
'SPVersion'=$os.servicepackmajorversion;
'OSBuild'=$os.buildnumber}
New-Object -TypeName PSObject -Property $props
}
function Get-InfoCompSystem {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$cs = Get-WmiObject -class Win32_ComputerSystem -ComputerName $ComputerName
$props = @{'Model'=$cs.model;
'Manufacturer'=$cs.manufacturer;
'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalmemory / 1GB);
'Sockets'=$cs.numberofprocessors;
'Cores'=$cs.numberoflogicalprocessors}
Creating HTML Reports in PowerShell
25
http://PowerShellBooks.com http://PowerShell.org
New-Object -TypeName PSObject -Property $props
}
function Get-InfoBadService {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$svcs = Get-WmiObject -class Win32_Service -ComputerName $ComputerName `
-Filter "StartMode='Auto' AND State<>'Running'"
foreach ($svc in $svcs) {
$props = @{'ServiceName'=$svc.name;
'LogonAccount'=$svc.startname;
'DisplayName'=$svc.displayname}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoProc {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$procs = Get-WmiObject -class Win32_Process -ComputerName $ComputerName
foreach ($proc in $procs) {
$props = @{'ProcName'=$proc.name;
'Executable'=$proc.ExecutablePath}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoNIC {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$nics = Get-WmiObject -class Win32_NetworkAdapter -ComputerName $ComputerName `
-Filter "PhysicalAdapter=True"
foreach ($nic in $nics) {
$props = @{'NICName'=$nic.servicename;
'Speed'=$nic.speed / 1MB -as [int];
'Manufacturer'=$nic.manufacturer;
'MACAddress'=$nic.macaddress}
New-Object -TypeName PSObject -Property $props
}
}
function Set-AlternatingCSSClasses {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,ValueFromPipeline=$True)]
[string]$HTMLFragment,

[Parameter(Mandatory=$True)]
[string]$CSSEvenClass,

[Parameter(Mandatory=$True)]
[string]$CssOddClass
)
[xml]$xml = $HTMLFragment
$table = $xml.SelectSingleNode('table')
$classname = $CSSOddClass
foreach ($tr in $table.tr) {
if ($classname -eq $CSSEvenClass) {
http://PowerShellBooks.com http://PowerShell.org
$classname = $CssOddClass
} else {
$classname = $CSSEvenClass
}
$class = $xml.CreateAttribute('class')
$class.value = $classname
$tr.attributes.append($class) | Out-null
}
$xml.innerxml | out-string
}
foreach ($computer in $computername) {
$style = "<style>$(get-content $csspath)</style>"
try {
$everything_ok = $true
Write-Verbose "Checking connectivity to $computer"
Get-WmiObject -class Win32_BIOS -ComputerName $Computer -EA Stop | Out-Null
} catch {
Write-Warning "$computer failed"
$everything_ok = $false
}
if ($everything_ok) {
$filepath = Join-Path -Path $Path -ChildPath "$computer.html"
$html_os = Get-InfoOS -ComputerName $computer |
ConvertTo-HTML -As List -Fragment `
-PreContent "<h2>OS</h2>" |
Out-String
$html_cs = Get-InfoCompSystem -ComputerName $computer |
ConvertTo-HTML -As List -Fragment `
-PreContent "<h2>Hardware</h2>" |
Out-String
$html_pr = Get-InfoProc -ComputerName $computer |
ConvertTo-HTML -Fragment |
Out-String |
Set-AlternatingCSSClasses -CSSEvenClass 'even' -CssOddClass 'odd'
$html_pr = "<h2>Processes</h2>$html_pr"
$html_sv = Get-InfoBadService -ComputerName $computer |
ConvertTo-HTML -Fragment |
Out-String |
Set-AlternatingCSSClasses -CSSEvenClass 'even' -CssOddClass 'odd'
$html_sv = "<h2>Check Services</h2>$html_sv"
$html_na a= Get-InfoNIC -ComputerName $Computer |
ConvertTo-HTML -Fragment |
Out-String |
Set-AlternatingCSSClasses -CSSEvenClass 'even' -CssOddClass 'odd'
$html_na = "<h2>NICs</h2>$html_na"
$params = @{'Head'="<title>Report for $computer</title>$style";
'PreContent'="<h1>System Report for $computer</h1>";
'PostContent'=$html_os,$html_cs,$html_pr,$html_sv,$html_na}
ConvertTo-HTML @params | Out-File -FilePath $filepath
}
}
}

Creating HTML Reports in PowerShell
27
http://PowerShellBooks.com http://PowerShell.org
And now heres my CSS template, which I save as Style.css:
body {
color:#333333;
font-family:Calibri,Tahoma;
font-size: 10pt;
}
h1 {
text-align:center;
}
h2 {
border-top:1px solid #666666;
}
th {
font-weight:bold;
color:#eeeeee;
background-color:#333333;
}
.odd { background-color:#ffffff; }
.even { background-color:#dddddd; }
And, just for fun, Im going to give you one final version. This one eliminates the need to have Style.css as a
separate file, and instead embeds it right into the script. For this version, Ive removed the -CssFile parameter, since
its no longer needed.
<#
.SYNOPSIS
Generates an HTML-based system report for one or more computers.
Each computer specified will result in a separate HTML file;
specify the -Path as a folder where you want the files written.
Note that existing files will be overwritten.
.PARAMETER ComputerName
One or more computer names or IP addresses to query.
.PARAMETER Path
The path of the folder where the files should be written.
.PARAMETER CssPath
The path and filename of the CSS template to use.
.EXAMPLE
.\New-HTMLSystemReport -ComputerName ONE,TWO `
-Path C:\Reports\
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[string[]]$ComputerName,
[Parameter(Mandatory=$True)]
[string]$Path
)
PROCESS {
$style = @"
<style>
body {
color:#333333;
font-family:Calibri,Tahoma;
font-size: 10pt;
}
h1 {
http://PowerShellBooks.com http://PowerShell.org
text-align:center;
}
h2 {
border-top:1px solid #666666;
}
th {
font-weight:bold;
color:#eeeeee;
background-color:#333333;
}
.odd { background-color:#ffffff; }
.even { background-color:#dddddd; }
</style>
"@
function Get-InfoOS {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$os = Get-WmiObject -class Win32_OperatingSystem -ComputerName $ComputerName
$props = @{'OSVersion'=$os.version;
'SPVersion'=$os.servicepackmajorversion;
'OSBuild'=$os.buildnumber}
New-Object -TypeName PSObject -Property $props
}
function Get-InfoCompSystem {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$cs = Get-WmiObject -class Win32_ComputerSystem -ComputerName $ComputerName
$props = @{'Model'=$cs.model;
'Manufacturer'=$cs.manufacturer;
'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalmemory / 1GB);
'Sockets'=$cs.numberofprocessors;
'Cores'=$cs.numberoflogicalprocessors}
New-Object -TypeName PSObject -Property $props
}
function Get-InfoBadService {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$svcs = Get-WmiObject -class Win32_Service -ComputerName $ComputerName `
-Filter "StartMode='Auto' AND State<>'Running'"
foreach ($svc in $svcs) {
$props = @{'ServiceName'=$svc.name;
'LogonAccount'=$svc.startname;
'DisplayName'=$svc.displayname}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoProc {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$procs = Get-WmiObject -class Win32_Process -ComputerName $ComputerName
foreach ($proc in $procs) {
Creating HTML Reports in PowerShell
29
http://PowerShellBooks.com http://PowerShell.org
$props = @{'ProcName'=$proc.name;
'Executable'=$proc.ExecutablePath}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoNIC {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$nics = Get-WmiObject -class Win32_NetworkAdapter -ComputerName $ComputerName `
-Filter "PhysicalAdapter=True"
foreach ($nic in $nics) {
$props = @{'NICName'=$nic.servicename;
'Speed'=$nic.speed / 1MB -as [int];
'Manufacturer'=$nic.manufacturer;
'MACAddress'=$nic.macaddress}
New-Object -TypeName PSObject -Property $props
}
}
function Set-AlternatingCSSClasses {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,ValueFromPipeline=$True)]
[string]$HTMLFragment,

[Parameter(Mandatory=$True)]
[string]$CSSEvenClass,

[Parameter(Mandatory=$True)]
[string]$CssOddClass
)
[xml]$xml = $HTMLFragment
$table = $xml.SelectSingleNode('table')
$classname = $CSSOddClass
foreach ($tr in $table.tr) {
if ($classname -eq $CSSEvenClass) {
$classname = $CssOddClass
} else {
$classname = $CSSEvenClass
}
$class = $xml.CreateAttribute('class')
$class.value = $classname
$tr.attributes.append($class) | Out-null
}
$xml.innerxml | out-string
}
foreach ($computer in $computername) {
try {
$everything_ok = $true
Write-Verbose "Checking connectivity to $computer"
Get-WmiObject -class Win32_BIOS -ComputerName $Computer -EA Stop | Out-Null
} catch {
Write-Warning "$computer failed"
$everything_ok = $false
}
if ($everything_ok) {
$filepath = Join-Path -Path $Path -ChildPath "$computer.html"
$html_os = Get-InfoOS -ComputerName $computer |
ConvertTo-HTML -As List -Fragment `
http://PowerShellBooks.com http://PowerShell.org
-PreContent "<h2>OS</h2>" |
Out-String
$html_cs = Get-InfoCompSystem -ComputerName $computer |
ConvertTo-HTML -As List -Fragment `
-PreContent "<h2>Hardware</h2>" |
Out-String
$html_pr = Get-InfoProc -ComputerName $computer |
ConvertTo-HTML -Fragment |
Out-String |
Set-AlternatingCSSClasses -CSSEvenClass 'even' -CssOddClass 'odd'
$html_pr = "<h2>Processes</h2>$html_pr"
$html_sv = Get-InfoBadService -ComputerName $computer |
ConvertTo-HTML -Fragment |
Out-String |
Set-AlternatingCSSClasses -CSSEvenClass 'even' -CssOddClass 'odd'
$html_sv = "<h2>Check Services</h2>$html_sv"
$html_na = Get-InfoNIC -ComputerName $Computer |
ConvertTo-HTML -Fragment |
Out-String |
Set-AlternatingCSSClasses -CSSEvenClass 'even' -CssOddClass 'odd'
$html_na = "<h2>NICs</h2>$html_na"
$params = @{'Head'="<title>Report for $computer</title>$style";
'PreContent'="<h1>System Report for $computer</h1>";
'PostContent'=$html_os,$html_cs,$html_pr,$html_sv,$html_na}
ConvertTo-HTML @params | Out-File -FilePath $filepath
}
}
}

So there you have it. I hope youve enjoyed, and I hope you find this useful.
But Im not done.
Creating HTML Reports in PowerShell
31
http://PowerShellBooks.com http://PowerShell.org
Creating an Awesome Report
If youre happy with the reporting capabilities youve gotten so far from this guide, then you can just stop right here.
My next step is going to be awesome, but its also going to be pretty intense. Its not only going to show you some
cool new HTML techniques, but also provide an example of the right way to build reusable tools in PowerShell.
Your HTML knowledge is going to be expanded as well.
Howd you like to create reports like this one?
Those last three sections can be toggled, so they start hidden and can be shown on-demand. The tables within those
sections are dynamic, meaning theyre paginated and the columns are click-sortable. This rocks.
In order to make this part of the guide as friendly as possible, Im going to start by explaining how to make this kind
of report, using the tools Im providing you. Then Ill explain how those tools work. That way, if you just want to
make these reports on your own, but dont really care about the under-the-hood magic, you can do that.
http://PowerShellBooks.com http://PowerShell.org
How to Do it
Some pre-requisites and caveats:
Ive only tested this on Internet Explorer 10. It ought to work on most other browsers, but it may not.
The extra functionality comes from JavaScript, and some browsers or e-mail clients may display
security warnings when the JavaScript loads.
As written, the JavaScript loads from Microsofts ASP.NET Content Delivery Network (CDN). You
can also download the files and host them on your local intranet, and simply point the report files there
to get the JavaScript.
Youll need some additional tools. Those should have been in the ZIP file along with this guide; if
theyre not there, download a fresh copy of the guide from http://PowerShellBooks.com.
Heres my main script file, which Ive named Old EnhancedHTML Module Demo.ps1.
<#
.SYNOPSIS
Generates an HTML-based system report for one or more computers.
Each computer specified will result in a separate HTML file;
specify the -Path as a folder where you want the files written.
Note that existing files will be overwritten.
.PARAMETER ComputerName
One or more computer names or IP addresses to query.
.PARAMETER Path
The path of the folder where the files should be written.
.PARAMETER CssPath
The path and filename of the CSS template to use.
.EXAMPLE
.\New-HTMLSystemReport -ComputerName ONE,TWO `
-Path C:\Reports\
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[string[]]$ComputerName,
[Parameter(Mandatory=$True)]
[string]$Path
)
BEGIN {
Import-Module EnhancedHTML
}
PROCESS {
$style = @"
<style>
body {
color:#333333;
font-family:Calibri,Tahoma;
font-size: 10pt;
}
h1 {
text-align:center;
}
h2 {
border-top:1px solid #666666;
}
Creating HTML Reports in PowerShell
33
http://PowerShellBooks.com http://PowerShell.org
th {
font-weight:bold;
color:#eeeeee;
background-color:#333333;
}
.odd { background-color:#ffffff; }
.even { background-color:#dddddd; }
.paginate_enabled_next, .paginate_enabled_previous {
cursor:pointer;
border:1px solid #222222;
background-color:#dddddd;
padding:2px;
margin:4px;
border-radius:2px;
}
.paginate_disabled_previous, .paginate_disabled_next {
color:#666666;
cursor:pointer;
background-color:#dddddd;
padding:2px;
margin:4px;
border-radius:2px;
}
.dataTables_info { margin-bottom:4px; }
.sectionheader { cursor:pointer; }
.sectionheader:hover { color:red; }
.grid { width:100% }
</style>
"@
function Get-InfoOS {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$os = Get-WmiObject -class Win32_OperatingSystem -ComputerName $ComputerName
$props = @{'OSVersion'=$os.version;
'SPVersion'=$os.servicepackmajorversion;
'OSBuild'=$os.buildnumber}
New-Object -TypeName PSObject -Property $props
}
function Get-InfoCompSystem {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$cs = Get-WmiObject -class Win32_ComputerSystem -ComputerName $ComputerName
$props = @{'Model'=$cs.model;
'Manufacturer'=$cs.manufacturer;
'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalmemory / 1GB);
'Sockets'=$cs.numberofprocessors;
'Cores'=$cs.numberoflogicalprocessors}
New-Object -TypeName PSObject -Property $props
}
function Get-InfoBadService {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$svcs = Get-WmiObject -class Win32_Service -ComputerName $ComputerName `
-Filter "StartMode='Auto' AND State<>'Running'"
http://PowerShellBooks.com http://PowerShell.org
foreach ($svc in $svcs) {
$props = @{'ServiceName'=$svc.name;
'LogonAccount'=$svc.startname;
'DisplayName'=$svc.displayname}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoProc {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$procs = Get-WmiObject -class Win32_Process -ComputerName $ComputerName
foreach ($proc in $procs) {
$props = @{'ProcName'=$proc.name;
'Executable'=$proc.ExecutablePath}
New-Object -TypeName PSObject -Property $props
}
}
function Get-InfoNIC {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$nics = Get-WmiObject -class Win32_NetworkAdapter -ComputerName $ComputerName `
-Filter "PhysicalAdapter=True"
foreach ($nic in $nics) {
$props = @{'NICName'=$nic.servicename;
'Speed'=$nic.speed / 1MB -as [int];
'Manufacturer'=$nic.manufacturer;
'MACAddress'=$nic.macaddress}
New-Object -TypeName PSObject -Property $props
}
}
foreach ($computer in $computername) {
try {
$everything_ok = $true
Write-Verbose "Checking connectivity to $computer"
Get-WmiObject -class Win32_BIOS -ComputerName $Computer -EA Stop | Out-Null
} catch {
Write-Warning "$computer failed"
$everything_ok = $false
}
if ($everything_ok) {
$filepath = Join-Path -Path $Path -ChildPath "$computer.html"
$params = @{'As'='List';
'PreContent'='<h2>OS</h2>';
'TableCssID'='tableOS';
'DivCssID'='divOS'}
$html_os = Get-InfoOS -ComputerName $computer |
ConvertTo-EnhancedHTMLFragment @params |
Out-String
$params = @{'As'='List';
'PreContent'='<h2>Computer System</h2>';
'TableCssID'='tableCS';
'DivCssID'='divCS'}
$html_cs = Get-InfoCompSystem -ComputerName $computer |
ConvertTo-EnhancedHTMLFragment @params |
Creating HTML Reports in PowerShell
35
http://PowerShellBooks.com http://PowerShell.org
Out-String
$params = @{'As'='Table';
'PreContent'='<h2>&diams; Processes</h2>';
'TableCssID'='tableProc';
'DivCssID'='divProc';
'EvenRowCssClass'='even';
'OddRowCssClass'='odd';
'MakeHiddenSection'=$true;
'TableCssClass'='grid'}
$html_pr = Get-InfoProc -ComputerName $computer |
ConvertTo-EnhancedHTMLFragment @params |
Out-String
$params = @{'As'='Table';
'PreContent'='<h2>&diams; Services to Check</h2>';
'TableCssID'='tableSvc';
'DivCssID'='divSvc';
'EvenRowCssClass'='even';
'OddRowCssClass'='odd';
'MakeHiddenSection'=$true;
'TableCssClass'='grid'}
$html_sv = Get-InfoBadService -ComputerName $computer |
ConvertTo-EnhancedHTMLFragment @params |
Out-String
$params = @{'As'='Table';
'PreContent'='<h2>&diams; NICs</h2>';
'TableCssID'='tableNIC';
'DivCssID'='divPNIC';
'EvenRowCssClass'='even';
'OddRowCssClass'='odd';
'MakeHiddenSection'=$true;
'TableCssClass'='grid'}
$html_na = Get-InfoNIC -ComputerName $Computer |
ConvertTo-EnhancedHTMLFragment @params |
Out-String
$params = @{'CssStyleSheet'=$style;
'Title'="System Report for $computer";
'PreContent'="<h1>System Report for $computer</h1>";
'CssIdsToMakeDataTables'=@('tableProc','tableNIC','tableSvc');
'HTMLFragments'=@($html_os,$html_cs,$html_pr,$html_sv,$html_na)}
ConvertTo-EnhancedHTML @params |
Out-File -FilePath $filepath
}
}
}
The main thing to notice is at the top of the script:
BEGIN {
Import-Module EnhancedHTML
}
EnhancedHTML is a new PowerShell module, which Im giving you along with this book. In order for PowerShell
to find it, the modules folder needs to go into one of the paths in the PSModulePath environment variable. By
default, that variable includes the path:
\[My ]Documents\WindowsPowerShell\Modules
http://PowerShellBooks.com http://PowerShell.org
So, you would save the module file as follows:
\[My ]Documents\WindowsPowerShell\Modules\EnhancedHTML\EnhancedHTML.psm1
Note Youll find two versions of EnhancedHTML in the ZIP file for this book. One is titled
OldEnhancedHTML and was from this books original release; EnhancedHTML is the newest and
includes the additional features discussed. EnhancedHTML is backward-compatible with the original
release.
Be careful on Windows Vista and newer systems, because the Documents Library shown in Explorer consists of
both My Documents and Public Documents. Make sure you expand that so you can clearly see My
Documents; PowerShell wont find the module if it winds up in the Public Documents portion.
The demo script is essentially unchanged from the prior versions in this guide. Whats changed is how I generate the
HTML fragments. Rather than using ConvertTo-HTML -Fragment as I did earlier, Im using my new ConvertTo-
EnhancedHTMLFragment command. Here are two examples from the script:
$params = @{'As'='List';
'PreContent'='<h2>Computer System</h2>';
'TableCssID'='tableCS';
'DivCssID'='divCS'}
$html_cs = Get-InfoCompSystem -ComputerName $computer |
ConvertTo-EnhancedHTMLFragment @params |
Out-String
$params = @{'As'='Table';
'PreContent'='<h2>&diams; Processes</h2>';
'TableCssID'='tableProc';
'DivCssID'='divProc';
'EvenRowCssClass'='even';
'OddRowCssClass'='odd';
'MakeHiddenSection'=$true;
'TableCssClass'='grid'}
$html_pr = Get-InfoProc -ComputerName $computer |
ConvertTo-EnhancedHTMLFragment @params |
Out-String
The first is creating a list, which wont be click-to-display. Youll notice that Im using a technique called splatting
to build the parameters for the command. Doing so simply lets me line the parameters up all nice and neat, and
makes them fit better into the page width for this guide. With splatting, youll also notice that the parameter names
dont get a dash (-) in front.
-PreContent is where I specify the heading for that section of my report.
-TableCssId lets me assign a CSS ID to the table, which lets me refer to it in the JavaScript.
-As lets me format this fragment as a list or table. Technically, its an HTML table either way; with -As
List you get a two-column table with property names in the left column and values in the right.
-DivCssId lets me assign a CSS ID to the <DIV> tag thats going to be wrapped around the table.
Again, this simply lets me refer to the thing from JavaScript.
The next section uses ConvertTo-EnhancedHTMLFragment again, but this time creates an interactive, click-to-show
grid. The parameters used:
-PreContent is where I specify the heading for that section of my report. Because Im going to make
this click-to-expand, Ive included a diamond symbol (&diams;), which looks a little (to me) like an
up/down arrow icon. You can put whatever you like, or nothing at all.
Creating HTML Reports in PowerShell
37
http://PowerShellBooks.com http://PowerShell.org
-TableCssId and DivCssId are here again.
-EvenRowCssClass and -OddRowCssClass let me define CSS classes for the even and odd rows of the
table, so that I can style them differently. The difference between a CSS ID and a CSS class is that an
ID is meant to uniquely refer to just one thing, while a class can be applied to multiple items that are
meant to be styled in the same way.
-MakeHiddenSection is what triggers the click-to-show functionality. Normally, this is a switch
parameter; since Im using splatting, I have to assign the value $True.
-TableCssClass assigns a class to the table. Ill use this in the stylesheet to force the table to be full-
width.
The last thing you can consider is the embedded stylesheet from the script:
$style = @"
<style>
body {
color:#333333;
font-family:Calibri,Tahoma;
font-size: 10pt;
}
h1 {
text-align:center;
}
h2 {
border-top:1px solid #666666;
}
th {
font-weight:bold;
color:#eeeeee;
background-color:#333333;
}
.odd { background-color:#ffffff; }
.even { background-color:#dddddd; }
.paginate_enabled_next, .paginate_enabled_previous {
cursor:pointer;
border:1px solid #222222;
background-color:#dddddd;
padding:2px;
margin:4px;
border-radius:2px;
}
.paginate_disabled_previous, .paginate_disabled_next {
color:#666666;
cursor:pointer;
background-color:#dddddd;
padding:2px;
margin:4px;
border-radius:2px;
}
.dataTables_info { margin-bottom:4px; }
.sectionheader { cursor:pointer; }
.sectionheader:hover { color:red; }
.grid { width:100% }
</style>
"@
This is constructed as a here-string, which lets me paste literal text into the script and have it treated as a single
string. Its stored in a variable named $style. In addition to styling several HTML tags (like <BODY>, <H1>, <H2>,
and <TH>) this assigns styles to several CSS classes:
http://PowerShellBooks.com http://PowerShell.org
.odd and .even are the classes I specified for my odd/even table rows.
The four .paginate classes and the .dataTables_info class are created by the JavaScript that makes the
tables into interactive grids. Here, Im styling the previous/next buttons to look like actual buttons.
The .sectionheader class will style the -PreContent text to turn red when you hover over it, and to make
the cursor into a pointer, suggesting that the content can be clicked.
The .grid class makes the tables full-width.
The last thing in the script you need to know about is this:
$params = @{'CssStyleSheet'=$style;
'Title'="System Report for $computer";
'PreContent'="<h1>System Report for $computer</h1>";
'CssIdsToMakeDataTables'=@('tableProc','tableNIC','tableSvc');
'HTMLFragments'=@($html_os,$html_cs,$html_pr,$html_sv,$html_na)}
ConvertTo-EnhancedHTML @params |
Out-File -FilePath $filepath
This takes all of the HTML fragments (which Ive been storing in variables $html_os, $html_cs, and so forth) and
combines them into the final HTML page. This is done using ConvertTo-EnhancedHTML, and its output is piped
into a text file.
-CssStyleSheet accepts the complete style sheet text for embedding. Alternately, if you have a style
sheet hosted on a Web server and want to point to that, you could use -CssUri instead.
-Title sets the window title bar text for the page.
-PreContent is the overall report header.
-CssIdsToMakeDataTables is an array of CSS IDs assigned to <TABLE> tags, telling the command
which tables are to be made interactive. My list corresponds to the three tables for which I provided a
-TableCssID parameter.
-HTMLFragments is an array of the actual HTML fragments, which were created with ConvertTo-
EnhancedHTMLFragment earlier.
The result is what you saw at the outset of this section. ConvertTo-EnhancedHTML and ConvertTo-
EnhancedHTMLFragment provide additional options, and you can use PowerShells Help system to see them all. I
suggest running help ConvertTo-EnhancedHTML -full to see everything, since the descriptions for the individual
parameters contain most of the useful information.
Creating HTML Reports in PowerShell
39
http://PowerShellBooks.com http://PowerShell.org
Formatting Individual Table Cells
Note that the January 2013 edition of this book introduced a substantially rewritten EnhancedHTML module. It is
backward compatible with the old version, but provides new features - as discussed in this chapter - and was
internally rewritten to support those features. You should find this new module included in the ZIP file with this
book. The original EnhancedHTML module is included as OldEnhancedHTML.psm1, just for reference; theres no
need to copy that to your actual production system.
Lets do some more. Im going to create a new demo script named HTMLDemo3.ps1. Its basically the same as the
one Ive been using up to now, but Im adding a new section for disk information. This starts by querying the disk
info:
function Get-InfoDisk {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)][string]$ComputerName
)
$drives = Get-WmiObject -class Win32_LogicalDisk -ComputerName $ComputerName `
-Filter "DriveType=3"
foreach ($drive in $drives) {
$props = @{'Drive'=$drive.DeviceID;
'Size'=$drive.size / 1GB -as [int];
'Free'="{0:N2}" -f ($drive.freespace / 1GB);
'FreePct'=$drive.freespace / $drive.size * 100 -as [int]}
New-Object -TypeName PSObject -Property $props
}
}
Im then turning that into an HTML fragment. Notice that Ive added a -Properties parameter:
$params = @{'As'='Table';
'PreContent'='<h2>&diams; Local Disks</h2>';
'TableCssID'='tableDisk';
'DivCssID'='divDisk';
'EvenRowCssClass'='even';
'OddRowCssClass'='odd';
'MakeHiddenSection'=$true;
'TableCssClass'='grid';
'Properties'='Drive',
@{n='Size(GB)';e={$_.Size}},
@{n='Free(GB)';e={$_.Free};css={if ($_.FreePct -lt 80) { 'red' }}},
@{n='Free(%)';e={$_.FreePct};css={if ($_.FreeePct -lt 80) { 'red' }}}}
$html_dr = Get-InfoDisk -ComputerName $computer |
ConvertTo-EnhancedHTMLFragment @params |
Out-String
That -Properties parameter works a lot like the one on Select-Object and Format-Table. It accepts simple strings as
property names, such as Drive. It also accepts hashtables.
The hashtable must include a key named n, name, l, or label, which specifies the column header.
The hashtable must also include a key named e or expression, which is a script block that defines the content of
the column.
Optionally, the hashtable may include a css or cssclass key. This accepts a script block that may output a string. If
it does, this string will be included as a CSS class for the associated table cell (the HTML <TD> tag).
http://PowerShellBooks.com http://PowerShell.org
What Ive done here is said that, for both the Free(GB) and Free(%) columns, assign the CSS class red to the
table cell if the free percentage is less than 80%. To support this, Ive defined the .red CSS class in my CSS style
sheet:
.red {
color:red;
font-weight:bold;
}

The result looks like this in the HTML report:
I know that setting an alert on 80% free space seems weird, but I only did so to make sure that Id get a red result
in the report, just to show you what it looks like.
With this capability, you can dynamically control the appearance of individual table cells.
Creating HTML Reports in PowerShell
41
http://PowerShellBooks.com http://PowerShell.org
Combining HTML Reports
and a GUI Application
Ive had a number of folks ask questions in the forums at PowerShell.org, with the theme of how can I use a
RichTextBox in a Windows GUI application to display nicely formatted data? My answer is dont. Use HTML
instead. For example, lets say you followed the examples in the previous chapter and produced a beautiful HTML
report. Keep in mind that the report stays in memory, not in a text file, until the very end:
$params = @{'CssStyleSheet'=$style;
'Title'="System Report for $computer";
'PreContent'="<h1>System Report for $computer</h1>";
'CssIdsToMakeDataTables'=@('tableProc','tableNIC','tableSvc');
'HTMLFragments'=@($html_os,$html_cs,$html_pr,$html_sv,$html_na)}
ConvertTo-EnhancedHTML @params |
Out-File -FilePath $filepath
For the sake of illustration, lets say thats now in a file named C:\Report.html. Im going to use SAPIENs
PowerShell Studio 2012 to display that report in a GUI, rather than popping it up in a Web browser. Here, Ive
started a simple, single-form project. Ive changed the text of the form to Report, and Ive added a WebBrowser
control from the toolbox. That control automatically fills the entire form, which is perfect. I named the WebBrowser
control web, which makes it accessible from code via the variable $web.
http://PowerShellBooks.com http://PowerShell.org
I expect youd make a form like this part of a larger overall project, but Im just focusing on how to do this one bit.
So Ill have the report load into the WebBrowser control when this form loads:
$OnLoadFormEvent={
#TODO: Initialize Form Controls here
$web.Navigate('file://C:\report.html')
}
Now I can run the project:
Creating HTML Reports in PowerShell
43
http://PowerShellBooks.com http://PowerShell.org
I get a nice pop-up dialog that displays the HTML report. I can resize it, minimize it, maximize it, and close it using
the standard buttons on the windows title bar. Easy, and it only took 5 minutes to create.
http://PowerShellBooks.com http://PowerShell.org
Contacting Me
If youre having problems, want to do something and cant figure out how, found a bug and want to offer a
correction, or just have feedback on this guide or the EnhancedHTML module, Id love to hear from you. The
easiest way is to post in the General Q&A forum on http://PowerShell.org/discuss. I keep a pretty close eye on
that, and Ill respond as soon as Im able.
Do check back on http://PowerShellBooks.com from time to time, to make sure youve got the most recent version
of this guide and its code.
Creating HTML Reports in PowerShell
45
http://PowerShellBooks.com http://PowerShell.org

S-ar putea să vă placă și