Be careful with that scope

I did some more reading on Powershell variable scope definition today and thought I’d write up what I found.  I’m undecided on the best practice for writing functions especially given the patchy documentation on passing variables to a function.  One way is to create some variables in the main script and just use them in the function although there are some caveats to doing that which I’ll cover in a second and this is definitely not best practice.  The other way is to explicitly pass the variables to the script, even ones you are not going to change within the function itself.  The benefit of the latter option is when reusing the function in another script you can see all the variables that need to be available in the parameter definition.  This approach can lead to long parameter lists and for simplicity I think most people tend to use the former strategy and this is where a discussion on variable scope is useful because it can have a big impact on the functionality of the script.

Variables have the following scope: GLOBAL, SCRIPT , LOCAL and PRIVATE

  • Global – available everywhere in the run space – so even without dot sourcing the script the variable will be available at the command line. ( dot sourcing is this  . .\mypowershellscript.ps1 the period followed by a space makes the script stay loaded in memory after it completes and all script variables are available at the command line or to any subsequent scripts you run in the same run space).
  • Script – available everywhere inside the script and inside any scripts called within the parent script.  As far as the script is concerned this is the same as Global because it’s available in every function too.  When the script finished though the variable is destroyed and you cannot reference it from the command line.  NOTE: this does not apply within an ISE which will often make every variable available as if it’s a global variable.  This means you need to test your script at least once from the command line if it calls other scripts.  What works in the ISE may not when you run it as a standalone script.
  • Local – Is this really a scope?  Local really means that the variable is accessible in the current scope.  So a script scope variable is local to the script and also local in any called function available within the script.
  • Private – is only available within the local scope where it was created and does not bleed into any other scope, even if you create the variable at the beginning of the main script the variable cannot be seen inside any of the called functions or scripts.

Pretty complex then!

here’s a little script that demonstrates this and it’s a modified one taken from this blog posting: http://www.jsnover.com/blog/2013/03/17/variable-scopes-and-private/

But before I get into the script just some quick notes for you……got your pens ready?

  1. You can create a variable by simply assigning it a value, even if that value is NULL, e.g. $MyVariable = $null. It’s quicker to create a variable by just declaring it and assigning a value which is why most people do it this way.
  2. A variables scope is assigned implicitly based on where it is created, e.g. if you create it in the main script is it ‘Script Scope’ and if you create it in a function it is effectively ‘Private  Scope’ to that function or more commonly referred to as ‘Local Scope’, i.e. it cannot be seen outside of the function.
  3. You can specify a variables scope when creating it by using the ‘new-variable’ cmdlet and specify the scope using the -scope parameter or just use Scope “Prefixing”, i.e. $<scope>:<variableName>.
    It’s not recommended to create variables of ‘Global Scope’ in case they interact with other scripts running at the same time.
  4. A variable in the main script, if its not inside a function is is ‘script scope’ unless you explicitly define it as global or Private using $global:MyVariable or $private:MyVariable.
  5. All child functions have ‘read access’ to the variables created by the parent or grandparent or great gran parent or …. you get the idea.
  6. If you don’t prefix the variable then PowerShell will search for the variable in the ‘Local Scope’ first , then ‘Script Scope’ and then finally ‘Global Scope’ before giving up and sulking.  Don’t forget point 4 if the variable is not in the ‘Local Scope’ then it’s a read only variable but read ahead to point 8 before you draw any conclusions 🙂 ( this is a bit like Bill and Teds excellent Adventure – just not as interesting)
  7. When assigning a value to a variable inside a child function, e.g. $MyVariable = 2, PowerShell will search the ‘Local Scope’ to see if it can find the variable and if it does then it updates the value if it can’t then it creates a new ‘Local Scope’ variable to store the value.  Read that carefully!  If the variable was instantiated in the main script then it assumes that you want to create a new ‘Local Scope’ variable as it can’t update the ‘Script Scope’ variable this is read only see point 5, e.g. Setting $myVariable = 1 in the main script and then setting it’s value to 2 inside the function like this $MyVariable = 2 is probably not doing what you expected it to do.  PowerShell creates a ‘Local Scope’ variable in the child function and when you return to the ‘Parent Scope’  calling line, $MyVariable is 1 not 2.  The two variables are in different scopes and now you are back in the main script you are looking at the ‘script scope’ variable which was set to 1.
  8. You can still update a parent scope variable inside a child scope function if you use Scope “Prefixing”, i.e. $script:MyVariable = 1 will update the ‘script scope’ variable.
  9. Point 7 can mean a world of pain when you apparently overwrite variables in child scopes ( functions called from the main script)
  10. It’s far too easy to be sloppy when programming and naming variables – and even easier to get spaghetti sauce on a white shirt.

