Monday, 15 November 2010

Powershell: Catching an unspecified number of arguments into an array

I recently had the need for creating a command to be executed on an unspecified number of objects, and being picky regarding the command syntax, I wanted it to be called in the "usual" way, like:

My-Command namedParameter file1 file2 ... file
I.e. the user should not have to know that the list of objects (in this case, files) had to be specified as an array.

This was fairly easy in Powershell, making use of the $MyInvocation predefined variable: simply define those parameters you want as named parameters, and catch the 'rest' into an array.
function My-Command {
   param ($namedParameter)
   #Catch the items to be labelled from the unbound arguments
   if ($MyInvocation.UnboundArguments.Count -gt 0) {
      [string[]]$items = $MyInvocation.UnboundArguments
   }
   else
   {
      [string[]]$items = (($pwd).toString())
   }
   ...
}
In this case, if no unbound arguments are given, the $items array at least gets one value consisting of the current directory path, but you could easily replace that with for example a throw()-statement instead.

Friday, 12 November 2010

Enhancing ClearCase with Powershell

Using Powershell to access IBM Rational ClearCase data gives (as with just about everything that you use with PS) enormous enhancements. Here's just an example: since you can interface with COM objects from Powershell and ClearCase has a (rather useful) COM application, you gain a much more flexible and powerful access to ClearCase data and commands than with the usual cleartool commands. And it's faster.

Here's a replacement for the cleartool command, using the COM interface instead. I haven't done any real tests, but a simple "eye-measure" test with an advanced search told me it's about three times faster:

function Invoke-Cleartool
{
    $CCCleartool = New-Object -COM ClearCase.Cleartool    #Get all arguments as one command-line
    $command = ""
    $MyInvocation.UnboundArguments | Foreach-Object {
        if ($_ -eq ".") {$command += " $pwd"}
        else { $command += " $_" }
    }
   
    # Create a "real" array to store the result in
    $result = New-Object System.Collections.ArrayList
   
    #Parse the output and store as items in the array
    ($CCCleartool.CmdExec($command)).replace($pwd,".").split("`n") | Foreach-Object { $result.Add($_) >$null}
   
    #Remove the last item (which will always be an empty string)
    $result.RemoveAt($result.Count - 1)
   
    $result
}


Update: Just noted that there's a bug here when cleartool requires quotations around the arguments (for example when calling find -name). Powershell is good at stripping any unneeded quotations from arguments, but in this case that doesn't suit me very well...

Tuesday, 9 November 2010

Testing for parameters in Powershell functions

I recently needed to test for parameter existence in a Powershell function. The idea was that if parameters were provided, the command should run directly in shell; else a GUI should be presented where the user can enter any of the parameters in a user-friendly way.

I started out with the "standard" test, using $args:
    if ($args.Count -gt 0)
    {
        ...
    }
    else
    {
        Show-GUI
    }




Unfortunately, $Args turned out to always be zero (at least when using from within a module as in this case). I found help at this post at EggheadCafé though, switching $args for $MyInvocation.BoundParameters:

    if ($MyInvocation.BoundParameters.Count -gt 0)
    {
        ...
    }
    else
    {
        Show-GUI
    }

A small word of warning though: $MyInvocation always refers to the "current context", which means that if you debug and try getting data, it will show the data for the current shell, not the function being debugged.

Wednesday, 29 September 2010

Powershell: Error-handling in modules

I've recently started using modules for my Powershell commands for managing my custom commands, since I this neing a great way to encapsulate and distribute such commands. Besides, I can choose the purpose of my session by selecting what modules to import.

However, there seems to be a BIG caveat with modules: the global variables seems not to be available. In particular, the automatic $error variable cannot be used inside modules. You can try this example:
New-Module -ScriptBlock {
    function div ($number) {
       try { 1/$number }
       catch { Write-Host $Error[0] }
    }
} -name DivMod | Import-module

PS > div 1

1

PS > div 0

Note the empty line: the $Error variable is empty! I need to dig further into this, but since not even Googling seems to have any answers(!), I think I've come up with a workaround, at least: use Invoke-Expression with the -ErrorVariable parameter for 'sensitive' commands (e.g. those that you need good error-handling for).
New-Module -ScriptBlock {
   function div ($number) {
      try { Invoke-Expression '1/$number' -ErrorVariable e }
      catch { Write-Error $e }
   }
} -name DivMod | Import-module

PS > div 1

1

PS > div 0
System.Management.Automation.CmdletInvocationException: Attempted to divide by zero...
Using this, you can use try..catch in modules almost as you would in scripts.

If anyone out there has a better solution (I'd really like to be able to use the $error variable...) don't hesitate to comment/point me in the right direction!