PowerShell Scopes: Understanding Variable Scope

Published:27 September 2019 - 11 min. read

Azure Cloud Labs: these FREE, on‑demand Azure Cloud Labs will get you into a real‑world environment and account, walking you through step‑by‑step how to best protect, secure, and recover Azure data.

When you write a PowerShell script with no functions and no external dependencies on other scripts, the concept of PowerShell scopes isn’t of much concern. The concept of PowerShell global variables isn’t front and center. But as you begin to build functions, modules and learn to call scripts from other scripts, the topic becomes more important.

In this article, you’re going to get a deep lesson in what scopes in PowerShell are, how they work, and how you can write code with scopes in mind. By the time you’re done, you’ll understand PowerShell’s global variables and a lot more!

Scopes: Kinda Like Buckets

Have you ever written a script where you define a variable and when you check the value of that variable, it’s something else? You may scratch your head at how that variable changed when you clearly defined it. One reason may be the variable’s value is getting overwritten in another scope.

Maybe you’ve wondered how certain PowerShell variables have values when you reference them in your console but don’t exist in your scripts. Chances are those variables are in another ‘bucket’ that’s not available at that time.

Scopes are like buckets. Scopes affect the way PowerShell isolates variables, aliases, functions, and PSDrives between different areas. A scope is like a bucket. It’s a place to collect all of these items together.

When PowerShell starts, it automatically creates these “buckets” for you. At that point, you’re already using scopes without realizing it. All scopes are defined by PowerShell and get created without any help on your part.

Scope Types

When PowerShell fires up, it automatically creates four “buckets” or scopes for various items to be placed in. You cannot create scopes on your own. You can only add and remove items from these scopes defined below.

Global Scope

Items defined when PowerShell opens are set at the global scope. These items include system-created objects like PowerShell drives and also anything you have defined in a PowerShell profile since your profile runs at startup.

Items in the global scope are available everywhere. You can reference items in the global scope interactively on the console, in any script you run, and in any function. PowerShell’s Global variables are everywhere. For this reason, the common use of PowerShell global variables is to use PowerShell global variables between scripts.

There is only one global scope that rules overall.

Script Scope

A script scope is automatically created every time a PowerShell script runs. You can have many different script scope instances. Script scopes are created when you execute a PS1 script or a module, for example.

Only items created in that particular script scope instance can reference each other.

Private Scope

Typically, an item defined can be accessed from other scopes – not true with items in a private scope. Items in a private scope, contain objects that are hidden to other scopes. A private scope is used to create items with the same name as items in other scopes to prevent overlap.

Local Scope

Unlike the other scopes, the local scope is a bit different. The local scope is a pointer to the global, script or private scope. The local scope is relative to whatever context the code runs in at the time.

If you create a variable, alias, function, or PSDrive without explicitly assigning it a scope (which we’ll cover later) it will go into the local scope.

Referencing Scopes

Now that you have an idea on the four types of scopes that exist, you should also know there are two ways to reference those scopes.

In PowerShell, there are two ways to reference scopes – named or numbered. Both methods are referencing the same scopes but simply in a different way. These are two different ways of interacting with scopes.

Named Scopes

Above in the Scope Type section, you learned about scopes referenced by name. Referencing a scope by name is, intuitively, called a named scope. The main purpose of referencing a scope by name is to assign an item to a scope. You’ll learn how to do this below.

Numbered Scopes

Along with a name, each scope has a number starting at zero which will always be the local scope. Scopes are numbered dynamically in relation to the current local scope.

For example, as soon as you open a PowerShell session you are operating in the global scope. At this point, the global scope is the local scope (remember the local scope is just a pointer).

Since the local scope is always scope zero, at this point, the global scope is also currently scope zero. But, when you run a script from that same session, a script scope is created. When running, the local scope pointer has then changed to the script scope. Now the script scope is scope zero and the global scope is scope one.

Scopes are numbered This process repeats for as many scopes as you have where the local scope is 0. Scopes are dynamically numbered by scope hierarchy.

Scope Hierarchy and Inheritance

As mentioned earlier, when you launch a PowerShell session, PowerShell creates some items for you in the global scope. These items can be functions, variables, aliases, or PSDrives. Anything you define in your PowerShell session will be defined in the global scope also.

