Wednesday, February 11, 2009

My First Dip Into Windows Powershell

One could take an extreme stance and say:

"Automating Repetitive Tasks is the Cornerstone of Productivity"

If not the cornerstone of productivity, it's at least a step away from a life bogged down with the boredom of doing repetitive tasks. In software, there is little productive work accomplished without a surrounding mountain of boilerplate repetitive tasks... and what do we have to fight back against those tasks? Well... scripting, naturally. And the first place a person looks to find a scripting language is at the built in command interpreters of their OS.

The Unix derivatives have a multitude of command interpreters (bash, csh, ...) and hence have a multitude of competing scripting languages... which surprisingly didn't benefit much from the competition as they all have subtly different and uncomfortably cryptic syntax and feature sets. Windows on the other hand had no internal competition, and thus the only command interpreter windows offered was 'cmd' with it's scripting language batch.

Ohhh batch. Anyone who has had to write anything more than the most trivial of dos batch scripts likely hopes never to repeat the experience. Dos batch scripting is so inflexible and outdated that it makes the awkward syntax used by bash and csh seem golden in comparison. Most any useful batch script required very clever hacks and an intimate understanding of the ins and outs of... well, actually, rather than wasting my time complaining about how terrible batch is, let's get to the point of this blog: Windows Powershell.

Windows powershell appears to have been in the dirty hands of the public since sometime in 2006 and perhaps is Microsoft's long awaited replacement for cmd. It's built on top of the .NET framework and, well, the best way I can describe it would be if the .NET framework and Bash got together on a wild night and Powershell was embarassingly delivered by the stork 9 months later. Unfortunately (as is all too common) it appears the stork neglected to bring decent documentation along with its delivery. . . google, here we come.

So I downloaded Powershell 1.0 along with the 'Powershell documentation pack' from microsoft, installed it, read through the 'documentation pack' (which basically just consisted of two 'getting started' documents- nothing comprehensive), played around with a few of the commands, lost interest, and closed it without opening it again until yesterday (two weeks later). My initial impression was that, despite being quite familiar with batch, bash, and the .NET framework, Powershell wasn't as intuitive as I hoped but did seem interesting enough to merit further investigation.

Well, yesterday I updated some css files and wanted to 'deploy' them to the location on my HD where I host my development copy of the company website. By 'deploy' I of course mean the repetitive task of copying the files by hand to their appropriate locations... a task I expected to do numerous times as I tweaked the css pixel by pixel to get everything aligned 'just right.' It didnt take more than two iterations before I got fed up with the repetitive task and decided to write a script to do the task for me. Typically I'd use python for a task such as this... but it seemed too 'easy' for python... all I wanted to do was (1) search a drive for instances of a particular file and then (2) overwrite those file locations with the new version... something so simple seemed like a good test for PowerShell.

The first thing to know about powershell is that the commands work with .NET objects, not strings. What does that mean? Well, to me it means that you can take the return of a function, put a '.' after it, and call GetType().Name to see the class name... and it took a little playing around but I did manage to accomplish that:

PS C:\> (dir C:\AUTOEXEC.BAT).GetType().FullName
System.IO.FileInfo

Sure enough, the dir command returns file info objects. But what if it returns more than one item?

PS C:\> (dir C:\*.*).GetType().FullName
System.Object[]

Interesting... it returns an array of objects... and how do I get at the elements? Trying an standard array operator with a zero index:

PS C:\> (dir C:\*.*)[0].GetType().FullName
System.IO.FileInfo
PS C:\> (dir C:\*.*)[0].Name
CONFIG.SYS

I'm convinced. (Note that PowerShell operates on 'cmdlets' and that 'dir' actually maps to Get-ChildInfo. Try typing "dir alias:" to look at the 'alias drive' and see all of the built in aliases.)

Now I dove into writing a function to do my file copy. The documentation pack's quickstarts gave a few trivial examples for functions, but I needed something more... I couldn't find any good Microsoft documentation (their faq pointed to the powershell blog, and the search function on the powershell blog didn't appear to work (searching for 'function' returned no results)) so I hit the ol' google and landed on "PowerShell Functions and Filters" at powershellpro.com. (Btw, check out the pictures on the front page of powershell.pro.com... quite possibly the cheesiest stock corporate pics ever.)

Ok... with knowledge on how to pass parameters to a function (how could M$'s documentation pack not cover passing parameters to a function?) I could at least write the first line of my function... but then came the question of input validation... how do I validate my required parameters are passed and have valid content? In fact, the primary parameter is the location of the source file which I'm going to use to overwrite the destination files... how can I call System.IO.Path's Exist method to check to see if it exists?

Well, rather than ask the thousand questions I asked while writing my first method, let's dump the contents of my method and discuss the more interesting points...


function deployfile (
[string]$sourcefile=$(throw "Must pass sourcefile param"),
[string]$filter,
[switch]$noprompt,
[switch]$whatif)
{
# Check to see if the source file exists
if ( ![System.IO.File]::Exists($sourcefile) )
{
write-host "The source file $sourcefile doesn't exist";
return;
}

# If they didn't pass in a 'search filter' then let's just use
# the input filename as the file to search for
if ( $filter -eq "" )
{
$filter = [System.IO.Path]::GetFileName($sourcefile);
}

# search the filesystem for instances of the file
$flist = $(Get-ChildItem -recurse -filter $filter)
if ( $flist.Count -eq 0 )
{
write-host "There were no files found";
return;
}

# Iterate over every instance found
foreach ($a in $flist)
{
# if the user didn't ask us not to prompt them, then let's ask if the file
# should be overwritten, if it shouldn't let's continue onto the next file
if ( !$noprompt )
{
$ans = read-host -prompt "Replace File $($a.get_FullName())? (Y/N)";
if ( ($ans -eq "n") -or ($ans -eq "N") )
{
continue;
}
}

# If we're not in pretend 'what-if' mode then actually do the replace,
# otherwise let the user know what we would have done.
if ( !$whatif )
{
write-host "Replacing $($a.get_FullName())";
copy-item -path $sourcefile -destination ($a.get_FullName())
}
else
{
write-host "Would replace $($a.get_FullName())";
}
}
}

Glossing over the takeaways from the above method...

Input Validation
If you qualify the type of a parameter by putting the type in brackets Powershell will ensure you're passed that type. If a parameter is mandatory you add a 'throw' as the 'default value'

[string]$sourcefile=$(throw "Must pass sourcefile param")

And you can further validate your parameters by calling Powershell or .Net functions and then returning if the content is invalid. Note the syntax for calling a static .Net method:

[System.IO.File]::Exists(...)

You put the fully qualified class name in square brackets to get at the object then use :: to get at the internal methods. If you want to create an instance of an object (a stringbuilder for instance) you can use the 'new-object' cmdlet.

The 'if' and 'foreach' syntax I got from the quick reference included with the documentation pack. Everything else about the function seems fairly straightforward. In order to have my function always available I apparently have to stick the code for it in a startup script which gets executed every time powershell starts. To do that I had to create the file:

My Documents\WindowsPowerShell\profile.ps1

And put my function definition in there... how's that for something which would have been nice in the documentation pack?

All in all I can see powershell becoming a command interpreter and scripting language I'll use more often... though it sure will be nice when it's documented well and in one place ;)

No comments:

Post a Comment