Sunday, July 6, 2008

Fun and Excitement with Variable Scoping!!!

Since PowerShell's syntax in scripts reminds me of Perl, my fingers keep trying to type the word "my" before I set every variable (you Perl geeks know what I'm talking about). Anyway, that got me thinking about scoping, which is amazing and exciting, and the most fun topic in the world.

Okay, so I was being sarcastic, but it is pretty important, and it turns out that there is a very good article on scoping right here. I'll try to break it down to a shorter, simpler version for the purposes of the blog, so here goes:

Let's say I do this:

PS C:\> $x = "Global"
PS C:\> function print_var {echo $x}
PS C:\> print_var

You'd expect it to output the string "Global", right?

What if I do this?

PS C:\> echo $x
PS C:\> function set_var {$x = "Local"; echo $x}
PS C:\> set_var
PS C:\> echo $x

You might expect the output to be

Global
Local
Local

but it's not. The output is

Global
Local
Global

That's because by default each variable is set in the local scope. In the case of the function set_var, a new variable named $x is created inside the script block that masks the global $x variable that was set outside of it. When PowerShell looks up the value of a variable, it starts in the local scope and then works its way up, to the parent scope, and then its parent, and so on until it reaches the global scope. If you accidentally create a new variable with the same name at a lower scope, PowerShell won't care that you also have a global variable with that name because it will stop looking as soon as it finds one.

The article I referenced earlier has some neat tricks you can do to set scope, and you can look up the help for Set-Variable for more info.

As a general rule it is best to just not set the value of a global variable inside a script block. It is better to return the value of the local variable as the output of the function and use that to set the variable in the parent scope. If you must set a global variable inside a script block, though, there are two ways to do it:

#1

PS C:\> echo $x
PS C:\> function set_var {$global:x = "Local"; echo $x}
PS C:\> set_var
PS C:\> echo $x

#2

PS C:\> echo $x
PS C:\> function set_var {Set-Variable -scope global "Local"; echo $x}
PS C:\> set_var
PS C:\> echo $x

There are two easy rules that apply to any language and will help mitigate this issue without having to think about it too much, though:

Rule #1: Use as few global variables as absolutely necessary.
Rule #2: Never use generic names for variables like $x.