Create an HTML PowerShell Help Page

Yesterday I posted an article about getting the online url for a cmdlet help topic. Today I want to demonstrate how we might take advantage of this piece of information.

Since the link is already in the form of a URL, wouldn’t it make sense to put this in an HTML document? At first glance, you might take the command from yesterday and pipe it to ConvertTo-HTML.


Get-Command -CommandType cmdlet | Get-Help |
Select Name,Synopsis,@{Name="URI";Expression={
($_.RelatedLinks | select -ExpandProperty NavigationLink | where {$_.uri}).uri}} |
Where {$_.URI} | ConvertTo-HTML -Title "Help Links" | Out-File c:\work\pshelp.htm

This code will work just fine. But if you look at the resulting file, we don’t have a link. I suppose it would be nice if ConvertTo-HTML could auto-detect URLs and automatically add a link, but it looks like we’ll have to do it. We could probably use a custom hash table to insert the HTML Anchor tags so here’s the first attempt:


Get-Command -CommandType cmdlet | Get-Help | Where {$_.RelatedLinks} |
Select Name,Synopsis,@{Name="URI";Expression={
#add the link tags as part of the output!
$link=$_.RelatedLinks | select -ExpandProperty NavigationLink | where {$_.uri}
if ($link.uri) {
$uri=$link.uri
Write "$uri"
}
else {
#no link so write a null
write $Null
}
}} | Where {$_.URI} | ConvertTo-HTML -Title "Help Links" |
Out-File c:\work\pshelp.htm

One additional change I made was to filter out cmdlets with no related links. Then in the expression scriptblock I can create a new value based on the URI value if it exists. But there is a problem with this, which you’ll see immediately if you run this code. ConvertTo-HTML sees the value of my new URI property and escapes the < and > characters.

That doesn’t help. But this is where the fact that ConvertTo-HTML only creates HTML code because we can parse the code and do a simple replace. Let me jump ahead and pull part of the finished dessert from the oven.


Function Convert-HTMLEscape {

<# convert < and > to < and >
It is assumed that these will be in pairs
#>

[cmdletbinding()]

Param (
[Parameter(Position=0,ValueFromPipeline=$True)]
[string[]]$Text
)

Process {
foreach ($item in $text) {
if ($item -match "<") {
<# replace codes with actual symbols This line is a shortcut to do two replacements with one line of code. The code in the first set of parentheses revised text with "<". This normally gets written to the pipeline. By wrapping it in parentheses it tells PowerShell to treat it as an object so I can then call the Replace() method again and add the >.
#>
($item.Replace("<","<")).Replace(">",">")
}
else {
#otherwise just write the line to the pipeline
$item
}
}
} #close process

} #close function

This function takes string input, presumably from ConvertTo-HTML and replaces the HTML escapes with the “real” characters. This I can use to write to the file.


...| Where {$_.URI} | ConvertTo-HTML -Title "Help Links" | Convert-HTMLEscape | Out-File c:\work\pshelp.htm

One of the reasons I put this in a function is to make it re-usable for future projects where I might need to escape these characters again. We’re getting closer. One last thing before we ice the final dessert: I have been using an expression to get all cmdlet help. But I like flexibility. What if tomorrow I only want a page with New* cmdlets from PowerCLI? So once again, I took my code that works just fine from a prompt into a more flexible and re-usable function.

Function Get-HelpUri {

[cmdletbinding()]

Param(
[Parameter(Position=0,Mandatory=$True,HelpMessage="Enter a cmdlet name",
ValueFromPipeline=$True,ValueFromPipelinebyPropertyName=$True)]
[ValidateNotNullorEmpty()]
[string]$Name
)

Process {
Write-Verbose "Processing $name"
Get-Help $name | Where {$_.RelatedLinks} |
Select Name,Synopsis,@{Name="URI";Expression={
#add the link tags as part of the output!
$link=$_.RelatedLinks | select -ExpandProperty NavigationLink | where {$_.uri}
if ($link.uri) {
$uri=$link.uri
Write "$uri"
}
else {
#no link so write a null
write $Null
}
}}
} #close process

} #close function

This will write a custom object with the cmdlet name, synopsis and URI property with the HTML code. I still need to convert to HTML and then fix the tags but I can verify it works.


