Friday, July 4, 2008

PowerShelling day 2

So I started with my first script finally after spending lot of time studying and designing (mostly designing, I am learning on fly now ;))

Today I learned a lot and also refreshed my memory - my whole day was combination of "Whoa, that's great" and "Grrrrr, doesn't work" ;) I love to learn new stuff and with PowerShell there is lot to learn :)

 

So which problems\solutions I run into? As this is my blog, I also like to use it as reminder ;)

PowerGUI

First of all, I started to use great (free) product PowerGUI. It consists of two user parts - first is visualization of PowerShell (this was promised for new version of MMC, not sure whether it is still the case). This is quite cool, however is not what I am looking for right now.

Second part is really great PowerGUI Script Editor - IDE for PowerShell. I started to use it today and I am already very satisfied, it is really great :)

My bad...

I also run into few problems because I am used to VB.NET programming  - I stuck at one function that simply didn't work correctly. It was supposed to accept 3 parameters, however second and third were always ignored. Skilled powershellers probably already know where was problem - yes, SomeFunction(Param1, Param2, Param3) is array and not 3 standalone arguments :) Ouch, learning something new always hurts, especially if you KNOW about that problem and you just forgot (and then you look at the code and everything seems perfectly normal ;))

Implementing debug log

For my script I wanted to implement debug log. Idea is pretty simple, because it will run as scheduled task, I want to be able to see what happened in case something goes wrong (this is generally problem with scheduled tasks, if they got stuck somewhere, without complex logging there is no way how to find out where or why they got stuck). When I wrote scripts for batches, I used MTee for that purpose. You can then create scheduled task that will automatically generate log file with current console of that script, which is really helpful. In PowerShell, I remembered there was cmdlet Start-Transcript. I used it few years ago for presentations (hmmmmm, maybe that was reason why it was even created ;)). So at beginning of my script I added Start-Transcript -Force -Path $DebugLog. After playing little bit I realized two problems.

PowerShell have by default (when you run script) disabled command echoing (as reminder, this is in batches by default enabled and is configured by @Echo on\off command). This makes complete sense, however it was not perfect for my debug scenario. Leo Tohill (thanks again) pointed me to Set-PSDebug. Parameter -trace 1 enables something very similar to command echoing, it is not exactly what I want (format is quite hard to read in case you have complex script), however it helped me a lot :)

Second problem was mentioned by Shay Levy - transcript outputs ONLY powershell output, so for example if you use ipconfig, you want be able to see it's output in debug log file. As workaround I tried Tee-Object around whole script (S4Maintenance.ps1 | Tee-Object ...). To my surprise situation was opposite - no powershell information, only output from external binaries :D So after a while I came with quite simple solution that works fine so far. I use Start-Transcript and whenever I need to call external binary, I use function Fix-Trascript:

Function Fix-Transcript ()
{
Process{
  Write-Host $_
}
}

For example with ipconfig it means ipconfig | Fix-Transcript. This way output from ipconfig is also stored in transcript file.

Scopes

I decided I want to use scopes also. In batches I implemented a lot of scopes. Not only global\local (SetLocal), but using Set <VarPrefix> behavior of Set command in cmd. For details, have a look at this blog post. By specifying prefix (for example Private.), I was able to automatically destroy all such variables at end (For /f "usebackq tokens=1,* delims==" %%i IN (`Set Private.`) Do Set %%i=).

In PowerShell it is much easier. For my script I would really like to allow fallbacks, which is very hard in batches. You specify some default values and you can override (NOT OVERWRITE) them in sub-scripts or functions. I run into one problem with PowerShell.

Consider example where I want to have (default\global) variable called $JobStorage. This variable must be provided by command line argument. Have a look at following line:

Param (
    [string]$Global:JobStorage = $(throw "You must specify folder where you jobs are stored as parameter."), #Specify folder where jobs are stored

Looks correct, right? If you try to use .\S4Maintenance.ps1 C:\Jobs, it works fine. But I really hate position based parameters - I prefer to use name-based whenever possible. PowerShell automatically supports named parameters, so instead of .\S4Maintenance.ps1 C:\Jobs I can use .\S4Maintenance.ps1 -JobStorage C:\Jobs.

Problem is that if you specify scope, you can't use named parameter. This makes sense and I don't expect that it could be considered bug. As workaround, I assigned it to global variable afterwards:

Param ([string]$JobStorage = $(throw "You must specify folder where you jobs are stored as parameter.")

$Global:JobStorage = $JobStorage

This works as expected.

Background processing

I was also thinking about background processing and run into really nice post here. Highly recommended, I will probably use it once my basic code is finished. BTW background processing is supported in PowerShell v2.

Array index evaluated to null

However of course I got stuck on something :( I use hashtable for some of my entries, but one returns me quite strange error that even Google is not able to help with:

Index operation failed; the array index evaluated to null.
At C:\Links\HDDs\DevHDD\WorkingProjects\S4Maintenance\S4Maintenance.ps1:108 cha
r:19
+                         $Containers[$ <<<< ($Container.Name)] = $Container

Code that I am trying to execute is nothing special from what I can say:

$ContainersToLoad = @{}

        ForEach ($Target in $($Container.failed.targetcontainer)) {
            $ContainersToLoad[$($Target.Name)] = $Target.Name
            }
        ForEach ($Target in $($Container.finished.targetcontainer)) {
            $ContainersToLoad[$($Target.Name)] = $Target.Name
            }
         Write-Host $ContainerToLoad.Count
        If ($ContainerToLoad.Count -gt 0) {
            ForEach ($Target in $ContainersToLoad) {$Target}
        }

 

UPDATE: Of course immediately after I posted it I realized where the problem is (in next minute ;)) one small typo, instead of $ContainersToLoad I used $ContainerToLoad ;) I will probably have a look at strict option in Set-PSDebug tomorrow ;)

No comments: