Invocation Operators, States and Scopes

I’m going to start off with invocation operators, but don’t worry it’ll come around full circle. If you’re here just for scopes, you can skip to the PSModuleInfo section.

Common Knowledge

In general you can think of the invocation operators & and . as a way of telling PowerShell “run whatever is after this symbol”. There are a few different ways you can use theses symbols.

Name or Path

& .\Path\To\Command.ps1

Invoke whatever is at this path. The path can be a script, an executable, or anything else that resolves as a command.

& Get-ChildItem

Invoke a command by name. You don’t typically need an operator to invoke a command, but what you might not know is you can also use the . operator here to invoke a command without creating a new scope. For example:

function MyPrivateCommand {
    $var = 10
}

. MyPrivateCommand
$var
# 10

Invoke a ScriptBlock

& { Get-ChildItem }

There’s a ton of ways to use this and it could probably be it’s own blog post. There is one use though, that I don’t see a lot and want to talk about.

Let’s say you have a bunch of commands that output to the pipeline, but you really don’t care about their output. You aren’t going to assign them to a variable, so you would typically use Out-Null or $null = on each statement to ensure your pipeline remains clean. Instead, you can group those commands in a single scriptblock and null them all at once.

Lets take StringBuilder for instance. This class returns an instance of itself with every method. It does this so you can “chain” the methods together, like this.

$sb = [System.Text.StringBuilder]::new()
$sb.Append("This").AppendLine(" is a string").Append("Cool right?").ToString()
# This is a string
# Cool right?

But let’s say you can’t chain all the way through, you need to stop and check some things after you append to see what comes next in the string. If you do that often enough, you end up with a lot of lines you need to null.

Alternatively, just group them together and don’t worry about it.

$sb = [System.Text.StringBuilder]::new()
$isString = $false
$null = & {
    $sb.Append("This is a")
    if ($isString) {
        $sb.Append(" string!")
    } else {
        $sb.Append("... I'm not sure actually")
    }
}

$sb.ToString()
# This is a... I'm not sure actually

Less Common Knowledge

CommandInfo objects

CommandInfo is the object type returned by the Get-Command cmdlet. I tend to use this when I need to look a few different places for a command, or if I need to be very specific.

Lets say you’re looking for a executable. If they have it installed and in their $env:PATH, you want to use that. If they don’t, you want to install it to the current folder.

function Get-DotNet {
    $dotnet = Get-Command dotnet -ErrorAction Ignore
    if (-not $dotnet) {
        # Some code that saves the dotnet cli to ./dotnet
        $dotnet = Get-Command ./dotnet/dotnet.exe -ErrorAction Ignore
    }

    if (-not $dotnet -or $dotnet.Version -lt '2.0.0') {
        throw "Couldn't find dotnet!"
    }

    return $dotnet
}

& (Get-DotNet) build

At the end of Get-DotNet I don’t care where I got it from, or even what type of command it is. I can call it all the same. You can use Get-Command to narrow down a command to a specific version, from a specific module, in a specific path, of a specific command type and more.

PSModuleInfo

Here’s where things get cool. I’m going to be honest though, this is not widely applicable. If you read this and think “I can’t see myself ever using this” don’t worry, that’s normal. But a few of you might be screaming with excitement.

$module = Get-Module Pester

& $module { $ExecutionContext.SessionState.Module }
# ModuleType Version    Name                ExportedCommands
# ---------- -------    ----                ----------------
# Script     4.0.6      pester              {Add-AssertionOperator,

This one works a little differently. It isn’t invoking the module, instead it’s acting as a modifier. It basically tells PowerShell to run whatever comes after the module as if it was ran from within the module.

More specifically, it replaces the SessionStateInternal of the command with the SessionStateInternal of the modules SessionState.

Non-Exported Commands

Most modules have private functions, classes, variables, etc. that it uses internally, but aren’t meant to be used from outside the module. Using this method, you can do just that.

Let me be clear here though. If you do use them, you’re on your own. They are private for a reason. Maybe they are dangerous, maybe they are buggy in less than perfect scenarios, or maybe they are just obtuse to use. Whatever the reason, they are not supported. If it’s your module though, go nuts. It’s great for troubleshooting and testing.

Anyway, lets look at Pester again. If you’ve ever glanced at Pester’s source, you’ve probably seen they make a table of “safe” commands that aren’t mocked. Lets say you wanted to make sure a command was resolved correctly in that table.

& (gmo Pester) { $SafeCommands['New-Object'] }

# CommandType     Name           Version    Source
# -----------     ----           -------    ------
# Cmdlet          New-Object     3.1.0.0    Microsoft.PowerShell.Utility

You can also call functions, change the value of script scope variables, and even return classes without having to use the using module syntax.

Vessels for State

Here’s a fun fact, the SessionState property on PSModuleInfo is writeable. This means you can throw any old SessionState into the module and you now have a portable, invokeable link to that state. Even if the state isn’t from a module.

