Monday, November 2, 2009

Making Static .NET Classes More PowerShellish

There are many cases where you want to use static methods on .NET classes in PowerShell.  There are several classes that you may use quite a bit and would like to make them easier to use than having to write out [System.Math]::abs for example.  It’s quite easy to wrap these commands in a PowerShell function, but if there are many methods, it can be tedious and a pain to maintain.  So, I came up with the following approach to make this a bit easier.  I use the Math and System.IO.Path class methods quite a bit.  My example will look at the Math methods.
Functions with a Single Argument
In a script (called WrapMath.ps1) I declare the class
$class = 'System.Math'
and the list of methods…
$methods = 'abs','acos','atan','atan2','ceiling','cos','cosh','equals','exp',` 'floor','log','log10','sign','sin','sinh','sqrt','tan','tanh','trunc'
In this case, all of the above methods are ones that except one argument.  I then define a function prototype with place holders for the method and class
$Prototype = @'
<#
.Synopsis
|method| from static class |class|
.Description
|method| from static class |class|
.Example
|method| -2.56
.Link
[|class|]::|method|
#>
function global:|method|
{
$args[0] | foreach{return [|class|]::|method|($_)}
}
'@
You’ll notice the document string preceding the function declaration.  This actually works.  The next part is the slick bit…  loop over the methods, replacing |method| and |class| each time in the prototype and use Invoke-Expression to dynamically create the command.
$methods | foreach{            
Invoke-Expression (($Prototype.Replace('|method|',$_)).Replace("|class|",$class))            
}
You’ll notice I make use of a pipeline in the function prototype, so that these methods all work with an array or a single value.  So they are actually a bit more useful than just calling [system.math]::method
Functions with 2 Arguments
For functions that except two arguments you can do the following:
################################################################################            
#Methods that take two arguments.  The first argument can be an array.  The            
#second must be a single value.  So you could use the pow method to square            
#each element in an array.            
################################################################################            
$class = 'System.Math'                       
$methods = 'pow','round','IEEERemainder'                       
$Prototype = @'
<#
.Synopsis
|method| from static class |class|
.Description
|method| from static class |class|    
.Example
|method| 5.2 3
.Link
[|class|]::|method|
#>
function global:|method|
{
$parm2 = $args[1]
$args[0] | foreach{return [|class|]::|method|($_,$parm2)}
}
'@                       
$methods | foreach{            
Invoke-Expression (($Prototype.Replace('|method|',$_)).Replace('|class|',$class))            
}
Max and Min
Finally Max and Min are special cases, as they are some-what recursive
################################################################################            
#Max and Min methods.  These work for any size array of values.            
################################################################################            
$class = 'System.Math'                        
$methods = 'max','min'                        
$Prototype = @'
<#
.Synopsis
|method| from static class |class|
.Description
|method| from static class |class|    
.Example
|method| 10,12,13,14
.Link
[|class|]::|method|
#>
function global:|method|
{
$retval = $args[0][0]
$args[0] | foreach{$retval = [|class|]::|method|($_,$retval)}
return $retval
}
'@                        
$methods | foreach{            
Invoke-Expression (($Prototype.Replace('|method|',$_)).Replace('|class|',$class))              
}
I’ve done a similar thing with the System.IO.Path class that I will write about later.  Hope this helps someone out.

Friday, October 30, 2009

PowerShell 2.0 RTM and WPK

So after several months of waiting PowerShell 2.0 for XP and Vista was released rather cryptically. It was released as part of the "Windows Management Framework on Windows XP, Windows Server 2003, Windows Vista, and Windows Server 2008." It is available for download here. I would have expected some sort of better announcement or explanation... canon fire or something. Oh well, it's here!

PowerShellPack
Microsoft has also released PowerShellPack. A collection of handy add ons to enhance your PowerShell experience. Included in PoweShellPack is a module called WPK, which bears a lot of resemblence to PowerBoots, that I have blogged about previously.  Here is a list of features in the PowerShellPack from the Microsoft PowerShell blog
@"
WPK
Create rich user interfaces quick and easily from Windows PowerShell. Think HTA, but easy. Over 600 scripts to help you build quick user interfaces.  To get started learning how to write rich WPF UIs in script, check out Writing User Interfaces with WPK.
IsePack
Supercharge your scripting in the Integrated Scripting Environment with over 35 shortcuts. TaskScheduler
List scheduled tasks, create or delete tasks
FileSystem
Monitor files and folders, check for duplicate files, and check disk space
DotNet
Explore loaded types, find commands that can work with a type, and explore how you can use PowerShell, DotNet and COM together
PSImageTools
Convert, rotate, scale, and crop images and get image metadata
PSRSS
Harness the FeedStore from PowerShell
PSSystemTools
Get Operating System or Hardware Information
PSUserTools
Get the users on a system, check for elevation, and start-processaadministrator
PSCodeGen
Generates PowerShell scripts, C# code, and P/Invoke

"@

Testing out WPK
So decided to give WPK a spin and do a simple image preview that I have done previously. It has left me scratching my head quite a bit.

The following works:
$width = 600        
$images = (ls *.jpg,*.png) | %{new-image $_.fullname -Width $width -Tag $_.Name}        
$text = $images |  %{New-Label $image.Tag -Width $width -FontSize 16}        
new-wrappanel {                     
  foreach($i in (0..($images.Count-1))){                                                           
    new-stackpanel {        
      $text[$i]        
      $images[$i]        
    }         
  }                            
}  -show        

While this does not:
function test-func        
{        
$width = 600        
$images = (ls *.jpg,*.png) | %{new-image $_.fullname -Width $width -Tag $_.Name}        
$text = $images | %{New-Label $image.Tag -Width $width -FontSize 16}        
new-wrappanel {                     
  foreach($i in (0..($images.Count-1))){                                                           
    new-stackpanel {        
      $text[$i]        
      $images[$i]        
    }         
 }                            
}  -show        
}        
test-func
 
The only difference between the two is that in the latter case the WPK calls are wrapped in a function. That's it. I'm totally confused as to why this might be happening.  This requires some head scratching. 

I'm curious to see how WPK and PowerBoots evolve in relation to each other.

Tuesday, June 16, 2009

Using WPF in PowerShell with PowerBoots

I got the bug to play with WPF a couple of weeks ago. I'm relatively familiar with Windows Forms, and thought I'd like to finally look at WPF. I downloaded VC# 2008 Express and messed around a little bit for a few days. I had the goal of writing a WPF program that would host powershell and act like a simple Matlab. Well, I ran across PowerBoots, written by Joel Bennet (http://huddledmasses.org/powerboots/). PowerBoots brings WPF to PowerShell with some extremely simple syntax. After playing with it for a day, I wrote this relatively simple preview-images function that provides an experience similar to the image "Preview" in XP. For a tutorial see http://huddledmasses.org/powerboots-tutorial-walkthrough/


I found a very nice online source code formatter to format the below code: http://www.manoli.net/csharpformat/

The result looks like this:




And Here is the code:


function global:preview-images
{
#by default load all images supported by the WPF image control (System.Windows.Controls.Image)
param($images = (ls *.bmp,*.gif,*.ico,*.jpg,*.png,*.wdp,*.tiff,*.tif ))

$count = $images.length
$global:images = $images
$global:i = 0
$global:imindex = 1 #index of the image control in the dockpanel children array

boots -left 100 -top 100 -width 1024 -title "Preview - $pwd" {

stackpanel{
wrappanel {
#0
button "<--" -FontSize 24 -width 50 -On_Click {
$global:i = 0
$this.parent.parent.children[1].children[$global:imindex].source = $images[$global:i].fullname
$block[0].Inlines.Clear();
$block[0].Inlines.Add($images[$global:i].name)
}
#1
button "<" -FontSize 24 -width 50 -On_Click {
if ($global:i -gt 0) {
$global:i--
}
else {
$global:i = $images.length-1
}
$this.parent.parent.children[1].children[$global:imindex].source = $images[$global:i].fullname
$block[0].Inlines.Clear();
$block[0].Inlines.Add($images[$global:i].name)
}
#2
button ">" -FontSize 24 -width 50 -On_Click {
if ($global:i -lt ($images.length-1)) {
$global:i++
}
else {
$global:i = 0
}
$this.parent.parent.children[1].children[$global:imindex].source = $images[$global:i].fullname
$block[0].Inlines.Clear();
$block[0].Inlines.Add($images[$global:i].name)
}
#3
button "-->" -FontSize 24 -width 50 -On_Click {
$global:i = $count - 1
$this.parent.parent.children[1].children[$global:imindex].source = $images[$global:i].fullname
$block[0].Inlines.Clear();
$block[0].Inlines.Add($images[$global:i].name)
}
#4
button "END" -FontSize 24 -width 75 -On_Click {
#$global:i = $count
$this.parent.parent.parent.close()
#$block[0].Inlines.Clear();
#$block[0].Inlines.Add( $images[$global:i].name)
}
#5
button "Image List" -FontSize 20 -On_Click {
$images | foreach{write-host $_.name}
}
#6
textblock " " -FontSize 30 -FontWeight bold -Foreground "#0000FF"
#7
textblock $images[$global:i].name -OutVariable script:block -FontSize 30 -FontWeight bold -Foreground "#0000FF"
}
dockpanel -lastchildfill $true {

$images | %{$_.name} | listbox -SelectedIndex 0 -maxheight 900 -On_MouseDoubleClick{
$global:i = $this.selectedIndex
$this.parent.children[$global:imindex].source = $images[$global:i].fullname
$block[0].Inlines.Clear();
$block[0].Inlines.Add($images[$global:i].name)
} | ForEach-Object { $_.SetValue(
[System.Windows.Controls.DockPanel]::DockProperty,
[System.Windows.Controls.Dock]::left)
$_
}
image -source $images[$i].fullname
}

}

} -Background (linearGradientBrush $(GradientStop -Offset 0 -Color "#ffffff";GradientStop -Offset 1 -Color "#222222"))
}

Wednesday, March 19, 2008

Using ZedGraph From PowerShell

I was messing around the other day and decided to see if I could use the ZedGraph graphing library from the command line with PowerShell. I took one of the samples in C# and converted it to PowerShell. It was actually pretty straight forward and I got a big grin on my face when a graph popped up. Nice!! The C# sample is here.

The Result

The Code
#Load the zedgraph dll

$ZedGraphDll = "J:\src\zedgraph_dll_v514_464\zedgraph_dll_v5.1.4\ZedGraph.dll"
[System.Reflection.Assembly]::LoadFrom($ZedGraphDll) | out-null

#create a new form and a ZedGraphControl
$global:form = new-object Windows.Forms.form
$form.Size = new-object System.Drawing.Size @(1000,600)

$zgc = new-object -typename ZedGraph.ZedGraphControl

#// Set the titles and axis labels
$zgc.GraphPane.Title.Text = "Demo of BaseTic property"
$zgc.GraphPane.XAxis.Title.Text = "Time, Days"
$zgc.GraphPane.YAxis.Title.Text = "Widget Production (units/hour)"

#// Build a PointPairList with points based on Sine wave
$list = new-object -typename Zedgraph.PointPairList
$pi = [System.Math]::pi
for ( $i=0; $i -lt 36; $i++ )
{
$x = $i * 10.0 + 50.0
$y = ([System.Math]::sin( ($i * ($pi) / 15.0) ) * 16.0)
$list.Add( $x, $y)
}

#// Hide the legend
$zgc.GraphPane.Legend.IsVisible = $false;

#// Add a curve
$curve = $zgc.GraphPane.AddCurve( "label", $list, [System.Drawing.Color]::Red,`
[ZedGraph.SymbolType]::Circle )
$curve.Line.Width = 1.5
$curve.Symbol.Fill = new-object -typename ZedGraph.Fill([System.Drawing.Color]::White )
$curve.Symbol.Size = 5
#// Make the XAxis start with the first label at 50
$zgc.GraphPane.XAxis.Scale.BaseTic = 50