Since you are in the global scope by default, if you do something that creates another scope like executing a script or running a function, a child scope will be created with the parent being the global scope. Scopes are like processes with parents and children.

Anything that is defined in a parent scope, the global scope, in this case, will be accessible in the child scope. But these items are only editable in the scope they were defined in.

Let’s say you have a script called Test.ps1. Inside of this script is a single line as shown below.

$a = 'Hello world!'

When you run this script, $a is assigned a value in the local scope (script while the script is running). When Test.ps1 is run, you can see below that you’re unable to reference it after the script executes. Since $a was assigned while in the script scope, the global scope (at the interactive console) can’t see it.

Global scope cannot see the variable
Global scope cannot see the variable

Let’s take this example a step further and make the Test.ps1 script look like below. The script is now attempting to output the value of $a before setting it in the same scope.

Write-Output $a
$a = 'Hello world!'

To demonstrate, assign a value to $a at the interactive console. This assigns the value at the global scope. Now, the script runs, it will inherit the parent scope (global) and should be able to see the value.

You can see below that when Test.ps1 is executed (creating a child scope of the global scope), it can see the value of $a. You can also see that the variable’s value is available at the global scope also since this scope is where it was set at. This means that $a is available both in the script (child) and parent (global) scopes.

Variable is available in script and global scopes
Variable is available in script and global scopes

Remember this scope inheritance behavior. By doing so will help you when troubleshooting occasions variable conflicts will occur such as variables in different scopes with the same name.

Defining and Accessing Items in Scopes

Now that you know what a scope is and how they work, how do you access them? Let’s see how to use PowerShell to set a variable scope (and access them).

Get/Set-Variable

In PowerShell, there are two cmdlets that allow you to set variables called Get-Variable and Set-Variable. These cmdlets allow you to retrieve the value of a variable or define a value.

Both cmdlets are similar with a Name and Scope parameter. Using these parameters, PowerShell allows you to set and retrieve variable values across all scopes.

Local Scopes

To set a variable in a local scope, use Set-Variable and provide it a local variable name and a value as shown below.

PS> Set-Variable -Name a -Value 'foo'

The local scope is always the default so not using the Scope parameter will always define the variable in the local scope.

To retrieve the value of a locally-scoped variable, use Get-Variable providing it the name.

PS> Get-Variable -Name a

Name         Value
----         -----
a            foo

Private/Script/Global Scopes

You’ll use the same parameters (Name and Value) when working with private, script, and global variables as well. The only difference is this time you’ll use the Scope parameter to explicitly define the scope.

The methods for setting a private, script, or globally-scoped variable are the same. Simply replace the value passed to the Scope parameter as Private,  Script  Global.

PS> Set-Variable -Name a -Value 'foo' -Scope <Private|Script|Global>

To retrieve the value of a script or globally-scoped variable, use Get-Variable providing it the name and scope.

PS> Get-Variable -Name a -Scope <Script|Global>

Name         Value
----         -----
a            foo

Note: You can also reference scopes using scope numbers instead of names with the Get/Set-Variable cmdlets.

Scope “Prefacing”

You can also retrieve and set variables in scopes using a shortcut. Instead of using PowerShell cmdlets, you will preface the scope to the variable when referencing it.

Local Scopes

Since the local scope is always the default, simply defining a variable and referencing will set and retrieve a local scope variable

PS> $a = 'foo'
PS> $a

foo

Private/Script/Global Scopes

If you’d like to define and reference script or global scopes, you can preface the variables with the scope name and a semicolon.

For example, to set the variable $a in the global scope, you can preface a with $global:.

$global:a = ‘foo’

The same can be done for a script-scoped variable.

$script:a = ‘foo’

Once the variables are set in the preferred scope, you’d then reference them the same way. Also, notice that you can exclude the scope preface if the defined scope is the local scope.

PS> $global:a = 'foo'
PS> $global:a
foo
PS> $a
foo

Scopes in Scriptblocks

PowerShell has a handy construct called scriptblocks. Scriptblocks allow you to move around snippets of code and execute them just about anywhere.

Like a PS1 script, scriptblocks run in their own script scope. When you execute a scriptblock, you’re essentially executing a PS1 script.