PS C:\> get-command new-object | get-helpuri | ConvertTo-Html |Convert-HTMLEscape



HTML TABLE

Name Synopsis URI
New-Object Creates an instance of a Microsoft .NET Framework or COM object. http://go.microsoft.com/fwlink/?LinkID=11335
5


The icing is to include some style via a CSS file. Here’s a short script on how I might build a file for all of the cmdlets in my current session.


Write-Host "Building cmdlet help report" -ForegroundColor Green

#the file to create
$file="c:\work\cmdletonline.htm"

#be sure to change the path to the CSS file if you want to use it
$cssPath="c:\scripts\blue.css"

#optional image
$imagePath="c:\work\talkbubble.gif"

#some pre content
$preContent=@"



Cmdlet Online Help




"@

#some post content
$postContent=@"

Help for cmdlets found on $env:computername on $(Get-Date)
"@

#a title for the report
$Title="Cmdlet Help"

<# Get all cmdlets in the current session, send them to the Get-HelpURI function to parse out help URLS, filter out those without a link, pass the remaining to the Convertto-HTML to generate HTML code which is piped to my function to replace < with > and the final HTML code is piped to
a file.
#>
Get-Command -CommandType cmdlet | Get-HelpURI | Where {$_.URI} |
ConvertTo-Html -PreContent $PreContent -PostContent $postContent -Title $Title -cssUri $cssPath |
Convert-HTMLEscape | Out-File -FilePath $file -Encoding ASCII

Write-Host "Finished. See $file for the results" -ForegroundColor Green

And here’s the final result in Internet Explorer.

Ok, maybe you don’t have a compelling need for this exact script, but I hope you picked up on the importance of writing code for re-use and taking advantage of the pipeline.

If you’d like to try my code out for yourself, including the graphic and CSS file, download this zip file.

Create HTML Bar Charts from PowerShell

I saw a very nice mention on Twitter today where someone had taken an idea of mine and created something practical and in production. It is always nice to hear. The inspiring article was something I worked up that showed using the PowerShell console as a graphing tool. Of course someone immediately wanted to know about turning this into HTML. Actually he wanted this in a web page that would automatically refresh. I don’t think I can manage that, but I came up with a demo script that will create a colorized bar graph in ah HTML page. If you want to update it, simply re-run your command.

Here’s the demo code.


#requires -version 2.0

Param (
[string[]]$computers=@($env:computername),
[string]$Path="drivereport.htm"
)

$Title="Drive Report"

#embed a stylesheet in the html header
$head = @"

$Title

"@

#define an array for html fragments
$fragments=@()

#get the drive data
$data=get-wmiobject -Class Win32_logicaldisk -filter "drivetype=3" -computer $computers

#group data by computername
$groups=$Data | Group-Object -Property SystemName

#this is the graph character
[string]$g=[char]9608

#create html fragments for each computer
#iterate through each group object

ForEach ($computer in $groups) {

$fragments+="

$($computer.Name)

"

#define a collection of drives from the group object
$Drives=$computer.group

#create an html fragment
$html=$drives | Select @{Name="Drive";Expression={$_.DeviceID}},
@{Name="SizeGB";Expression={$_.Size/1GB -as [int]}},
@{Name="UsedGB";Expression={"{0:N2}" -f (($_.Size - $_.Freespace)/1GB) }},
@{Name="FreeGB";Expression={"{0:N2}" -f ($_.FreeSpace/1GB) }},
@{Name="Usage";Expression={
$UsedPer= (($_.Size - $_.Freespace)/$_.Size)*100
$UsedGraph=$g * ($UsedPer/2)
$FreeGraph=$g* ((100-$UsedPer)/2)
#I'm using place holders for the < and > characters
"xopenFont color=Redxclose{0}xopen/FontxclosexopenFont Color=Greenxclose{1}xopen/fontxclose" -f $usedGraph,$FreeGraph
}} | ConvertTo-Html -Fragment

#replace the tag place holders. It is a hack but it works.
$html=$html -replace "xopen","<" $html=$html -replace "xclose",">"

#add to fragments
$Fragments+=$html

#insert a return between each computer
$fragments+="
"

} #foreach computer