#// Fill the axis background with a gradient
$zgc.GraphPane.Chart.Fill = new-object -typename ZedGraph.Fill(`
[System.Drawing.Color]::White,`
[System.Drawing.Color]::SteelBlue, 45.0 )


#// Calculate the Axis Scale Ranges
$zgc.AxisChange()

#add the graph to the forms control
$Form.Controls.Add($zgc)
$zgc.dock = [System.Windows.Forms.DockStyle]::Fill

#display the form
$Form.Add_Shown({$form.Activate()})
[void]$form.showdialog()

-bc

Saturday, January 12, 2008

PowerShell Script to Get Text From and OpenDocument ODT File

I've been putting together some general help snippets for a collection of PowerShell scripts that I've been writing. I wanted the information to be available from the command line with "command -h" and I wanted the same information available in a standard document - Word, OpenDocument (ODT) , or PDF. I've messed around a little bit with ODT files and thought that might be the way to go. ODT files are essentially zip files with xml files (and pictures) inside defining the contents of the document. So I thought why not see If I can define the help information in and ODT document and have a script that will actually get the info from the document. Seems a bit complicated at first glance, but it's really not bad at all.

So In this post I'll just show how to get the contents of an odt file into PowerShell...

Step One - Get the contents of the "contents.xml" file
The contents.xml file has all of the text for the document it's in the root of the odt archive. I chose to use the 7-zip program ( www.7-zip.org) to extract the file. This is done like so:

