PS-Scope_Final-001

In batch scripts, changes to environment variables have a global impact to the current session by default. For PowerShell, the exact opposite is true because scopes are used to isolate a script’s modifications. Here, we’ll explore how scopes affect PowerShell scripts and how to work in and around them.

What is a Scope?

In PowerShell, a “scope” refers to the current environment in which a script or command shell is operating. Scopes are used to protect certain objects within the environment from being unintentionally modified by scripts or functions. Particularly, the following things are protected from modification by commands run from another scope, unless specified otherwise by parameters in those commands:

  • Variables
  • Aliases
  • Functions
  • PowerShell Drives (PSDrives)

New scopes are created whenever you run a script or function, or when you create a new session or instance of PowerShell. Scopes created by running scripts and functions have a “parent/child” relationship with the scope from which they were created. There are a few scopes that have particularly special meanings, and can be accessed by name:

  • The Global scope is the scope that is created when PowerShell starts. It includes the variables, aliases, functions, and PSDrives that are built-in to PowerShell as well as any which are made by your PowerShell profile.
  • The Local scope refers to whatever the current scope is. When you start PowerShell it will refer to the Global scope, within a script it will be the Script scope, etc.
  • The Script scope is created when a script is run. The only commands that operate within this scope are those that are in the script.
  • Private scopes can be defined within the current scope, to prevent commands in other scopes from being able to read or modify items they might otherwise have access to.

Scopes can also be referred to by number in certain commands, where the current scope is referred to as zero and its ancestors are referenced by increasing integers. For example, within a script run from the Global scope, the Script scope would be 0 and the Global scope would be 1. A scope that was further nested within the Script scope, such as a function, would refer to the Global scope as 2. Negative numbers will not work to reference child scopes though – the reason for this will be apparent shortly.

How Scopes Affect Commands

As mentioned earlier, commands executed within one scope will not affect things in another scope unless specifically told to do so. For example, if $MyVar exists in the Global scope and a script runs a command to set $MyVar to a different value, the Global version of $MyVar will remain unaltered while a copy of $MyVar is placed in the Script scope with the new value. If a $MyVar doesn’t exist, a script will create it within the Script scope by default – not in the Global scope. This is important to remember as you learn about the actual parent/child relationship between scopes.

The parent/child relationship of scopes in PowerShell is one-way. Commands can see into, and optionally modify, the current scope, its parent, and any scopes above that. However, they cannot see or modify things in any children of the current scope. This is primarily because, once you’ve moved into a parent scope, the child scope has already been destroyed because it has fulfilled its purpose. For example, why would you have to see or modify a variable in the Script scope, from the Global scope, after the script has terminated? There’s plenty of cases where you need a script’s or function’s changes to persist beyond its completion, but not so many where you’d need to make changes to objects within the script’s or function’s scope before or after it is run. (Usually, such things will be handled as part of the script or function itself anyway.)

Of course, what are rules without exceptions? One exception to the above are Private scopes. Objects in the Private scopes are only accessible to commands run in the scope from which they were created. Another important exception is items which have the AllScope property. These are special variables and aliases for which a change in any scope will affect all scopes. The following commands will show you which variables and aliases have the AllScope property:

Get-Variable | Where-Object {$_.Options -match 'AllScope'}
Get-Alias | Where-Object {$_.Options -match 'AllScope')

Scopes in Action

For our first look at scopes in action, we’re going to start in a PowerShell session where the variable $MyVar has been set to a string, ‘I am a global variable!’, from the command line. Then, the following script will be run from a file called Scope-Demo.ps1:

Function FunctionScope
{
    'Changing $MyVar with a function.'
    $MyVar = 'I got set by a function!'
    "MyVar says $MyVar"
}
''
'Checking current value of $MyVar.'
"MyVar says $MyVar"
''
'Changing $MyVar by script.'
$MyVar = 'I got set by a script!'
"MyVar says $MyVar"
''
FunctionScope
''
'Checking final value of MyVar before script exit.'
"MyVar says $MyVar"
''

