Friday, June 12, 2009

Why PowerShell (and .NET) sucks

The below is an excerpted email conversation in which I had turned to a trusted PowerShell resource to sort out why I couldn't filter a list of mailboxes from the PowerShell cmdlet get-mailbox based on their ExchangeVersion attribute. First, here's how I posed the question...

"I’m trying to get a list of Exchange 2007 mailboxes in a mixed environment using the –filter parameter of get-mailbox. But I seem to have run into a chicken-and-the-egg situation here…

Trying to feed the filter a string for comparison causes an error saying it cannot convert the string to the proper type. Trying to feed the filter a properly typed instance causes an error saying it cannot convert the typed variable to a string!

For example, my geoff@somewhere.info mailbox is an E2K7 mailbox, so:

[PS] C:\Windows\System32>$geoff = get-mailbox -id geoff@somewhere.info

PS runs off and sticks my mailbox data into the $geoff variable. If I output $geoff.ExchangeVersion on the command line, it spits out the string

0.1 (8.0.535.0). Fantastic. So I ought to be able to use it in a filter, like this, right?

[PS] C:\Windows\System32>get-mailbox -filter {ExchangeVersion -eq $geoff.ExchangeVersion}

No, sir! I get this lovely error message instead:

Get-Mailbox : Cannot bind parameter 'Filter' to the target. Exception setting "Filter": "Unable to cast object of type 'System.Management.Automation.PSObject' to type 'System.String'."

At line:1 char:20

+ get-mailbox -filter <<<< {ExchangeVersion -eq $geoff.ExchangeVersion}

Okay, I have a sense of humor. Clearly, now PowerShell is telling me it wants a string for this filter. I can output $geoff.ExchangeVersion.ToString() to the command line and get 0.1 (8.0.535.0), so I’ll just tell the filter this:

[PS] C:\Windows\System32>get-mailbox -filter {ExchangeVersion -eq $geoff.ExchangeVersion.ToString()}

Surely this is what it wants, right? Nope.

Get-Mailbox : Cannot bind parameter 'Filter' to the target. Exception setting "Filter": "Unable to cast object of type 'System.Mana

gement.Automation.PSObject' to type 'System.String'."

At line:1 char:20

+ get-mailbox -filter <<<< {ExchangeVersion -eq $geoff.ExchangeVersion.ToString()}

This is starting to get painful. Okay, sense of humor still intact. My next move: feed it a string.

[PS] C:\Windows\System32>$string = $geoff.ExchangeVersion.ToString()

And I’ll double-check that $string got what I wanted it to…

[PS] C:\Windows\System32>$string

0.1 (8.0.535.0)

Beautiful! I must be on a roll now, let’s see:

[PS] C:\Windows\System32>get-mailbox -filter {ExchangeVersion -eq $string}

Get-Mailbox : Cannot bind parameter 'Filter' to the target. Exception setting "Filter": "The value "0.1 (8.0.535.0)" could not be converted to type Microsoft.Exchange.Data.ExchangeObjectVersion."

At line:1 char:20

+ get-mailbox -filter <<<< {ExchangeVersion -eq $string}

AARGH!

So after a little while, my PowerShell elder responds with a nice explanation of how the ExchangeVersion is itself a .NET object, and how one can use the get-member cmdlet to tear into it and figure out what all the types are. I'll spare you the details, they're largely unimportant. The point is, when he was all done he said this:

>> Pain in the a$$, isn’t it? Now at this point, I pretty much throw my arms up in disgust at the rookie CS major that designed this tangled web of nested object types and start to look for a short cut. Given enough time, I am sure a real developer could figure this out. I’m no real developer so it’s hack time.

Ok, fair enough. His hack was to use a different attribute to filter the objects, specifically the LegacyMailbox attribute. Here's what he suggested:

get-mailbox -resultsize unlimited -filter {RecipientTypeDetails -eq "LegacyMailbox"}

Now everyone who writes code has run into the case where sometimes you've got to change course to get what you need done in a reasonable amount of time. It's a wise way to make the best use of your time: accept the paradigm shift and move on. So what the heck, I thought, why not try a -ne operator and I'll have exactly what I need?


[PS] C:\Windows\System32>get-mailbox -resultsize unlimited -filter {RecipientTypeDetails -ne "LegacyMailbox"}

Get-Mailbox : Property RecipientTypeDetails used in the filter has unsupported operator NotEqual.

At line:1 char:12

+ get-mailbox <<<< -resultsize unlimited -filter {RecipientTypeDetails -ne "LegacyMailbox"}

