Weird Terminating Error Behavior

I’m writing a dirty hack PowerShell provider so that I can have a reasonable base of knowledge before approaching something more work-oriented.  One component of the path the provider accepts must be an integer, so I decided to throw a terminating error if the path does not follow this rule. I also decided I’d do this so I could examine how terminating errors are handled. Something that mystifies me is the terminating error isn’t behaving as I understand it.  Here’s the code I use to throw the error:

var record = new ErrorRecord(new ArgumentException("path"), "InvalidItemIndex",
                           ErrorCategory.InvalidArgument, null);
var details = new ErrorDetails("The second element of a text file path must be an integer.");
record.ErrorDetails = details;
ThrowTerminatingError(record);

I expect that I’d see the information in the ErrorDetails property in some way, based on how the documentation is written; instead it seems like this information gets lost somewhere:

PS C:\Documents and Settings\myUsername\My Documents> get-item text:\Section1\item1
Get-Item : The second component of the path must be an integer.
At line:1 char:9
+ get-item  <<<< text:\Section1\item1
PS C:\Documents and Settings\myUsername\My Documents> $error[0].ErrorDetails -eq $null
True
PS C:\Documents and Settings\myUsername\My Documents>

Maybe it’s fixed in PowerShell v.2.0, or maybe I’m doing something wrong, but this is definitely not what I expected.

Writing Objects from PowerShell Scripts

I’m going to continue writing about PowerShell because it’s fun to talk about and fun to use.

Today I decided to try and analyze some data in text files using a PowerShell script. The files were simulations of a betting strategy in a coin-toss scenario. The first line represented the final balance and every line thereafter represented the balance after a bet was completed. I was interested in the average balance over time and the average minimum balance for all files. The script itself was pretty uninteresting until I got to the output:

$files = get-childitem *.txt
$sum = 0
$count = 0
$minSum = 0

foreach ($file in $files)
{
    Write-Verbose "Processing file: $file"
    $sum += [long](get-content $file -totalCount 1)
    $count += 1
    
    $values = get-content $file
    $minimum = 0
    
    foreach ($line in $values)
    {
        $value = [int]$line
        $minimum = [Math]::Min($value, $minimum)
    }
    
    $minSum += $minimum
}

$average = $sum / $count
$averageMinimum = $minSum / $count

Now is when it gets fun. I originally just wrote the output using write-output:

write-output "Average: $average"
write-output "MinAverage: $minAverage"

This didn’t seem very PowerShell-like. I wanted to output an object that could be used by other scripts (for the heck of it at least.) I knew how to do this from a cmdlet, but had to search to find how to do it from a script. There seems to be two ways to make objects or object-like things to write.

Associative Arrays

Perl hackers love their hashes, and with good reason: this little data structure is very powerful. In the .NET world, we call them "dictionaries" for some reason. PowerShell associative arrays are declared with this syntax:

$array = @{name = value; name2 = value2}

If I wrote one of these using write-object, I got a nice list format with headers and could access the individual values via property syntax. Joy! Still, this isn’t really what I was looking for because I was really fishing for a way to output a PSObject.

PSObjects

You can create a PSObject using New-Object, but at first I couldn’t figure out how to let this object know it should have the properties I wanted. This is something you can use Add-Member for. You have to specify the type of member you are adding, and I believe the valid member types are listed in the PSMemberTypes enumeration. In this case, I needed NoteProperty since there’s no base object:

$output = New-Object PSObject   
$output | add-member NoteProperty -name Count -value $count
$output | add-member NoteProperty -name Average -value $average
$output | add-member NoteProperty -name AverageMinimum -value $averageMinimum
write-output $output

Expanding Wildcards in PowerShell

Yesterday I thought I’d jump into the deep end of PowerShell and try to make a provider; this lets you treat some data store as a drive in PowerShell.  It didn’t work out very well, mainly because I had no plan and just kind of hacked my way around it.  I didn’t want to do an AccessDB provider like the MS one and came up with the idea of a provider that treated each line of a text file like an item.  I kind of got overwhelmed with new information and decided to put it off for a bit.

So I switched to cmdlets today, since there’s a lot less planning involved in making some kind of cmdlet that does something.  The one I’m making takes Path and LineNumber parameters, then tries to retrieve the line of text corresponding to LineNumber from the indicated files.  I wanted to be fancy and accept wildcards in the Path parameter, and found that most people talking about wildcards are using a different approach to them than what I needed.

What You’ll Find

It seems like most people who talk about wildcards on the internet are excited about the WildcardPattern class.  I can see why.  Suppose I have a list of things I want to process and the user has given me a wildcard pattern to specify what items are important; if you don’t mind LINQ this is all it takes to prune your list:

var options = WildcardOptions.Compiled | WildcardOptions.IgnoreCase;
var pattern = new WildcardPattern(userPattern, options);

var matchedItems = from item in itemList
                   where pattern.IsMatch(item)
                   select item;

That’s awesome; I don’t have to bother processing wildcards.  However, it’s only useful if you have the list of items in the first place.  In my cmdlet, the user can pass me any arbitrary path with wildcards and I need to fill in the blanks.  The internet led me to believe this was the only wildcard processing in place, so I was looking at having to traverse the file system to make the list, then apply filtering.  Yuck.

What I Needed

One of the things I like about PowerShell is I can tell MS really stands behind it and wants it to become the new command-line.  The Cmdlet Development Guidelines are already up; compare this to other exciting technologies like WPF which get guidelines way after they’re released (I haven’t found any WPF ones, sadly).  Anyway, I was looking over these guidelines and found what I needed under the section "Support Windows PowerShell Paths".  The SessionState.Path property is a PathIntrinsics instance that has several useful methods that do the wildcard expansion for me.  For example, assuming I just happen to know the path is a PowerShell path, I can write this code:

ProviderInfo pInfo;
var resolvedPaths = SessionState.Path.GetResolvedProviderPathFromPSPath(FilePath, out pInfo);

Now, resolvedPaths is a Collection<string> of every path name that matches the wildcards in the parameter.

One thing I’m curious about is what to do with this.  My Path parameter is an array, as recommended by the design guidelines.  My plan was to loop over all of the parameters, get the expanded paths, then get the indicated line numbers from each item.  But I’m curious: is it wrong to do this expansion in BeginProcessing and replace my Path property with an array of fully-expanded items, then loop over that in ProcessRecord?  It seems wrong to change the value of the user-provided parameters, but for some reason I don’t like the nested for loops that doing it all in ProcessRecord would require.  I wonder if there’s a better way?

Working with Enumerations in PowerShell

Two posts in one day should show you I’m excited about PowerShell.

I wanted to update the script from my last post to find my My Documents folder so if I hop to Vista I won’t get an error, and I found it slightly difficult to figure out how to use Environment.GetFolderPath because I got a lot of errors trying to use the Environment.SpecialFolder enumeration.  It turns out if you pass the string value for the enumeration value you want, you get basically what you want:

$myDocs = $myDocs = [System.Environment]::GetFolderPath("MyDocuments")
Set-Location $myDocs
Set-Variable -name home -value $myDocs -force

Apparently, you can also use flags enumerations.