If PowerShell scripts worked the same as batch scripts, we’d expect the vale of $MyVar (or %MyVar% in batch syntax) to change from ‘I am a global variable!’, to ‘I got set by a script!’, and finally to ‘I got set by a function!’ where it would stay until it’s explicitly changed again or the session is terminated. However, see what actually happens here as we move through each of the scopes – particularly, after the FunctionScope function has completed its work and we check the variable again from the Script, and later the Global, scope.

image

As you can see the variable appeared to change as we moved through the script because, up until the FunctionScope function was completed, we were checking on the variable from within the same scope it was last changed. After FunctionScope was done though, we moved back into the Script scope where $MyVar was left untouched by the function. Then, when the script terminated, we came back out into the Global scope where it hadn’t been modified at all.

Reaching Outside the Local Scope

So, this is all well and good to help you keep from accidentally applying changes to the environment beyond your scripts and functions, but what if you actually do want to make such modifications? There’s a special, and fairly simple, syntax for creating and modifying objects beyond the Local scope. You just put the scope name at the start of the variable name, and put a colon between the scope and variable names. Like this:

$global:MyVar
$script:MyVar
$local:MyVar

You can use these modifiers both when viewing and setting variables. Let’s see what happens with this demonstration script:

Function FunctionScope
{
    ''
    'Changing $MyVar in the local function scope...'
    $local:MyVar = "This is MyVar in the function's local scope."
    'Changing $MyVar in the script scope...'
    $script:MyVar = 'MyVar used to be set by a script. Now set by a function.'
    'Changing $MyVar in the global scope...'
    $global:MyVar = 'MyVar was set in the global scope. Now set by a function.'
    ''
    'Checking $MyVar in each scope...'
    "Local: $local:MyVar"
    "Script: $script:MyVar"
    "Global: $global:MyVar"
    ''
}
''
'Getting current value of $MyVar.'
"MyVar says $MyVar"
''
'Changing $MyVar by script.'
$MyVar = 'I got set by a script!'
"MyVar says $MyVar"

FunctionScope

'Checking $MyVar from script scope before exit.'
"MyVar says $MyVar"
''

As before, we’ll start by setting the variable in the Global scope and end with checking the final Global scope result.

image

Here you can see that FunctionScope was able to change the variable in the Script scope, and have the changes persist after it was completed. Also, the change to the variable in the Global scope persisted even after the script had exited. This can be particularly useful for if you have to repeatedly change variables within a script, or within the Global scope, using the same code – you just define a function or script that’s written to modify the variable where and how you need it done, and call on that whenever those changes are necessary.

As mentioned earlier, scope numbers can also be used in certain commands to modify the variable at different levels in relation to the Local scope. Here’s the same script used in the second example above, but with the function modified to use the Get-Variable and Set-Variable commands with scope numbers instead of directly referencing the variable with named scopes:

Function FunctionScope
{
    ''
    'Changing $MyVar in scope 0, relative to FunctionScope...'
    Set-Variable MyVar "This is MyVar in the function's scope 0." –Scope 0
    'Changing $MyVar in scope 1, relative to FunctionScope...'
    Set-Variable MyVar 'MyVar was changed in scope 1, from a function.' –Scope 1
    'Changing $MyVar in scope 2, relative to Functionscope...'
    Set-Variable MyVar 'MyVar was changed in scope 2, from a function.' –Scope 2
    ''
    'Checking $MyVar in each scope...'
    ‘Scope 0:’
    Get-Variable MyVar –Scope 0 –ValueOnly
    ‘Scope 1:’
    Get-Variable MyVar –Scope 1 –ValueOnly
    ‘Scope 2:’
    Get-Variable MyVar –Scope 2 –ValueOnly
    ''
}
''
'Getting current value of $MyVar.'
"MyVar says $MyVar"
''
'Changing $MyVar by script.'
$MyVar = 'I got set by a script!'
"MyVar says $MyVar"

FunctionScope

'Checking $MyVar from script scope before exit.'
"MyVar says $MyVar"
''
image

Similar to before, we can see here how commands in one scope can modify objects in its parent scope.

Additional Information

There’s still much more that can be done with scopes than can fit in this article. Scopes affect more than just variables, and there’s still more to be learned about Private scopes and the AllScope variables. For more useful information, you can run the following command from within PowerShell:

Get-Help about_scopes

The same help file is also available on TechNet.


Scope image credit: spadassin on openclipart