Saturday, August 29, 2015

PowerShell's Strict Mode (and a rant about implicit behaviors)

PowerShell's strict mode (enabled by running set-strictmode latest) is really nice.  So nice, in fact, that I'm wondering why it isn't the default.  In fact, why is there even the option to have a non-strict mode?

PowerShell's strict mode makes the interpreter throw terminating exceptions when it would otherwise silently fail, with respect to uninitialized variables in expressions.  This is megas useful nyoro~n for finding typo'd variable names and generally keeping you from being confused later on as to why an exception is getting thrown elsewhere, or why your script isn't outputting correctly.

I would argue that it should be taken a step further.  There should be a version of strict mode that prevents assignment to uninitialized variables as well, so that you have to use the new-variable cmdlet to explicitly create variables.  I already do this in all the scripts I write, because of a broader issue I have with PowerShell, namely, its never-ending set of implicit and sometimes unintuitive behaviors.

So that I can be 100% sure I know what's happening, I force PowerShell to bend to my will by explicitly defining every behavior that I want to happen, even when it would normally happen implicitly.  This has also led to my adoption of the mantra "If it doesn't work in PowerShell, throw more parentheses at it.", which you can often find in an all caps comment in my code near where I've added parentheses to make something work.

Because those of you who aren't familiar with PowerShell may be wondering what sorts of code wouldn't work as expected without parentheses, but works properly as soon as they are added, I will offer an example.  Let's use something for this example that looks innocent enough: declaring a new variable and initializing both its type and its value using a typecast.
> new-variable -name foo -value [decimal]0 > $foo [decimal]0 > $foo.gettype() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True String System.Object
That's right, the way you'd immediately think of doing it results in $foo being the string '[decimal]0' instead of the value 0 as type System.Decimal.  Now, let's stick some unnecessary-looking parentheses in there and see what happens.
> new-variable -name bar -value ([decimal]0) > $bar 0 > $bar.gettype() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Decimal System.ValueType
Now we get our expected behavior.  This only happens because PowerShell will implicitly interpret unquoted sequences of characters as strings.  This implicit behavior is the cause of a rather large class of annoying behaviors in PowerShell: the parser decides too quickly that something is a string instead of code to be executed, which can produce a variety of headache-inducing results.

Here's another one.  Again, we're making a new variable, however, we want to assign it an object that we're creating via the new-object cmdlet.  Once again, this looks innocent enough, but fails:
> new-variable -name baz -value new-object system.xml.xmldocument New-Variable : A positional parameter cannot be found that accepts argument 'system.xml.xmldocument'. At line:1 char:13 + new-variable <<<< -name baz -value new-object system.xml.xmldocument + CategoryInfo : InvalidArgument: (:) [New-Variable], ParameterBindingException + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.NewVariableCommand
As you might expect by this point, merely putting parentheses around the call to new-object gives us our desired behavior.
> new-variable -name baz -value (new-object system.xml.xmldocument) > $baz NodeType : Document ParentNode : DocumentType : Implementation : System.Xml.XmlImplementation Name : #document LocalName : #document DocumentElement : OwnerDocument : Schemas : System.Xml.Schema.XmlSchemaSet XmlResolver : NameTable : System.Xml.NameTable PreserveWhitespace : False IsReadOnly : False InnerXml : SchemaInfo : System.Xml.Schema.XmlSchemaInfo BaseURI : Value : ChildNodes : {} PreviousSibling : NextSibling : Attributes : FirstChild : LastChild : HasChildNodes : False NamespaceURI : Prefix : InnerText : OuterXml : > $baz.gettype() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True False XmlDocument System.Xml.XmlNode
Now, let's try to initialize a new variable to the result of a static method call.  I'm sure you get the drill by now.
> new-variable -name qux -value [math]::floor(3.5) New-Variable : A positional parameter cannot be found that accepts argument '3.5'. At line:1 char:13 + new-variable <<<< -name qux -value [math]::floor(3.5) + CategoryInfo : InvalidArgument: (:) [New-Variable], ParameterBindingException + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.NewVariableCommand > new-variable -name qux -value ([math]::floor(3.5)) > $qux 3 > $qux.gettype() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Double System.ValueType
There's another one that's more difficult to demonstrate where the interpreter will implicitly flatten a single-element array to its sole value, which is annoying when the code around it is expecting an array.  I could go on and on listing implicit behaviors and how to explicitly define them to get your code to do what you want, but the lesson is already plainly obvious: Be very vigilant while writing and testing your code.  Always keep the interpreter nearby to test things and make sure that they work.

Also, if you point out the implicit behavior that all of my code examples are using without turning it into the equivalent explicit behavior, you get a cookie.

No comments:

Post a Comment

I moderate comments because when Blogger originally implemented a spam filter it wouldn't work without comment moderation enabled. So if your comment doesn't show up right away, that would be why.