Even the most rookie developer in India could see that this is a travesty at best. They don't support the -ne operator in a filter? WTF? Have I come up with an unusual request in that I would like to know only about the mailboxes that are E2K7 mailboxes? Surely I am not the first to ask this question, right? But wait a minute. This is a fundamental problem with a programming language when a comparison operator is only conditionally supported.


Alrighty. So if old school comparison operators don’t work, let’s try it the Microsoft way.

Sense of humor still intact, I decided to apply what my PowerShell mentor had taught me about the ExchangeVersion attribute being an instance of an object. Why not, after all, I DO consider myself a real developer. How hard can it be? After a little hacking around, I arrived at the conclusion that I could compare one attribute of the object-within-an-object to an integer. Ought to be simple, right?


[PS] C:\Windows\System32>get-mailbox -resultsize unlimited -filter {ExchangeVersion.ExchangeBuild.Major -gt 6}

Get-Mailbox : Cannot bind parameter 'Filter' to the target. Exception setting "Filter": """ is not a valid operator. For a list of

supported operators see the (worthless) command help.

"ExchangeVersion.ExchangeBuild.Major -gt 6" at position 16."

At line:1 char:42

+ get-mailbox -resultsize unlimited -filter <<<< {ExchangeVersion.ExchangeBuild.Major -gt 6}

Here’s where things really get ugly. The above “”” tells me that the PowerShell parser isn’t up to the task of figuring out what I’m asking for. They’re not supporting their own object model, let alone the whole thing about simple comparison operators like –ne. Plus, they can't even parse a line of code because nobody ever envisioned somebody trying to use it.

I remember well coming into HP and being a big VBScript proponent. Joe Richards used to give me hell about using a half-baked language, and I set out to prove him wrong. For the most part, I could do everything in VBScript that he could do in Perl. I was a true believer, so I overlooked the fact that it took twice as much code. But then one day I had a situation where I had a floating point number that I needed to convert to an Integer. No biggie, right? VBScript had a cint() operator.…that returned a floating point value about 5% of the time.

THAT was enough for me. I’m sure Joe still remembers that day that I (literally) threw up my hands and said “goddamned VBScript!” He laughed at me for a good, long time that day. And I promptly sat down and learned Perl.

It looks like the same people who designed VBScript (and abandoned it!) were the ones calling the design shots for PowerShell!

This is *precisely* why I have made the conscious decision to do as many things in plain old Perl as I possibly can. Have you ever stopped to think about how needlessly complicated things are anytime you get close to .NET? All the object casting and typing that has to go on…and absolutely none of it is there to make our lives as developers easier. I think the truly telling thing is Windows 7: by now, Microsoft has had more than adequate time to rewrite their OS kernel in .NET if it chose to do so. They haven’t. Why? I think I know why…because the systems kernel guys at Microsoft recognize that they actually gotta get something done. And what’s worse is they haven’t patched up all the strange little hacks one needs from .NET to do things in the underlying OS – stuff like consuming a COM object ought to be native in the framework without having to dance the Microsoft three-step through acronym hell – there shouldn’t even be COM objects anymore – and still, Windows developers are going to have to live with this bloated crap for yet another entire run of a client OS through their product cycle.


There are other, less beautiful solutions to the problem than what I was envisioning using. Looks like it’s time for one of those. Originally I had written this thing in C#, and it worked great…until I put it into a customer environment with over 100K mailboxes and the resulting 300MB of data output by the wrapped PowerShell cmdlet blew up the memory management in the framework. Ever had to tell a customer that you couldn’t give them reports because they have too many mailboxes, and it breaks your software? I’m still smarting over that one.


Maybe some smart PowerShell hacker will read this post and take the opportunity to tell me how dumb I am, that the solution was right in front of my face the whole time. Why, (I'm imagining he'll say), I'm not smart enough to even be a Systems Administrator and use a command shell properly, how dare I have the gall to call myself a developer?!


All I can say is this: in ten years of programming Perl, I have never, ever run into a problem like this in the core language. I've run across a few CPAN modules that didn't work as advertised, and I've seen a CPAN module I relied upon get abandoned a time or two when I needed something fixed. Maybe I'm not smart enough for .NET. Or maybe...just maybe...


.NET sucks.



2 comments:

  1. I feel your pain today. I fought with powershell and WS-Man remote nonsense for 8 hours today. Invoke-command and pssession all failed to run a simple install program on a remote machine. I'm pretty sure I'm doing something wrong, but I tried so many permutations and went through a ton of help files and still couldn't get it.

    Ofcourse all of that would not have been necessary if Windows could pass my credentials properly when using schtasks.

    ReplyDelete
  2. So, I think the problem is that you're not reading the error message completely. It tells you with <<<< and character numbers where the casting problem is: not inside the filter scriptblock. The problem is that -Filter doesn't take a script block, it takes an OPath string.

    ReplyDelete