Notice in the example below where a variable is defined in the global scope and then attempted to overwrite in a script scope. As you learned above, this won’t work because a child scope can’t reference a parent scope.

A child scope cannot overwrite a parent scope
A child scope cannot overwrite a parent scope

This example shows that when $a is changed in the scriptblock, the global variable definition for $a is not changed because the scriptblock is a script child scope.

Dot Sourcing Scripts (Swapping Local Scopes)

PowerShell has a concept called dot-sourcing. This is a method that allows you to execute a PS1 script and bring everything that would be script-scoped into the local scope instead.

By putting a dot (.) before referencing a PS1 script and executing it, this “dot sources” the content of the script and brings everything into the local scope.

To demonstrate, I have a Test.ps1 script again which defines a variable as shown below.

$a = 'Hello world!'

In a PowerShell console, set a value for an $a variable then dot source this script as shown below. Notice that the original variable was overwritten. PowerShell “merged” the two local scopes together.

Original variable overwritten
Original variable overwritten

Using the AllScope Property (Option)

You have seen how to interact with items in specific scopes, but so far each item is still defined in a single scope. But what if you don’t know what scope a variable is defined in?

When defining a variable with the Set-Variable cmdlet, you can place the variable into all scopes at once. To do this, use the AllScope value for the Option parameter.

To demonstrate, the Test.ps1 script has been modified to set an a variable in all scopes. That variable is then output as shown below.

Set-Variable -Name a -Value 'Hello world!' -Option AllScope
Write-Output $a

You can then see below that a value is being set for $a in the global scope and the Test.ps1 script is executed. However, instead of having no effect, $a’s value has been overwritten. Not only has it been defined in the script scope (Write-Output $a) but it’s also overwritten the global scope.

Variable overwritten in global scope
Variable overwritten in global scope

The AllScope option is handy but be careful. This option essentially does away with the concept of scopes and merges everything together.

Function Scopes

When you execute a function, all code within that function is in its own child scope. Function scopes follow the same child/parent behavior as other scopes.

Having separate scopes for each function is a good idea. It allows better control over items not having to worry about items conflicting, It also gives the added benefit of automated cleanup of variables in a function. As soon as the function completes, all of the items defined in the function will be cleared.

To demonstrate, copy/paste the function below directly into the PowerShell console.

Function Do-Thing {
    $var = 'bar'
}

Once pasted, execute the function. Notice you cannot access the $var variable outside of the function.

PS> Do-Thing
PS> $var

Keeping Items Private (Disabling Inheritance)

Typically, if a variable is defined in a parent scope, that variable will be defined in the child scope. But perhaps you’d like to use a variable name but that variable name has already been defined in one of the scopes to be run in a session. In that case, you can either choose a different variable name or define the variable in a private scope making it a private variable.

A way to use scopes to reduce conflicts is to use the private scope. Using the private scope disables inheritance on that specific variable. When multiple child scopes are created, those child scopes will not see any variables defined in a private scope.

A Test.ps1 script outputs the value of $a as shown below.

Write-Output $a

You can see below I’m defining a privately-scoped variable at the global scope and then executing the Test.ps1 script. Usually, when you define a variable in a parent scope, that variable will be available in the child scope – not so with a privately-scoped variable.

In the below example, you can see that the script child scope created by executing the Test.ps1 script could not see the private-scoped $a variable defined in the parent scope.

Script scope cannot see the private-scoped variable
Script scope cannot see the private-scoped variable

Unlike global scopes or the AllScope option on the Set-Variable cmdlet, privately-scoped variables are an excellent way to compartmentalize items.

Scoping Best Practices

Thinking that defining variables in the global scope or using the AllScope option is the way to go is common. After all, all variables are available everywhere. There’s no need to worry about the complications of scopes. While this does provide additional freedom for access to what is defined, it can quickly get out of hand and get difficult to troubleshoot.

Instead of trying to prevent using scopes follow these tips:

  1. Instead of specifying scopes in functions, use parameters to pass the required information to the function.
  2. Stay within the local scope as much as possible.
  3. Instead of defining global variables from a script, use the Write-Output cmdlet to output everything and save it to a variable when needed from the console.

The main point here is to embrace scopes and learn to use them to your advantage instead of trying to circumvent them.

Further Reading

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!