开发者

Technique for selectively formatting data in a PowerShell pipeline and output as HTML

Say that you want to do some fancy formatting of some tabular output from powershell, and the destination is to be html (either for a webserver, or to be sent in an email). Let's say for example that you want certain numeric values to have a different background color. Whatever. I can think of two solid programmatic ways to accomplish this: output XML and transform with XSLT, or output HTML and decorate with CSS.

XSLT is probably the harder of the two (I say that because I don't know it), but from what little I recall, it has the benefit of bring able to embed the selection criteria (xpath?) for aforementioned fancy formatting. CSS on the other hand needs a helping hand. If you wanted a certain cell to be treated specially, then you would need to distinguish it from its siblings with a class, id, or something along those lines. PowerShell doesn't really have a way to do that natively, so that would mean parsing the HTML as it leaves convertto-html and adding, 开发者_C百科for example, a "emphasis" class:

<td class="emphasis">32MB</td>

I don't like the idea of the required text parsing, especially given that I would rather be able to somehow emphasize what needs emphasizing in Powershell before it hits HTML.

Is XSLT the best way? Have suggestions for how to markup the HTML after it leaves convertto-html or ideas of a different way?


Hey, I have come up with another answer which I like better. This one doesn't rely on the browser supporting JavaScript ...

Add-Type -AssemblyName System.Xml.Linq

# Get the running processes to x(ht)ml
$xml = [System.Xml.Linq.XDocument]::Parse( "$(Get-Process | ConvertTo-Html)" )

# Find the index of the column you want to format:
$wsIndex = (($xml.Descendants("{http://www.w3.org/1999/xhtml}th") | Where-Object { $_.Value -eq "WS" }).NodesBeforeSelf() | Measure-Object).Count

# Format the column based on whatever rules you have:
switch($xml.Descendants("{http://www.w3.org/1999/xhtml}td") | Where { ($_.NodesBeforeSelf() | Measure).Count -eq $wsIndex } ) {
   {200MB -lt $_.Value } { $_.SetAttributeValue( "style", "background: red;"); continue } 
   {20MB  -lt $_.Value } { $_.SetAttributeValue( "style", "background: orange;"); continue } 
   {10MB  -lt $_.Value } { $_.SetAttributeValue( "style", "background: yellow;"); continue } 
}
# Save the html out to a file
$xml.Save("$pwd/procs2.html")

# Open the thing in your browser to see what we've wrought
ii .\procs2.html

Is there a special badge for stealing the "marked as answer" from yourself? ;-)


How about using JQuery, and inserting a header with the JQUery script and some styling, like:

Get-Process | ConvertTo-Html -Head @'

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {

   $("table tr td:nth-child(" + ( $("table th:contains('WS')").index() + 1 ) + ")").each(function() {
      if($(this).html() > 209715200) { // 200MB
         $(this).css("background", "red" );
      } else if($(this).html() > 20971520) { // 20MB
         $(this).css("background", "orange" );
      } else if($(this).html() > 10485760) { // 10MB
         $(this).css("background", "yellow" );
      }
   });

})
</script>

'@ | Out-File procs.html; ii .\procs.html


A much faster way:

Ok, I keep promising myself that I won't spend time on solved problems anymore, but ... that switch statement in my second answer was taking over 10 seconds to run on my system, -- because it's doing the "where" stuff in PowerShell instead of in LINQ.

Since PowerShell doesn't support LINQ, I solved it by writing a static helper method in an Add-Type call (and sped up that switch statement by about 1000x):

Add-Type -Language CSharpVersion3 -ReferencedAssemblies System.Xml, System.Xml.Linq -UsingNamespace System.Linq -Name XUtilities -Namespace Huddled -MemberDefinition @"    
    public static System.Collections.Generic.IEnumerable<System.Xml.Linq.XElement> GetElementByIndex( System.Xml.Linq.XContainer doc, System.Xml.Linq.XName element, int index) {
        return from e in doc.Descendants(element) where e.NodesBeforeSelf().Count() == index select e;
    }
    public static System.Collections.Generic.IEnumerable<System.Xml.Linq.XElement> GetElementByValue( System.Xml.Linq.XContainer doc, System.Xml.Linq.XName element, string value) {
        return from e in doc.Descendants(element) where e.Value == value select e;
    }