#start with a clean slate

Remove-Variable -Name ScriptBlock -ErrorAction Ignore  

$ScriptBlock = “top”  

$Private:onlyinmain = “only in the main”

“======================================================”

“In main,ScriptBlock = “ + $ScriptBlock  

“In main, middlevariable = “ + $middlevariable

“In main, onlyinmain = “ + $onlyinmain

“======================================================”
function child 

{  

    “======================================================”

    “In child, ScriptBlock = “ + $ScriptBlock

    “In child, Script:ScriptBlock = “ + $Script:ScriptBlock

    “In child, onlyinmain = “ + $onlyinmain 

    “In child, middlevariable = “ + $middlevariable

    “======================================================”

}  

function middle  

{  

    $Private:ScriptBlock = “middle”  

    $middlevariable = “Middle”  

    “======================================================”

    “In middle, ScriptBlock = “ + $ScriptBlock  

    “In middle, Script:ScriptBlock = “ + $Script:ScriptBlock

    “In middle, onlyinmain = “ + $onlyinmain 

    “In middle, middlevariable = “ + $middlevariable

    “======================================================”

    child  

}  

middle

“======================================================”

“In main,ScriptBlock = “ + $ScriptBlock  

“In main, middlevariable = “ + $middlevariable

“In main, onlyinmain = “ + $onlyinmain

“======================================================”

. middle

“======================================================”

“In main, middlevariable = “ + $middlevariable

“======================================================”

There are two functions middle and child.  Child is nested in the middle function so is only available to the middle function.  You can’t call it from the main script.  At the start of the script I create a private variable just to prove you can’t see it anywhere but in the main script body.  Then I call the middle function and this prints to screen the values of the variables – then middle calls child and does the same.  Notice the variable scriptblock in middle is of private scope to middle and is not available in child – so Powershell searches back through the scopes and finds the scriptblock variable we set in the main script.   the variable we create in middle, middlevariable is available in middle and child but not in the main script.

======================================================
In main, ScriptBlock = top
In main, middlevariable =
In main, onlyinmain = only in the main
======================================================
======================================================
In middle, ScriptBlock = middle
In middle, Script:ScriptBlock = top
In middle, onlyinmain =
In middle, middlevariable = Middle
======================================================
======================================================
In child, ScriptBlock = top
In child, Script:ScriptBlock = top
In child, onlyinmain =
In child, middlevariable = Middle
======================================================
======================================================
In main, ScriptBlock = top
In main, middlevariable =
In main, onlyinmain = only in the main
======================================================
======================================================
In middle, ScriptBlock = middle
In middle, Script:ScriptBlock = middle
In middle, onlyinmain = only in the main
In middle, middlevariable = Middle
======================================================
======================================================
In child, ScriptBlock = middle
In child, Script:ScriptBlock = middle
In child, onlyinmain =
In child, middlevariable = Middle
======================================================
======================================================
In main, middlevariable = Middle
======================================================

So what’s the best strategy for calling functions and using variable scope…… Hmmmm I’ll need to think about this some more before I can post my opinions on this one now that I have a better understanding of scope I may  change my current script writing style to utilise this knowledge.

If your still with me and interest why I suddenly started researching this – it’s because I’ve been handed over the development of an application built on a bunch of powershell scripts written by someone else.  Their style was to use . sourcing every where and call global variable scope which works but it’s very hard to follow and debug.  We need to maintain some variable naming standards and always think about the scope of variables.  I’ve grown to hate . sourcing unless it’s for a quick and dirty script when is when that technique is brilliant.  In a production level script I’d advise you stay away from it.  The other thing I encountered in these scripts was using dos command and paths based on using dots to navigate to different directories – wow, like I don’t have enough things to worry about now we just added folder location of the running script and all the .sourced scripts running in different locations.

One last gripe with . sourcing before I go, when you get an error it give you a line number that nears no relation to the line number in any of the scripts and the actual line of code could be in any one of the . sourced scripts called in the main script.  ARRRRGHHHHHH  – nothing a few beers wont fix though – well you wont care about fixing it, for a while at least and to be fair us scripters could probably do with being more sociable so maybe that’s not such a bad thing after all 🙂 .

Here are some more links discussing variable scope – if I haven’t already bored you to death with this post.

http://blogs.technet.com/b/heyscriptingguy/archive/2010/02/11/hey-scripting-guy-february-11-2010.aspx

http://www.jsnover.com/blog/2013/03/17/variable-scopes-and-private/