#get the contents of the odt file
$res = ."c:\program files\7-zip\7z.exe" e $ODTfile content.xml #extracts only the content.xml from the archive to the current directory
$content = Get-Content content.xml
remove-item content.xml
#modified content
$mc = concat $content " "

The above snippet extracts the contents.xml file and loads it's contents into the variable $contents. I then use a concatenation script I wrote to concatenate all of the lines together into a single string. This will make the searching we need to do a little bit easier and cleaner.

Step Two - Define some regular expressions so we can identify xml tags
We now have a whole lot of xml in $mc and want to process (I use that term loosely) it a little bit. There are only a couple of elements that we really are interested in to get some base functionality. So let's define our regular expressions...

#regular expressions for identifying relevant xml tabs
$rpar = New-Object -typename System.Text.RegularExpressions.Regex("<text:[p|h][^<>]*>") #a pagraph or header line
$rtab = New-Object -typename System.Text.RegularExpressions.Regex("<text:tab[^<>]*>") #a tab character
$rtag = New-Object -typename System.Text.RegularExpressions.Regex("<[^<>]+>") #any other xml tag
$rspace = New-Object -typename System.Text.RegularExpressions.Regex("<text:s text:c[^<>]*>") #a number of spaces in a row
$rint = New-Object -typename System.Text.RegularExpressions.Regex("\d+") #an integer