"@

# Get the running processes to x(ht)ml
$xml = [System.Xml.Linq.XDocument]::Parse( "$(Get-Process | ConvertTo-Html)" )

# Find the index of the column you want to format:
$wsIndex = [Huddled.XUtilities]::GetElementByValue( $xml, "{http://www.w3.org/1999/xhtml}th", "WS" ) | %{ ($_.NodesBeforeSelf() | Measure).Count }


switch([Huddled.XUtilities]::GetElementByIndex( $xml, "{http://www.w3.org/1999/xhtml}td", $wsIndex )) {
   {200MB -lt $_.Value } { $_.SetAttributeValue( "style", "background: red;"); continue } 
   {20MB  -lt $_.Value } { $_.SetAttributeValue( "style", "background: orange;"); continue } 
   {10MB  -lt $_.Value } { $_.SetAttributeValue( "style", "background: yellow;"); continue } 
}

# Save the html out to a file
$xml.Save("$pwd/procs2.html")

# Open the thing in your browser to see what we've wrought
ii .\procs2.html

PowerShell 3:

I redid this in PowerShell 3 after someone linked to this post, and you no longer need the compiled types to get it fast:

Add-Type -AssemblyName System.Xml.Linq

$Process = $(Get-Process | Select Handles, NPM, PM, WS, VM, CPU, Id, ProcessName)

$xml = [System.Xml.Linq.XDocument]::Parse( "$($Process | ConvertTo-Html)" )
if($Namespace = $xml.Root.Attribute("xmlns").Value) {
    $Namespace = "{{{0}}}" -f $Namespace
}

# Find the index of the column you want to format:
$wsIndex = [Array]::IndexOf( $xml.Descendants("${Namespace}th").Value, "WS")

foreach($row in $xml.Descendants("${Namespace}tr")){
    switch(@($row.Descendants("${Namespace}td"))[$wsIndex]) {
       {200MB -lt $_.Value } { $_.SetAttributeValue( "style", "background: red;"); continue } 
       {20MB  -lt $_.Value } { $_.SetAttributeValue( "style", "background: orange;"); continue } 
       {10MB  -lt $_.Value } { $_.SetAttributeValue( "style", "background: yellow;"); continue } 
    }
}
# Save the html out to a file
$xml.Save("$pwd/procs1.html")

# Open the thing in your browser to see what we've wrought
ii .\procs2.html


As a result of some github discussions, I've updated this for PowerShell 4/5/6 using the .where() method thus eliminating the dependency on the pipeline. The slow part is generating the HTML whereas the actual XML manipulation only takes ~200 ms.

Add-Type -AssemblyName System.Xml.Linq

# Get the running processes to x(ht)ml. This is *SLOW*.
$xml = [System.Xml.Linq.XDocument]::Parse([string] (Get-Process | ConvertTo-Html))

# Find the index of the column you want to format:
$wsIndex = $xml.Descendants("{http://www.w3.org/1999/xhtml}th").
    Where{$_.Value -eq "WS" }.
        NodesBeforeSelf().
            Count

# Format the column based on whatever rules you have:
switch($xml.Descendants("{http://www.w3.org/1999/xhtml}td").Where{@($_.NodesBeforeSelf()).Count -eq $wsIndex} ) {
   {200MB -lt $_.Value } { $_.SetAttributeValue( "style", "background: red;"); continue } 
   {20MB  -lt $_.Value } { $_.SetAttributeValue( "style", "background: orange;"); continue } 
   {10MB  -lt $_.Value } { $_.SetAttributeValue( "style", "background: yellow;"); continue }
}

# Save the html out to a file
$xml.Save("$pwd/procs2.html")

# Open the thing in your browser to see what we've wrought
ii .\procs2.html
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