#add a footer
$footer=("
Report run {0} by {1}\{2}" -f (Get-Date -displayhint date),$env:userdomain,$env:username)
$fragments+=$footer

#write the result to a file
ConvertTo-Html -head $head -body $fragments | Out-File $Path

The key concept here is that I’m building a final HTML file from a collection of HTML fragments. I think most of the script should be self-explanatory. The core part of the script takes the collection of drives for a computer, selects some custom properties and converts it into an HTML fragment.


#create an html fragment
$html=$drives | Select @{Name="Drive";Expression={$_.DeviceID}},
@{Name="SizeGB";Expression={$_.Size/1GB -as [int]}},
@{Name="UsedGB";Expression={"{0:N2}" -f (($_.Size - $_.Freespace)/1GB) }},
@{Name="FreeGB";Expression={"{0:N2}" -f ($_.FreeSpace/1GB) }},
@{Name="Usage";Expression={
$UsedPer= (($_.Size - $_.Freespace)/$_.Size)*100
$UsedGraph=$g * ($UsedPer/2)
$FreeGraph=$g* ((100-$UsedPer)/2)
#I'm using place holders for the < and > characters
"xopenFont color=Redxclose{0}xopen/FontxclosexopenFont Color=Greenxclose{1}xopen/fontxclose" -f $usedGraph,$FreeGraph
}} | ConvertTo-Html -Fragment

The tricky part is the value for “Usage” In the Expression scriptblock I’m calculating a value for how much disk space is used and how much is free. I then create a string that is my graphing character ([CHAR]9608) times a value. The value is the corresponding percentage divided by 2 because I want to put the used graphic right next to the free graphic. And this leads me to the hack: I want the used graph to be in Red and the free graph string to be in Green. I’m taking the easy way and using a FONT tag. But, I can’t use the < and > characters because Convertto-HTML “translates” them. I couldn’t find anyway around this. So instead I use a place holder.


"xopenFont color=Redxclose{0}xopen/FontxclosexopenFont Color=Greenxclose{1}xopen/fontxclose" -f $usedGraph,$FreeGraph

All I need to do is replace the placeholders from the HTML output.


#replace the tag place holders. It is a hack but it works.
$html=$html -replace "xopen","<" $html=$html -replace "xclose",">"

#add to fragments
$Fragments+=$html

Perhaps not the most elegant solution, but it works. The script wraps up by taking all of the HTML code and creates the final file.


ConvertTo-Html -head $head -body $fragments | Out-File $Path

To run this for a group of computers I can run a command like:


PS C:\scripts> .\demo-HtmlBarChart.ps1 "jdhit-dc01","serenity","quark"

Here is the result:

I haven’t found a way to make this process more generic and my script, while it works, isn’t really production ready; that’s why I call it a demo script. But perhaps you’ll take inspiration and come up with some wonderful. If you do, I hope you’ll let me know and share with the PowerShell community.

Download Demo-HtmlBarChart.

The PowerShell Morning Report

ZazuI love how easy it is to manage computers with Windows PowerShell. It is a great reporting tool, but often I find people getting locked into one approach. I’m a big believer in flexibility and re-use and using objects in the pipeline wherever I can. So I put together a PowerShell script that I can run every morning on my computer and get a quick summary about what is happening, or perhaps not happening. My script, MorningReport.ps1, relies on WMI to gather a variety of system information. By default it connects to the local host, but I’ve provided a computername parameter. I’m assuming current credentials are good enough for any remote system, but you can always add support for alternate credentials, assuming I don’t in some future version.

By default the script writes a custom object to the pipeline that contains all of the other WMI information like disk utilization, service status, and event logs. But I wanted flexibility and ease of use, so the script also supports parameters of -Text and -HTML. The former creates nicely formatted text suitable for printing or saving to a file. The latter creates HTML code from the original objects. I rely heavily on creating HTML fragments with ConvertTo-HTML and then assembling everything at the end. The script writes the HTML code to the pipeline so if you want to save results to a file, simply pipe to Out-File. I did this because there may be times when you want the “raw” HTML code. You might want to save the HTML and create an HTML mail message with Send-MailMessage. Or maybe further tweak the HTML before saving it to a file. Again, I didn’t want to lock myself in. Here’s the main part of the script.


[cmdletbinding(DefaultParameterSetName="object")]

Param(
[Parameter(Position=0,ValueFromPipeline=$True)]
[ValidateNotNullOrEmpty()]
[string]$Computername=$env:computername,
[ValidateNotNullOrEmpty()]
[alias("title")]
[string]$ReportTitle="System Report",
[ValidateScript({$_ -ge 1})]
[int]$Hours=24,
[Parameter(ParameterSetName="HTML")]
[switch]$HTML,
[Parameter(ParameterSetName="TEXT")]
[switch]$Text
)

#script internal version number used in output
[string]$reportVersion="1.0.8"

<# define some HTML style here's a source for HTML color codes http://www.immigration-usa.com/html_colors.html the code must be left justified #>
$head = @"

$ReportTitle
"@

If ($computername -eq $env:computername) {
#local computer so no ping test is necessary
$OK=$True
}
elseIf (($computername -ne $env:computername) -AND (Test-Connection -ComputerName $computername -quiet -Count 2)) {
#not local computer and it can be pinged so proceed
$OK=$True
}

If ($OK) {

Try {
$os=Get-WmiObject Win32_operatingSystem -ComputerName $computername -ErrorAction Stop
#set a variable to indicate WMI can be reached
$wmi=$True
}
Catch {
Write-Warning "WMI failed to connect to $($computername.ToUpper())"
}

if ($wmi) {
Write-Host "Preparing morning report for $($os.CSname)" -ForegroundColor Cyan

#OS Summary
Write-Host "...Operating System" -ForegroundColor Cyan
$osdata=$os | Select @{Name="Computername";Expression={$_.CSName}},
@{Name="OS";Expression={$_.Caption}},
@{Name="ServicePack";Expression={$_.CSDVersion}},
free*memory,totalv*,NumberOfProcesses,
@{Name="LastBoot";Expression={$_.ConvertToDateTime($_.LastBootupTime)}},
@{Name="Uptime";Expression={(Get-Date) - ($_.ConvertToDateTime($_.LastBootupTime))}}

#Computer system
Write-Host "...Computer System" -ForegroundColor Cyan
$cs=Get-WmiObject -Class Win32_Computersystem -ComputerName $computername
$csdata=$cs | Select Status,Manufacturer,Model,SystemType,Number*

#services
Write-Host "...Services" -ForegroundColor Cyan
#get services via WMI and group into a hash table
$wmiservices=Get-WmiObject -class Win32_Service -ComputerName $computername
$services=$wmiservices | Group State -AsHashTable

#get services set to auto start that are not running
$failedAutoStart=$wmiservices | Where { ($_.startmode -eq "Auto") -AND ($_.state -ne "Running")}

#Disk Utilization
Write-Host "...Logical Disks" -ForegroundColor Cyan
$disks=Get-WmiObject -Class Win32_logicaldisk -Filter "Drivetype=3" -ComputerName $computername
$diskData=$disks | Select DeviceID,
@{Name="SizeGB";Expression={$_.size/1GB -as [int]}},
@{Name="FreeGB";Expression={"{0:N2}" -f ($_.Freespace/1GB)}},
@{Name="PercentFree";Expression={"{0:P2}" -f ($_.Freespace/$_.Size)}}

#NetworkAdapters
Write-Host "...Network Adapters" -ForegroundColor Cyan
#get NICS that have a MAC address only
$nics=Get-WmiObject -Class Win32_NetworkAdapter -filter "MACAddress Like '%'" -ComputerName $Computername
$nicdata=$nics | Foreach {
$tempHash=@{Name=$_.Name;DeviceID=$_.DeviceID;AdapterType=$_.AdapterType;MACAddress=$_.MACAddress}
#get related configuation information
$config=$_.GetRelated() | where {$_.__CLASS -eq "Win32_NetworkadapterConfiguration"}
#add to temporary hash
$tempHash.Add("IPAddress",$config.IPAddress)
$tempHash.Add("IPSubnet",$config.IPSubnet)
$tempHash.Add("DefaultGateway",$config.DefaultIPGateway)
$tempHash.Add("DHCP",$config.DHCPEnabled)
#convert lease information if found
if ($config.DHCPEnabled -AND $config.DHCPLeaseObtained) {
$tempHash.Add("DHCPLeaseExpires",($config.ConvertToDateTime($config.DHCPLeaseExpires)))
$tempHash.Add("DHCPLeaseObtained",($config.ConvertToDateTime($config.DHCPLeaseObtained)))
$tempHash.Add("DHCPServer",$config.DHCPServer)
}

New-Object -TypeName PSObject -Property $tempHash

}

#Event log errors and warnings in the last $Hours hours
$last=(Get-Date).AddHours(-$Hours)
#System Log
Write-Host "...System Event Log Error/Warning since $last" -ForegroundColor Cyan
$syslog=Get-EventLog -LogName System -ComputerName $computername -EntryType Error,Warning -After $last
$syslogdata=$syslog | Select TimeGenerated,EventID,Source,Message

#Application Log
Write-Host "...Application Event Log Error/Warning since $last" -ForegroundColor Cyan
$applog=Get-EventLog -LogName Application -ComputerName $computername -EntryType Error,Warning -After $last
$applogdata=$applog | Select TimeGenerated,EventID,Source,Message

} #if wmi is ok

#write results depending on parameter set
$footer="Report v{3} run {0} by {1}\{2}" -f (Get-Date),$env:USERDOMAIN,$env:USERNAME,$reportVersion

if ($HTML) {
#prepare HTML code
$fragments=@()
#insert a graphic header with one of the two following lines
#$fragments+="

"
$fragments+="Zazu

The Morning Report

"

#insert navigation bookmarks
$nav=@"
Services
Failed Auto Start
Disks
Network
System Log
Application Log
"@
$fragments+=$nav
$fragments+="
"

#add a link to the document top
$nav+="`nTop"

$fragments+="

System Summary

"
$fragments+=$osdata | ConvertTo-Html -as List -Fragment
$fragments+=$csdata | ConvertTo-Html -as List -Fragment
$fragments+=ConvertTo-Html -Fragment -PreContent "

Services

"
$services.keys | foreach {
$fragments+= ConvertTo-Html -Fragment -PreContent "

$_

"
$fragments+=$services.$_ | Select Name,Displayname,StartMode| ConvertTo-HTML -Fragment
#insert navigation link after each section
$fragments+=$nav
}

$fragments+=$failedAutoStart | Select Name,Displayname,StartMode,State |
ConvertTo-Html -Fragment -PreContent "

Failed Auto Start

"
$fragments+=$nav

$fragments+=$diskdata | ConvertTo-HTML -Fragment -PreContent "

Disk Utilization

"
$fragments+=$nav

#convert nested object array properties to strings
$fragments+=$nicdata | Select Name,DeviceID,DHCP*,AdapterType,MACAddress,
@{Name="IPAddress";Expression={$_.IPAddress | Out-String}},
@{Name="IPSubnet";Expression={$_.IPSubnet | Out-String}},
@{Name="IPGateway";Expression={$_.DefaultGateway | Out-String}} |
ConvertTo-HTML -Fragment -PreContent "

Network Adapters

"
$fragments+=$nav

$fragments+=$syslogData | ConvertTo-HTML -Fragment -PreContent "

System Event Log Summary

"
$fragments+=$nav

$fragments+=$applogData | ConvertTo-HTML -Fragment -PreContent "

Application Event Log Summary

"
$fragments+=$nav

Write $fragments | clip
ConvertTo-Html -Head $head -Title $ReportTitle -PreContent ($fragments | out-String) -PostContent "
$footer"
}
elseif ($TEXT) {
#prepare formatted text
$ReportTitle
"-"*($ReportTitle.Length)
"System Summary"
$osdata | Out-String
$csdata | format-List | Out-String
Write "Services"
$services.keys | foreach {
$services.$_ | Select Name,Displayname,StartMode,State
} | Format-List | Out-String
Write "Failed Autostart Services"
$failedAutoStart | Select Name,Displayname,StartMode,State
Write "Disk Utilization"
$diskdata | Format-table -AutoSize | Out-String
Write "Network Adapters"
$nicdata | Format-List | Out-String
Write "System Event Log Summary"
$syslogdata | Format-List | Out-String
Write "Application Event Log Summary"
$applogdata | Format-List | Out-String
Write $Footer
}
else {
#Write data to the pipeline as part of a custom object

New-Object -TypeName PSObject -Property @{
OperatingSystem=$osdata
ComputerSystem=$csdata
Services=$services.keys | foreach {$services.$_ | Select Name,Displayname,StartMode,State}
FailedAutoStart=$failedAutoStart | Select Name,Displayname,StartMode,State
Disks=$diskData
Network=$nicData
SystemLog=$syslogdata
ApplicationLog=$applogdata
ReportVersion=$reportVersion
RunDate=Get-Date
RunBy="$env:USERDOMAIN\$env:USERNAME"
}
}

} #if OK

else {
#can't ping computer so fail
Write-Warning "Failed to ping $computername"
}

As you can see, it is a lengthy script, but I’ve tried to include a fair amount of internal comments and documentation, so I won’t repeat it here, but I will touch on a few key points.

First, I’m embedding a style sheet directly in any HTML output so any files I create can stand alone. You could certainly modify the relevant sections and use the -CSSUri parameter with ConvertTo-HTML. Next, I use a combination of Test-Connection and Try/Catch to handle computers that are offline or I can’t access. This speeds up the script and makes it nicer for the script user. Assuming all is good, I create a number of variables that hold the WMI information I am interested in.


...
#OS Summary
Write-Host "...Operating System" -ForegroundColor Cyan
$osdata=$os | Select @{Name="Computername";Expression={$_.CSName}},
@{Name="OS";Expression={$_.Caption}},
@{Name="ServicePack";Expression={$_.CSDVersion}},
free*memory,totalv*,NumberOfProcesses,
@{Name="LastBoot";Expression={$_.ConvertToDateTime($_.LastBootupTime)}},
@{Name="Uptime";Expression={(Get-Date) - ($_.ConvertToDateTime($_.LastBootupTime))}}

#Computer system
Write-Host "...Computer System" -ForegroundColor Cyan
$cs=Get-WmiObject -Class Win32_Computersystem -ComputerName $computername
$csdata=$cs | Select Status,Manufacturer,Model,SystemType,Number*
...

After everything is collected, then I can write output using an If/ElseIf statement depending on what parameters were passed. If no parameters were specified then a custom object is assembled and written to the pipeline.


else {
#Write data to the pipeline as part of a custom object

New-Object -TypeName PSObject -Property @{
OperatingSystem=$osdata
ComputerSystem=$csdata
Services=$services.keys | foreach {$services.$_ | Select Name,Displayname,StartMode,State}
FailedAutoStart=$failedAutoStart | Select Name,Displayname,StartMode,State
Disks=$diskData
Network=$nicData
SystemLog=$syslogdata
ApplicationLog=$applogdata
ReportVersion=$reportVersion
RunDate=Get-Date
RunBy="$env:USERDOMAIN\$env:USERNAME"
}

But perhaps you’d like to see this in action. Here’s a sample HTML SampleReport. In this report, there are no recent errors or warnings in the Application event log.

Or if you have a few minutes, here’s a short clip of the script in action.

I hope you find this a useful jumping off point for your own script, although I think I’ve already worked out information you are most likely interested in. Download MorningReport.ps1 and try it out for yourself.

Update: Read about a revised version that accepts pipelined input.

New Event Report Revised

Last year I posted an update to an old Mr. Roboto script that was an update to an even older VBScript. Still with me? My last revision leveraged the new Get-WinEvent cmdlet to create an HTML report of recent error activity on one or more computers. The problem was that I didn’t account for older computers that don’t support Get-WinEvent. I finally have a version that does. Continue reading

New Event Report

For a number of years I wrote the popular Mr. Roboto column for REDMOND magazine. When I first started the column, many of my scripts were written in VBScript. Then as PowerShell came along that became the preferred tool. Over time I realized there were some VBScripts that could be rewritten and even improved using PowerShell. One of them was a script that queried event logs on computers for errors and warnings in the last X number of hours and prepared a colorized HTML report. I revised that script to a PowerShell v1 script. Recently, a loyal reader asked about revising the script again for PowerShell 2.0. That seemed reasonable so here is my revision. Continue reading