Process the tags
#process paragraphs
$rpar.matches($mc) | foreach{$mc = $mc.replace($_.value,"`r`n")}
#process tabs
$rtab.matches($mc) | foreach{$mc = $mc.replace($_.value,"`t")}

Spaces are a little trickier to handle. Multiple spaces in a row are handled with a tag that looks like <text:s text:c="4">. So we need to search for the tags, find out how many spaces are in each instance, and then create a string with that many spaces. Then we need to replace the xml tags with the strings of spaces...

#process spaces
$spaceCount = New-Object System.Collections.ArrayList
$spaces = New-Object System.Collections.ArrayList
#match the xml for the space tags
$m_spaces = $rspace.matches($mc)

if ($m_spaces.Count -gt 0) {
#get the number of spaces for each match
$m_spaces | foreach{
$result = $spaceCount.add(($rint.match($_.value)).value)
}
#create strings with the correct number of spaces
for ($i = 0;$i -lt $m_spaces.Count;$i++) {
$result = $spaces.add(("").padleft([int]$spaceCount[$i]))
}
#replace the xml space tag with the string of spaces
for ($i = 0;$i -lt $m_spaces.Count;$i++) {
$mc = $mc.Replace($m_spaces[$i].value,$spaces[$i])
}
}

Clean up a little more and return the modified string

#strip remaining xml tags
$rtag.Matches($mc) | foreach{$mc = $mc.replace($_.value,"")}

#clean up other characters
$mc = $mc.Replace("&gt;",">")
$mc = $mc.Replace("&lt;","<")
$mc = $mc.Replace("&apos;","'")

return $mc

Left to do
Alot. Some things that would be nice to add...
  • Ability to handle numbered and bulleted lists - currently you get the text next to the number or bullet, but not the number or bullet
  • Tables
  • Make headings a different color?
  • A write-OdtText script would be nice, and an interesting little challenge
If you have any commments and suggestions or improvements please let me know. I'm still a relative novice with regular expressions. I was amazed at how little code it took to do this.

