Archive for April 2011

A tool for table formatting   3 comments

As I was working on the SMLets CodePlex project, I created a bunch of new cmdlets and I wanted to make sure that the output looked good and was useful. In order to do that I needed to sling a bunch of XML and create my own formatting file. If you’ve ever looked at the format files in PowerShell (to see what’s shipped, type get-childitem “$pshome/*.format.ps1xml”) it can be pretty daunting, so most folks (me included) start with something that works an then modify it. After I did that a few times, I reckoned that I had better build a tool to make life easier. My requirements were pretty straight-forward; build a tool that emitted the XML that I needed to put in the formatting file. Second, commit no unnatural acts with regard to parameters to create the XML. What I really wanted, is to use Format-Table to get the output exactly how I wanted and then just substitute my tool for Format-Table. Thus, New-TableFormat is born.

The following is how I use it, for this example, I’m just using a process object in this example, but the concept applies to any object. First I get the output looking exactly like I want, so in this case, I show the processID, the handles, the name and then the handles in KB. In order to do that last bit, I need to use a script block. After that, it’s simply replace format-table with new-tableformat and hey! presto! I’ve got my XML!

PS> get-process lsass|format-table id,handles,name,@{L="HandlesKB";E={$_.Handles/1KB};A="Right";F="{0:N2}"} -au

 Id Handles Name  HandlesKB
 -- ------- ----  ---------
552    1274 lsass      1.24

PS> get-process lsass|new-tableformat id,handles,name,@{L="HandlesKB";E={$_.Handles/1KB};A="Right";F="{0:N2}"} -au
   <View>
    <Name>ProcessTable</Name>
    <ViewSelectedBy>
     <TypeName>System.Diagnostics.Process</TypeName>
    </ViewSelectedBy>
    <TableControl>
     <AutoSize />
     <TableHeaders>
      <TableColumnHeader><Label>id</Label></TableColumnHeader>
      <TableColumnHeader><Label>handles</Label></TableColumnHeader>
      <TableColumnHeader><Label>name</Label></TableColumnHeader>
      <TableColumnHeader>
        <Label>HandlesKB</Label>
        <Alignment>Right</Alignment>
      </TableColumnHeader>
     </TableHeaders>
     <TableRowEntries>
      <TableRowEntry>
       <TableColumnItems>
        <TableColumnItem><PropertyName>id</PropertyName></TableColumnItem>
        <TableColumnItem><PropertyName>handles</PropertyName></TableColumnItem>
        <TableColumnItem><PropertyName>name</PropertyName></TableColumnItem>
        <TableColumnItem><ScriptBlock>$_.Handles/1KB</ScriptBlock><FormatString>{0:N2}</FormatString></TableColumnItem>
       </TableColumnItems>
      </TableRowEntry>
     </TableRowEntries>
    </TableControl>
   </View>

In order for me to take advantage of this, I need to save it to a file and then call update-formatdata on the new file. You might notice that this is not quite complete as it doesn’t have the required elements for the beginning and end of the file, so I’ve got another parameter –complete which emits complete, standalone XML which I can dump into a file and then import. Once imported I can just use format-table as usual (in this case since there’s already a table format for process objects, I need to include the view name, which is “ProcessTable”, in this case. If you’re building formatting for an object of your own creation or doesn’t already exist, this last step is not necessary. Just output the object and the formatter takes care of everything automatically.

PS> get-process lsass|new-tableformat id,handles,name,@{L="HandlesKB";E={$_.Handles/1KB};A="Right";F="{0:N2}"} -au -comp
 > handleKB.format.ps1xml
PS> update-formatdata handleKB.format.ps1xml
PS> get-process lsass | format-table -view ProcessTable

id  handles name  HandlesKB
--  ------- ----  ---------
552 1263    lsass      1.23

Here’s the script, it’s only 125 lines or so!

param (
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]$object,
    [Parameter(Mandatory=$true,Position=0)][object[]]$property,
    [Parameter()][switch]$Complete,
    [Parameter()][switch]$auto,
    [Parameter()][string]$name,
    [Parameter()][switch]$force
    )
End
{
if ( $object )
{
    if ( $object -is "PSObject")
    {
        $TN = $object.psobject.typenames[0]
    }
    else
    {
        $TN = $object.gettype().fullname
    }
}
elseif ( $name )
{
    $TN = $name
}
$NAME = $TN.split(".")[-1]
$sb = new-object System.Text.StringBuilder
if ( $complete )
{
    [void]$sb.Append("<Configuration>`n")
    [void]$sb.Append(" <ViewDefinitions>`n")
}
[void]$sb.Append(" <View>`n")
[void]$sb.Append(" <Name>${Name}Table</Name>`n")
[void]$sb.Append(" <ViewSelectedBy>`n")
[void]$sb.Append(" <TypeName>${TN}</TypeName>`n")
[void]$sb.Append(" </ViewSelectedBy>`n")
[void]$sb.Append(" <TableControl>`n")
if ( $auto )
{
    [void]$sb.Append(" <AutoSize />`n")
}
[void]$sb.Append(" <TableHeaders>`n")
# 
# Now loop through the properties, creating a header for each 
# provided property
#
foreach($p in $property)
{
    if ( $p -is "string" )
    {
        [void]$sb.Append(" <TableColumnHeader><Label>${p}</Label></TableColumnHeader>`n")
    }
    elseif ( $p -is "hashtable" )
    {
        $Label = $p.keys | ?{$_ -match "^L|^N" }
        if ( ! $Label )
        {
            throw "need Name or Label Key"
        }
        [void]$sb.Append(" <TableColumnHeader>`n")
        [void]$sb.Append(" <Label>" + $p.$label + "</Label>`n")
        $Width = $p.Keys |?{$_ -match "^W"}|select -first 1
        if ( $Width )
        {
            [void]$sb.Append(" <Width>" + $p.$Width + "</Width>`n")
        }
        $Align = $p.Keys |?{$_ -match "^A"}|select -first 1
        if ( $Align )
        {
            [void]$sb.Append(" <Alignment>" + $p.$align + "</Alignment>`n")
        }
        [void]$sb.Append(" </TableColumnHeader>`n")
        # write-host -for red ("skipping " + $p.Name + " for now")
    }
}
[void]$sb.Append(" </TableHeaders>`n")
[void]$sb.Append(" <TableRowEntries>`n")
[void]$sb.Append(" <TableRowEntry>`n")
[void]$sb.Append(" <TableColumnItems>`n")
foreach($p in $property)
{
    if ( $p -is "string" )
    {
        [void]$sb.Append(" <TableColumnItem><PropertyName>${p}</PropertyName></TableColumnItem>`n")
    }
    elseif ( $p -is "hashtable" )
    {
        [void]$sb.Append(" <TableColumnItem>")
        $Name = $p.Keys | ?{ $_ -match "^N" }|select -first 1
        if ( $Name )
        {
            $v = $p.$Name
            [void]$sb.Append("<PropertyName>$v</PropertyName>")
        }
        $Expression = $p.Keys | ?{ $_ -match "^E" }|select -first 1
        if ( $Expression )
        {
            $v = $p.$Expression
            [void]$sb.Append("<ScriptBlock>$v</ScriptBlock>")
        }
        $Format = $p.Keys | ?{$_ -match "^F" }|select -first 1
        if ( $Format )
        {
            $v = $p.$Format
            [void]$sb.Append("<FormatString>$v</FormatString>")
        }
        [void]$Sb.Append("</TableColumnItem>`n")
    }
}
[void]$sb.Append(" </TableColumnItems>`n")
[void]$sb.Append(" </TableRowEntry>`n")
[void]$sb.Append(" </TableRowEntries>`n")
[void]$sb.Append(" </TableControl>`n")
[void]$sb.Append(" </View>`n")
if ( $complete )
{
    [void]$sb.Append(" </ViewDefinitions>`n")
    [void]$sb.Append("</Configuration>`n")
}
$sb.ToString()
}

the thing to note here is that I’m just building the XML string and then emitting it, I’m not using all the XML programming goo, which would probably be better practice, but I didn’t want to be bothered (clearly, that could be a future enhancement). You’ll notice that it handles pretty much everything that format-table does.

I hope this is as useful for you as it is for me

Advertisements

Posted April 19, 2011 by jtruher3 in PowerShell

missing in action   1 comment

Clearly i haven’t posted for a long while, and while there are many reasons which I don’t need to go into, it’s not because I’ve been slacking. Most recently, I’ve been working on a codeplex project (http://smlets.codeplex.com) which uses PowerShell to interact with Service Manager (i’ve been working on Service Manager for a while). The project has been great fun and has let me do a quite a bit of cmdlet development, which I really enjoy. If you use Service Manager, you should definitely check it out.

The real point of this post is to share my experience over the last couple of days at the PowerShell Deep Dive which happened at the Experts Conference in Las Vegas. I have always been extremely proud of my work while I was working on PowerShell; giving birth to that product was a long, and at times, difficult experience which taught me a lot about myself. However, seeing how it has affected (in a positive way) so many people really makes all that worth while. Seeing the excitement in others cannot help but uplift the spirit and I am so glad that I had the opportunity to contribute. The stories about how PowerShell have improved the lives of others in concrete ways is awesome.

The experience has reinvigorated me as well; talking to so many intelligent, articulate people has given me a whole bunch of new ideas to write on, so I hope you will see a stream of posts here (as long as I can follow through!). My next post will be on creating custom formatting, hopefully posted before the plane lands.

Posted April 19, 2011 by jtruher3 in General, PowerShell, ServiceManager