August 19, 2010

Powershell Annoyance

I want to love Powershell. There is much that is cool about it, and it is certainly beats batch files or (gag) VBScript. I'm a beginner, and perhaps I'll eventually learn to accept its quirks, but I bump into annoyances frequently enough that Powershell and I are still just friends.

What frequently happens is that I get excited about a cool new concept or feature, but some detail of the execution mars it. I may post more later about a script I've been trying to write, but for now here is a simple example.

Users of bash and other Unix shells will be familiar with the $? variable. It stores the exit status of the last command executed. Generally a value of zero means "success", and anything else indicates an error of some sort:

    $ ls modlist.txt
    modlist.txt
    $ echo $?
    0
    $ ls nosuchfile
    ls: cannot access nosuchfile: No such file or directory
    $ echo $?
    2

In the first case, because the ls command succeeded, the value of $? is 0 . In the second, the value of $? is 2, indicating an error. This is not very helpful at the command line, but when writing a script you might use it to branch depending on whether a command succeeded or not (though there are more concise ways to accomplish that).

In Powershell, exit statuses aren't puny integers: They are full-fledged objects, of type ErrorRecord. And instead of automatically saving just the last one, there's a ring buffer array with the last 256 (by default) error objects! The most recent error is stored in the first element of the array (at index 0):

    PS H:\> dir nosuchfile
    Get-ChildItem : Cannot find path 'H:\nosuchfile' because it does not exist.
    At line:1 char:4
    + dir <<<<  nosuchfile
        + CategoryInfo          : ObjectNotFound: (H:\nosuchfile:String) [Get-ChildItem], ItemNotFoundException
        + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

    PS H:\> $error[0]
    Get-ChildItem : Cannot find path 'H:\nosuchfile' because it does not exist.
    At line:1 char:4
    + dir <<<<  nosuchfile
        + CategoryInfo          : ObjectNotFound: (H:\nosuchfile:String) [Get-ChildItem], ItemNotFoundException
        + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

    PS H:\> $error[0].CategoryInfo


    Category   : ObjectNotFound
    Activity   : Get-ChildItem
    Reason     : ItemNotFoundException
    TargetName : H:\nosuchfile
    TargetType : String



    PS H:\> $error[0].TargetObject
    H:\nosuchfile
    PS H:\> $error[0].Exception
    Cannot find path 'H:\nosuchfile' because it does not exist.
    PS H:\>

As you can see, I tried to dir a non-existent file, and this generated an ErrorRecord which was stored in $error[0]. I can go back and examine the properties of this object, like CategoryInfo, TargetObject, Exception, etc. Neato. So far, so good.

Before I continue, you need to know that PowerShell also supports tab-completion of property names (kind of like IntelliSense), another great feature. So I can reference a property name with the first few characters of the name, press the <TAB> key, and the PowerShell interpreter will finish it for me:

    PS H:\> $name = "David"
    PS H:\> $name.le     # Press <TAB> here ...
    PS H:\> $name.Length
    5
    PS H:\>

It even gives me the canonical capitalization. Well done, PowerShell. But having grown accustomed to this tab-completion, it's only natural that I would try to use it with $error[0]. Recall that $error[0].Exception is a perfectly legal expression: this is the value of the Exception property of the object stored in $error[0]. But if I type $error[0].Exc<TAB>, nothing happens. Even worse, if I then finish typing it by hand, I find that there is no Exception property anymore!

    PS H:\> $error[0].Exc    # Press <TAB> here ...  no auto-completion :^(
    PS H:\> $error[0].Exception     # Fine, I'll type the extra six characters msyelf.         
    PS H:\> $error[0]

What happened? Everything's gone all crazy! Let's see what's in $error[0] now:

    PS H:\> $error[0]
    Unable to find type [0]: make sure that the assembly containing this type is loaded.

Now I'm really confused, because I can't tell whether the error message is simply the display representation of the ErrorRecord object stored in $error[0], or whether that error message is about the $error array itself. I'm pretty sure it's the former. Going back through the $error array, I eventually find my nosuchfile error at index 2:

    PS H:\> $error[1]
    Unable to find type [0]: make sure that the assembly containing this type is loaded.
    At line:1 char:10
    + $_val=[0] <<<<
        + CategoryInfo          : InvalidOperation: (0:String) [], RuntimeException
        + FullyQualifiedErrorId : TypeNotFound

    PS H:\> $error[2]
    Get-ChildItem : Cannot find path 'H:\nosuchfile' because it does not exist.
    At line:1 char:4
    + dir <<<<  nosuchfile
        + CategoryInfo          : ObjectNotFound: (H:\nosuchfile:String) [Get-ChildItem], ItemNotFoundException
        + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

So not only doesn't tab-completion of property names work when indexing array variables, but it appears to silently generate an ErrorRecord object which is then stored in the $error array. Perhaps there's a very good reason why it has to be this way (if so, let me know), but it's quite annoying.

Long story short: don't try to tab-complete $error array properties. I recommend assigning $error[0] to a new variable (like $err), and then working with that:

    PS H:\> dir blargh
    Get-ChildItem : Cannot find path 'H:\blargh' because it does not exist.
    At line:1 char:4
    + dir <<<<  blargh
        + CategoryInfo          : ObjectNotFound: (H:\blargh:String) [Get-ChildItem], ItemNotFoundException
        + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

    PS H:\> $err = $error[0]
    PS H:\> $err.Exception       # Here tab-completion will work
    Cannot find path 'H:\blargh' because it does not exist.
    PS H:\>

Then you can use tab-completion to your heart's content.

Posted by cradle at 12:52 PM | Comments (1)