-bc

Wednesday, May 16, 2007

Customizing your Shell Prompt

I do a lot of work from the command line. This involves both Linux and Windows. The BASH shell is very good and much better than the Windows cmd.exe. Microsoft has finally developed a much better shell called PowerShell (which will be the subject of a future post).

In both shells I find the prompt lacking a bit. The BASH shell only shows you the name of the lowest level directory you are in (i.e. if you are in /work/stuff/more/stuff it only shows "stuff"). The default windows prompts show you the entire path, but if the path is really long then you have only a couple of characters on the line before the command you are typing wraps around. Here is how to fix this problem with both the BASH shell and PowerShell...

I want a prompt that looks like this:
user@machine | /work/stuff/more/stuff
:: Type Command Here

This gives me the full path and I still have a full line to type my command. Also when looking back at previous commands I can see what directory they were executed in.

BASH
For the BASH shell the prompt text can be controlled by the PS1 envrionment variable. For a very good overview see this webpage or this webpage. To accomplish the above I have set my PS1 variable in the .bash_profile file to:

PS1="\e[33;1m\u@\\h | \e[34;1m\w \n\e[37;1m::"

This gives the user name in yellow, the path in blue, and the command text in yellow.

PowerShell
For PowerShell the prompt text is controlled by creating a function in your PowerShell profile file called prompt. The profile file does not exist by default, but PowerShell checks for it on start up. This file should be created as :
C:\Documents and Settings\\My Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1

Here is my prompt function:
########################################################
# Prompt
function prompt {
$d = pwd
$u = get-item Env:\USERNAME
$u = $u.value
$c = get-item Env:\COMPUTERNAME
$c = $c.value
$s = "$u@$c | "
$color = "Green"
write-host $s -ForegroundColor "Yellow" -NoNewLine
write-host $d -ForegroundColor "Green"
$promptText = "::";
$title = (get-location).Path;

write-host $promptText -NoNewLine -ForegroundColor "Yellow"
$host.UI.RawUI.WindowTitle = $title;

return " "
}
########################################################

This gives the user name in yellow and the path in green (the default PowerShell background is blue). The command text still ends up being white.

Tuesday, May 8, 2007

PowerShell

When I first started work after college I despised command line programs. The command line programs I had been exposed to in college were tedious to run and we never needed to run them lot's of time so automation was never an issue.

After a while I began to see the benefits of automation by the command line. The Windows command prompt leaves quite a bit to be desired. I always felt much more comfortable on a linux command line (and I admittedly don't know nearly as much as I would like about it) than a cmd prompt. The BASH shell is simply superior to the Windows command prompt.


Enter Windows PowerShell...
Microsoft (in usual fashion) finally implemented some bits of technology that had been employed by others for years, but did it with a twist. In most current shells data is passed between commands as text. Powershell actually passes .NET objects (classes). This gives the user some interesting and powerful options. For example...

$a = dir *.txt -recurse

will find all of the text files in the current directory and subdirectories. $a is actually an array of System.IO.FileInfo objects. You can access the first item in the array as $a[0]. To get the name of the fie $a[0].Name. To get the directory: $a[0].Directory.

A True Scripting Language
With PowerShell Microsoft made sure that it supported a true scripting language. It is similar in syntax to C# in many regards. A script or function can be called with arguments. There are several ways to get at these arguments. The most basic way is through the args array. This is an array of all arguments passed to the script (assuming you haven't explicitly defined what they should be).

There is a foreach-object loop that is quite useful. If I wanted to execute all of the batch files in all directories below the one I am in the following would work quite nicely:

:: dir -filter *.bat -recurse | foreach-object { cmd /c $_}

The first part of the command finds all of the batch files in this directory and all child directories. The pipe | says to pass the output from the dir command to the foreach-object command. The foreach-object takes a code block enclosed by {} as an argument. Each object in the array that is output from the dir command is accessed through the $_ variable. So the foreach-object command runs "cmd /c " for each batch file found. PowerShell cannot natively execute batch files, so the cmd command is need to execute the batch files.

Script Editing
As described in my first blog entry the I use the PSPad editor for writing PowerShell scripts. There are several other alternatives such as PowerShell Analyzer and PowerShell IDE.

Resources