What Scope Am I In?

In PowerShell the question of scope is too complicated and convoluted. I’m going to try to help you understand it, but I’m not guaranteeing that I will be able to make it seem any simpler than it actually is. Hopefully, I won’t make it more complicated than it inherently is ;-)

In PowerShell you always have three named variable scopes: script, local and global. The default scope is always the same as the local scope, so when you set a variable without specifying the scope, it’s always set at local scope. One thing to note is that you can access these named scopes through the $variable notation, or through the variable drive, so all of the following are equivalent:


Set-Variable var "one" -Scope Local
$var = "one"
$local:var = "one"
Set-Content Variable::Local:Var "one"
 

A side note: the PSProvider drive notation means that when you’re in strict mode, if you want to test for the existence of a variable without causing an error, the simplest way to do it is with Test-Path Variable:Var

What’s so hard about that?

Well, the question is: what scope are you in “right now” when a line of code is executing from a function or a script …

Sometimes local scope is ALSO script scope, and sometimes, script scope is also global scope. Specifically: when you’re typing at the console, all three scopes are the same.

Let’s write a function to determine what named scope we’re in:


function Get-Scope {
   New-Variable scope_test
   Write-Output @{
      $GlobalScope = [bool](Get-Variable scope_test -Scope global -ea 0)
      $ScriptScope = [bool](Get-Variable scope_test -Scope script -ea 0)
      $LocalScope = $true
   }
}
 

By setting a variable, and then testing the named scopes, we can verify what scope we were in when we set the variable. Of course, there’s no point testing the Local scope, because we already know that is where we are… then we return a hashtable of booleans indicating which scopes are active.

If you dot-source that Get-Scope function, you’ll find the scope that you’re in where you dot-source it:

  • if you do it at the interactive prompt it should tell you all three scopes are set
  • if you do it in a script file it should tell you Local and Script
  • but if you do it in a function, it will always just tell you “local” — and will not tell you if the function is in a script or not … nor how deep you are.
  • and if you do it in a module, the results will depend on whether Find-Scope is defined in the module or not (this is very weird)

So are we done?

Not even remotely. On top of the named scopes, PowerShell also has nested scopes. Each script, function, scriptblock, etc. adds to the nested scopes, and takes you further from the global scope. On top of that, PowerShell has Modules. In a module, scope is flattened. Specifically, in a module, the default scope becomes the Script scope, which in this case is not actually reserved for a single file, but for any scripts, functions, etc that are executed from within that module’s context.

To determine nesting level of the scope, we must do something like this:


function Get-ScopeDepth {
   trap { continue }
   $depth = 0
   do {
     Set-Variable scope_test -Scope (++$depth)
   } while($?)
   return $depth - 1
}

# Test it like this:
. Get-ScopeDepth
&{ . Get-ScopeDepth }
&{ &{ . Get-ScopeDepth } }
 

As long as modules aren’t involved, that will tell you how many scopes there are between you and global scope (and thus, return zero when dot-source from the console commandline).

This falls apart a bit if you’re in a module (in a module, you can’t get to global scope by increasing the scope value — effectively, the highest you can go is script scope, but in reality you can still access global scope by naming it). To determine if you’re in a module, you can simply check for the existence of the $PSScriptRoot variable, or verify that $Invocation.MyCommand.Module is set to something.

Additionally, if you’re in a module, you need to define this function in that module for it to work at all.

There’s one more thing you could use to help you learn what scope you’re in, and that is the Get-PSCallStack cmdlet. This will tell you how many commands have been called to get you where you are. It’s usually the same as the Scope Depth, but not when it’s in a script file, etc.

Here’s my finished scope digging function:


function Get-Scope {
   [CmdletBinding()]
   Param(
      [Parameter(Mandatory=$true)]
      [System.Management.Automation.InvocationInfo]$Invocation
   )

   function Get-ScopeDepth {
      trap { continue }
      $depth = 0
      do {
        Set-Variable scope_test -Scope (++$depth)
      } while($?)
      return $depth - 1
   }
   $depth = Get-ScopeDepth
   
   Remove-Variable scope_test
   New-Variable scope_test

   New-Object PSObject @{
      ModuleScope = [bool]$Invocation.MyCommand.Module
      GlobalScope = [bool](Get-Variable scope_test -Scope global -ea 0)
      ScriptScope = [bool](Get-Variable scope_test -Scope script -ea 0)
      ScopeDepth  = $depth
      PipelinePosition = $Invocation.PipelinePosition
      PipelineLength = $Invocation.PipelineLength
      CallStack = Get-PSCallStack
      StackDepth = (Get-PSCallStack).Count - 1
      Invocation = $Invocation
   }
}
 
  • Remember: modules sometimes flatten scope.
  • Remember: when calling this function you should dot-source it, and pass it $MyInvocation

As as side note and counter-example, take for instance this module:


New-Module {
   function test-scope { . Get-Scope $MyInvocation }
   function test-nestedscope { test-scope }
   function test-nestednestedscope { test-nestedscope }
} | Import-Module
 

All three of those functions will return ScopeDepth = 2 (the test-scope function, and the module itself), but the StackDepth will increase correctly. I don’t know why. It’s late, so I’m going to stop writing before I get more confusing.

Similar Posts:

One thought on “What Scope Am I In?”

Comments are closed.