$globalState = [PSModuleInfo]::new($false)
$globalState.SessionState = $ExecutionContext.SessionState

$myGlobalVar = 'Hello from global!'
$myGlobalVar
# Hello from global!

$null = [PSModuleInfo]::new({
    $myGlobalVar = 'Hello from a module!'
})
$myGlobalVar
# Hello from global!

$null = [PSModuleInfo]::new({
    . $globalState { $myGlobalVar = 'Hello from a module!' }
})
$myGlobalVar
# Hello from a module!

In the first example, the variable changed in the module, but it didn’t change the original because of the different SessionState.

Enter a Modules Scope

Have you ever been troubleshooting a module and wished you could just enter the scope of the module, and run commands from the prompt like you were in the psm1?

. (gmo Pester) { $Host.EnterNestedPrompt() }

There you go, you’re now in the psm1. Run exit when you want to return to the global scope. Do you instead wish that it worked more like you were in a function? Switch the . with &.

Some of you may have read that last sentence and thought “Wait what? That’s not how I thought dot sourcing worked…” Keep reading, because that’s why I want to talk about what I call the “PowerShell State Tree”.

The PowerShell State Tree

Alright so I just made up the name “State Tree”, I don’t think there is an actual term that refers to the whole thing. The rest are real terms though :)

PowerShell Process
   |------ ExecutionContext
   |      |------ SessionStateInternal
   |      |      |------ SessionStateScope
   |      |             |------ SessionStateScope
   |      |             |      |------ SessionStateScope
   |      |             |------ SessionStateScope
   |      |
   |      |------ SessionStateInternal
   |             |------ SessionStateScope
   |
   |------ ExecutionContext
          |------ SessionStateInternal
                 |------ SessionStateScope

ExecutionContext

For every runspace in a process, there is an ExecutionContext. This holds instances of a lot of high level features you don’t ever need to worry about, like the help system, the parser, and most importantly, all of our SessionState objects.

The public facing version of this object is EngineIntrinsics which is confusingly returned from the variable $ExecutionContext.

SessionStateInternal

For every module in a runspace, there is a SessionStateInternal. Plus one “Top Level” or “Global” SessionStateInternal. These internally hold references to several SessionStateScope objects.

  • Global - Typically this scope is the link back to the top level state.
  • Script - Typically the psm1 file in a module or the first child scope otherwise. If no child scopes are created (like when running commands from the console) this scope is the same as global.
  • Current - The newest child scope, can be the same as script or global.

The public facing version of this object is SessionState. It is the object returned from $ExecutionContext.SessionState, PSModuleInfo.SessionState and pretty much anything else that calls itself “SessionState”.

SessionStateScope

I could probably write a whole post on scopes alone. Scopes are like a moment in time during the invocation of a script. Many scopes are created during a single script execution.

At the very least, any time a scriptblock is invoked, a new scope is created by default. Keep in mind, almost everything is boiled down or brought up to a scriptblock at some point in the invocation cycle. Functions, PowerShell class methods, modules, scripts, even the text you type at the prompt is or will be a scriptblock at some point. Cmdlets (compiled commands like Get-ChildItem) do not create scopes.

When a scope is created, it is added as a child to the Current scope of the session state the command belongs to. This small detail causes a lot of common misconceptions about how scopes work. When you first hear of scopes, you initially picture something more like the call stack. You assume that if you call a function from a module then the scope that gets created will be a child of the scope you are calling it in. But instead, it’s a part of it’s own separate tree of scopes entirely.

There is no public facing version of this object.

What is Dot Sourcing Really

Towards the start of this post, when describing the dot source operator I described it as “a way to invoke a command without creating a new scope”. The phrasing used was very specific and purposeful.

When you dot source something it runs in the scope that is marked as the current scope in the session state the command is from. That part is very important. It means that you aren’t creating a new scope, but you may still change scopes. Thankfully, invoking modules provides a very easy way to demonstrate this.

$pester = Get-Module Pester
# Creates a new scope in the Pester module.  This is the same context
# a function from the module would run in.
& $pester { $test; $test = 'test' }
& $pester { $test; $test = 'test' }

Both of the above calls return nothing because test was defined in a child scope that stops existing as soon as the command finishes.

$pester = Get-Module Pester
# What happens if we add another in the middle that is dot sourced?
& $pester { $test; $test = 'test' } # Nothing
. $pester { $test; $test = 'test' } # Nothing
& $pester { $test; $test = 'test' } # test
$test # Nothing

The first two still return nothing, but the third returns the value we set. This is because we dot sourced the second, so no new scope was created, therefore $test was defined in the current scope of the session state. This is just like if the code in the scriptblock was placed in the psm1 file directly.

The last line doesn’t return anything because it runs in the top level session state. Remember, dot sourcing runs the command in what is marked as the current scope of the session state for the command, it does not run the literal current scope.