http://www.sapien.com/blog/2013/03/06/first-rule-of-powershell-scoping-rules/

Sadly I’ve just broken rule No. one from the link above…..

Command line polution – persistent variables

PowerShell variables have scope.  Scope means where the variable exists in memory and who can have access to the variables. Variables can have global, local, private or script and this is why you can have the same variable name used in multiple functions as PowerShell keeps track of the variables scope as the script runs. This is one reason why, except at the command line, I prefer not to use the in process object $_ as this can change and can get confusing in nested loops.  Sadly it’s often more efficient to use the pipeline and then of course you have to use the in process object.

If you want more details on variable scope try reading this post :  http://blogs.msdn.com/b/powershell/archive/2007/04/14/controlling-the-scope-of-variables.aspx

Normally when you run a PowerShell script the variables you create are destroyed when the script finishes. The same applies when the script returns from a function, any variables created inside the function are recovered and are not available outside of the function when back in the main part of the script – unless of course you explicitly change the scope of the variable when you create it using New-Variable and the -scope switch.

If you dot source a script then the variables become available at the command line.  This can be really useful, especially when debugging a script.  Because even after the script has run you can access the variables and manipulate them.  You can even call the functions that were part of the PowerShell script.

This isn’t always a benefit though.    Sometimes the variables that get left in memory can trip you up, especially when working within an ISE.  I prefer then, to make sure things are cleaned up and I use a little function to do this.

The meat of this code is Get-Variable which returns all the variables currently in scope and then I pipe this into a Where-Object script block that calls Remove-Variable -name $($_.name) -Force -Scope Global
this will clear out all the variables in memory.

If that were all there was to it then my post would be finished, but what if I wanted some variables to be persistent.  I can get around variable pollution by always instantiating the variables in my script and setting them to known values or clearing them by setting them to $null or an empty string in my script header and it’s good practice to do this too.

When I start my script I instantiate any variables I want to remain after the script has run then I call my clean-memory function and this looks for a Global array variable called $Global:startupVariables.  If it exists then it will remove any variables that are not in the array by doing a simple comparison -notcontains $_.name .  If the variable doesn’t exist, as would be the case at the start of my script run then it creates the variable with global scope using New-Variable -name startupVariables -Force -Scope “Global” -value ( Get-Variable | ForEach-Object { $_.Name } which is the same trick I used to clear all the variables using Get-Variable to list the existing variables.

Here’s the full code – just put it at the top of your script, instantiate any variables you want to keep and then call the function.  Then right at the end of your script call clean-memory again to clear all the variables from memory.

function Clean-Memory {
<# .SYNOPSIS Removes all variables from memory that did not exist before this script was run. .DESCRIPTION Removes all variables from memory that did not exist before this script was run.                 The script uses a global variable to record any existing variables before the   script is run and uses this to idetnify new variables which must have been   created during the script run.                 $Global:startupVariables          Call the function at the beginning of a script and then call it at the end   to clear all the veriables created during the script run.                 The script uses the Remove-Variable cmdlet to force the deletion of the variables not stored in the $Global:startupVariables variable.                 The script does not have any input parameters .EXAMPLE Clean-Memory .INPUTS None .OUTPUTS A global variable $Global:startupVariables .NOTES NAME     : Clean-Memory VERSION  : Version 1.0 AUTHOR   : Lee Andrews CREATED  : 13th November 2012 LASTEDIT : 13th November 2012 - Version 1.0 .LINK Remove-Variable .LINK   http://collaborate/technology/eng/global/platform/ps/default.aspx        #>
 if ($Global:startupvariables) {
   # if the startup variable exists we assume the call is to clean up after a script run
    Get-Variable |
     Where-Object { $Global:startupVariables -notcontains $_.Name } |
      ForEach-Object {
       try { Remove-Variable -Name "$($_.Name)" -Force -Scope "global" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue}
       catch { }
      } # remove the variables from memory that are not in the startupVariable global variable
    # now clean the startupVariables
    try {Remove-Variable -Name startupvariables  -Scope "Global" -Force -ErrorAction SilentlyContinue }
    catch { }
    # just in case this is an inital run after the script had failed in the last run lets set up the variable again
    New-Variable -name startupVariables -Force -Scope "Global" -value ( Get-Variable | ForEach-Object { $_.Name } )
  }  # If the global variable startupVariables exists then remove any variables that are not in it.
  else {
    New-Variable -name startupVariables -Force -Scope "Global" -value ( Get-Variable | ForEach-Object { $_.Name } )
  }                           # Else - Store all the start up variables in startupVariables so you can clean up when the script finishes.
} # Removes all redundant variables from memory
#