Today's post is about a niche problem or something that maybe you never considered before. And while I will share a finished PowerShell function, you may want to create your own tooling based on the techniques and concepts. The problem begins with a command like this:
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Get-HotFix -ComputerName $env:computername | Sort-Object Description,InstalledOn -Descending | Select-Object Description,HotfixID,Caption,InstalledBy,InstalledOn
And suppose you want to create an HTML report with this information which you can do by piping this to ConvertTo-HTML and then Out-File. The result is adequate.
Yes, I know I can make it pretty by applying a style but that won't help one drawback. The links under the Caption column aren't actual hyperlinks. Wouldn't it be nice if they were? In fact, you may have data from any number of other sources that include a link that you want to turn into an html page. Here's one approach you might take.
First, I'm going to save my hotfix data as an HTML fragment.
$h = Get-HotFix -ComputerName $env:computername | Sort-Object Description,InstalledOn -Descending | Select-Object Description,HotfixID,Caption,InstalledBy,InstalledOn | ConvertTo-Html -Fragment
What I need to do is change the table definitions for the caption to include an <a href=> tag. What I need to do is change text like this:
<td>http://support.microsoft.com/?kbid=4509096</td>
Into this:
<td><a href="http://support.microsoft.com/?kbid=4509096">http://support.microsoft.com/?kbid=4509096></a></td>
This sounds like a good use case for regular expressions. Here's a pattern to match on a url.
[regex]$rx = "http(s)?:\/\/[a-zA-Z0-9\.\/\?\%=&\$-]+"
Yes, I know there are many ways to define a regular expression pattern, but this will suffice for my examples.
My approach is to go through each line of the HTML and there is a matching URL replace it with the necessary additional tags. To keep it simple, I'll create a new here string which will eventually be the body of the HTML document.
[string]$out = @" <H1>$env:computername</H1> "@
Next, I'll go through each line of the HTML stored in $h. If the line matches on the regex object I will get the matching value and save it to a variable. With that I can build the replacement text with the link. The last step is to replace the line with the link. This has the effect of replacing just the http string with the new link.
foreach ($line in $h) { if ($rx.IsMatch($line)) { $value = $rx.Match($line).value $link = "<a href = ""$value"" target = ""_blank"">$value</a>" $out += $rx.Replace($line, $link) } else { #just add the line $out += $line } }
Each new line is added to the here string. If the line doesn't match the regular expression, I just add it as is to the here string, $out.
You can see that the <td> element with the URL now has a hyperlink. Al that remains is to turn it into an HTML document.
$head = @" <title>Hot Fix Report</title> <style> table { font-family:"Trebuchet MS", Arial, Helvetica, sans-serif; border-collapse:collapse; } td { font-size:1em; border:1px solid #98bf21; padding:5px 5px 5px 5px; } th { font-size:1.1em; text-align:center; padding-top:5px; padding-bottom:5px; padding-right:7px; padding-left:7px; background-color:#A7C942; color:#ffffff; } name tr { color: #060606; background-color:#EAF2D3; } </style> "@ Convertto-Html -head $head -body $out -PostContent "<h5><i>report run $(Get-Date) </i></h5>" | out-file D:\temp\hotfix.html
I'm embedding a style sheet into the document.
Of course, what would make this even easier is a function.
Function ConvertTo-HTML2 { [cmdletbinding()] [outputtype([system.string])] Param( [Parameter(Mandatory, ValueFromPipeline)] [object]$InputObject, [switch]$Fragment, [ValidateSet("Table", "List")] [string]$As, [string[]]$Head, [string]$Title, [uri]$CSSUri, [string[]]$PostContent, [string[]]$PreContent, [object[]]$Property ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" [regex]$rx = "http(s)?:\/\/[a-zA-Z0-9\.\/\?\%=&\$-]+" $in = @() $processing = $False } #begin Process { if (-Not $processing) { #only display this message once Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Storing Data " } $in += $InputObject $processing = $True } #process End { Write-Verbose "[$((Get-Date).TimeofDay) END ] Converting input to html fragment" [void]$PSBoundParameters.remove("InputObject") $h = $in | ConvertTo-Html @PSBoundParameters [string]$out = @" "@ foreach ($line in $h) { if ($rx.IsMatch($line) -AND ($line -notmatch "DOCTYPE html PUBLIC") -AND ($line -notmatch "html xmlns")) { $value = $rx.Match($line).value $link = "<a href = ""$value"" target = ""_blank"">$value</a>" $out += $rx.Replace($line, $link) } else { #just add the line $out += $line } } $out Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" } #end } #close ConvertTo-HTML2
This function is essentially a wrapper for ConvertTo-HTML. The parameters are the same so that I can simply splat PSBoundParameters to ConvertTo-HTML. The function gets all of the input and then in the End block goes through the regex replacing. The function ends by writing HTML to the pipeline like ConvertTo-HTML. But now I can run a command like:
Get-HotFix -ComputerName $env:computername | Sort-Object Description, InstalledOn -Descending | ConvertTo-HTML2 -head $head -property Description, HotfixID, Caption, InstalledBy, InstalledOn -PreContent "<H1>$env:computername</H1>" -PostContent "<h5><i>report run $(Get-Date) </i></h5>" | Out-File D:\temp\t.html
to create the same report. This example is using the $head variable from before. I can use this function with anything that has an http type link.
Here's a function I have to get an RSS feed.
Function Get-RSSFeed { Param ( [Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [ValidatePattern("^http(s)?:\/\/")] [Alias('url')] [string[]]$Path = "https://jdhitsolutions.com/blog/feed" ) Begin { Write-Verbose -Message "Starting $($MyInvocation.Mycommand)" } #begin Process { foreach ($item in $path) { $data = Invoke-RestMethod -Uri $item foreach ($entry in $data) { #link might be different names if ($entry.origLink) { $link = $entry.origLink } elseif ($entry.link) { $link = $entry.link } else { $link = "unknown" } #clean up description #hash table of HTML codes #http://www.ascii.cl/htmlcodes.htm $decode = @{ '<(.|\n)+?>' = "" '’' = "'" '“' = '"' '”' = '"' '»' = "..." '–' = "--" '…' = "..." '–' = "@" ' ' = " " } #description could be an XML element or a simple string if ($entry.description -is [System.Xml.XmlElement]) { $description = $entry.description.innertext } else { $description = $entry.description } foreach ($key in $decode.keys) { [regex]$rgx = $key $description = $rgx.Replace($description, $decode.Item($key)).Trim() } #create a custom object [pscustomobject]@{ Title = $entry.title Author = $entry.creator.innertext Description = $description Published = $entry.pubDate -as [datetime] Category = $entry.category.'#cdata-section' Link = $Link } #hash } #foreach entry } #foreach item } #process End { Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" } #end } #end Get-RSSFeed
I can use it to create an HTML report with links.
$cparams = @{ Title = "The Lonely Administrator" CSSUri = "C:\scripts\samplecss\sample2.css" Property = "Title","Description","Published","Link" As = "table" } Get-RSSFeed | ConvertTo-HTML2 @cparams | Out-File d:\temp\rss.html
I hope you'll give the code snippets a try for yourself. And if you feel regular expressions are too hard for you, stay tuned. I am wrapping up a Pluralsight course on PowerShell and Regular Expressions that hopefully will help take away the mystery.
1 thought on “Creating Linked HTML with PowerShell”
Comments are closed.