Show-PSStateTree

One of the most challenging things when you are trying to learn about scopes is how difficult it is to pin down exactly when one is being created. First, there is no public facing interface for scopes, so you are limited to using a ton of reflection to even get to a representation of them. Second, almost all of the things you do in PowerShell create a new scope, so how do you really know how many scopes you created just trying to figure out if a scope was created?

I mentioned cmdlets don’t create a scope, so I made a small cmdlet to help track scopes. It will return the hash code of several parts of the state tree. This won’t tell you much, but you can compare them to see when they change, which ones stick around, etc.

Below is the same example as above, with this cmdlet added.

$pester = Get-Module Pester

& $pester { Show-PSStateTree; $test; $test = 'test' }

# ExecutionContext     : 56919981
# TopLevelSessionState : 3678064
# EngineSessionState   : 49566835
# GlobalScope          : 62978787
# ModuleScope          : 22936020
# ScriptScope          : 22936020
# ParentScope          : 22936020
# CurrentScope         : 23035345

. $pester { Show-PSStateTree; $test; $test = 'test' }

# ExecutionContext     : 56919981
# TopLevelSessionState : 3678064
# EngineSessionState   : 49566835
# GlobalScope          : 62978787
# ModuleScope          : 22936020
# ScriptScope          : 22936020
# ParentScope          : 62978787
# CurrentScope         : 22936020

& $pester { Show-PSStateTree; $test; $test = 'test' }

# ExecutionContext     : 56919981
# TopLevelSessionState : 3678064
# EngineSessionState   : 49566835
# GlobalScope          : 62978787
# ModuleScope          : 22936020
# ScriptScope          : 22936020
# ParentScope          : 22936020
# CurrentScope         : 42591724
# test

Show-PSStateTree; $test

# ExecutionContext     : 56919981
# TopLevelSessionState : 3678064
# EngineSessionState   : 3678064
# GlobalScope          : 62978787
# ModuleScope          : 62978787
# ScriptScope          : 62978787
# ParentScope          : 0
# CurrentScope         : 62978787

Here’s the Gist if you want to play around with it. Copy and paste it into the console, save it or save it as a file and run it, then test away.

$typeDefinition = @'
using System;
using System.Management.Automation;
using System.Reflection;

namespace PSStateTree.Commands
{
    [OutputType(typeof(StateTreeInfo))]
    [Cmdlet(VerbsCommon.Show, "PSStateTree")]
    public class ShowPSStateTreeCommand : PSCmdlet
    {
        private struct StateTreeInfo
        {
            public int ExecutionContext;
            public int TopLevelSessionState;
            public int EngineSessionState;
            public int GlobalScope;
            public int ModuleScope;
            public int ScriptScope;
            public int ParentScope;
            public int CurrentScope;
        }

        private const BindingFlags BINDING_FLAGS = BindingFlags.Instance | BindingFlags.NonPublic;

        protected override void ProcessRecord()
        {
            StateTreeInfo result;
            EngineIntrinsics engine = GetVariableValue("ExecutionContext") as EngineIntrinsics;
            var context = engine
                .GetType()
                .GetTypeInfo()
                .GetField("_context", BINDING_FLAGS)
                .GetValue(engine);

            var stateInternal = GetProperty("Internal", SessionState);
            var currentScope = GetProperty("CurrentScope", stateInternal);
            var parent = GetProperty("Parent", currentScope);
            
            result.ParentScope = 0;
            if (parent != null)
            {
                result.ParentScope = parent.GetHashCode();
            }

            result.ExecutionContext = context.GetHashCode();
            result.EngineSessionState = GetProperty("EngineSessionState", context).GetHashCode();
            result.TopLevelSessionState = GetProperty("TopLevelSessionState", context).GetHashCode();
            result.CurrentScope = currentScope.GetHashCode();
            result.ScriptScope = GetProperty("ScriptScope", stateInternal).GetHashCode();
            result.ModuleScope = GetProperty("ModuleScope", stateInternal).GetHashCode();
            result.GlobalScope = GetProperty("GlobalScope", stateInternal).GetHashCode();

            WriteObject(result);
        }

        private object GetProperty(string propertyName, object source)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new ArgumentNullException("propertyName");
            }

            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            return source
                .GetType()
                .GetTypeInfo()
                .GetProperty(propertyName, BINDING_FLAGS)
                .GetValue(source);
        }
    }
}
'@

if (-not ('PSStateTree.Commands.ShowPSStateTreeCommand' -as [type])) {
    $module = New-Module -Name PSStateTree -ScriptBlock {
        $types = Add-Type -TypeDefinition $typeDefinition -PassThru
        Import-Module -Assembly $types[0].Assembly
        Export-ModuleMember -Cmdlet Show-PSStateTree
    }
    Import-Module $module
}
blog comments powered by Disqus