O'Reilly logo

Monad (AKA PowerShell) by Andy Oakley

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Managing MSH Scope and State

So far, we've seen a number of different aspects of MSH, including the idea of cmdlets, pipelines, and the shell language of variables, functions, and filters. In this chapter, we'll look at the MSH infrastructure that brings all of these components together and allows them to work seamlessly with each other.

In the following pages, we'll look at some of the services that MSH provides. The material we'll cover here is applicable throughout the shell, and tools such as text matching and regular expressions will likely become an indispensable part of your toolkit.

Control Access to Variables and Functions

We've already touched on the idea that variables defined in functions might not always be accessible from other functions, scripts, or cmdlets. This limited visibility is known as scoping , and it is used by MSH to segregate data when several different script blocks and cmdlets are in play. It's important to realize that scoping doesn't offer privacy or security; instead, the ability to hide is used to simplify the authoring of scripts. As we'll see, this behavior comes in handy with more complicated scripts and tasks as it reduces the potential for these script fragments to interfere with each other.

In general, MSH controls scope automatically; scripts and functions often "just work" as a result. However, it's always wise to understand how scoping comes into play, especially in cases in which there's a need to do something differently.

How Do I Do That?

In interactive mode, we are working in the global scope, in which any variables or functions defined are accessible from everywhere within the shell:

    MSH D:\MshScripts> function showA { write-host $a }
    MSH D:\MshScripts> $a = 10
    MSH D:\MshScripts> showA
    10

This little example seems obvious. Let's see what happens when we define a variable and assign it a value inside a function:

    MSH D:\MshScripts> function doWork { $processes = get-process }
    MSH D:\MshScripts> doWork
    MSH D:\MshScripts> $processes.Count
    MSH D:\MshScripts>

After we've run the function, it might be reasonable to expect that $processes would contain some data, yet it remains undefined when we try to inspect it. This is actually a good thing: if $processes had been storing important data, its value would have been accidentally overwritten inside the function call.

In fact, what we've done here is define a variable in a local scope (one that is created just for the function). By the time we're back at the prompt, any variables defined in that scope have disappeared. Generally speaking, this behavior helps functions keep their own activities to themselves and encourages information to be emitted explicitly as a result (as we saw in the previous chapter), rather than relying on variables to pass information around.

Because there are cases where we'd like variables to live longer than the lifetime of the function that defines them, MSH has syntax for working explicitly with variables in the global scope:

    MSH D:\MshScripts> function doWork { $global:processes = get-process }
    MSH D:\MshScripts> doWork
    MSH D:\MshScripts> $processes.Count
    29

This difference in scope can give rise to some unexpected behavior. In these cases, it's possible for a variable to take on a value that we don't expect or for an assignment to apparently fail. Let's go back to the showA example and add another function that updates the value of $a before we display it:

    MSH D:\MshScripts> function setA { $a = 5 }
    MSH D:\MshScripts> $a=10
    MSH D:\MshScripts> setA
    MSH D:\MshScripts> showA
    10

As in the doWork example, here the setA function is making a change to its own $a variable in its local scope. Even though there's a global variable $a already present, the setA function will not make any changes to it. Because showA has a completely different local scope—one in which no local $a is present—it uses the value of the global $a instead.

Fortunately, MSH provides multiple levels of scope. When one function is invoked from inside another, the invoked function can see the global scope and the scope of its parent; it also has its own separate local scope. If we call the showA function from within a scope in which the value has been changed, it will see the new value instead:

    MSH D:\MshScripts> function setAndShowA { $a = 5; showA }
    MSH D:\MshScripts> $a=10
    MSH D:\MshScripts> setAndShowA
    5
    MSH D:\MshScripts> $a
    10

MSH offers several other explicit scope indictors in the same format as the global: syntax. As the local scope is always assumed, the following example is functionally equivalent to the previous setA definition, but the use of local: helps to convey the scope considerations (in other words, it clarifies how futile this setA function is):

    MSH D:\MshScripts> function setA { $local:a = 5 }

Scripts, like functions, are run within their own special script scope, which is created when the script starts and discarded when it ends. The script: prefix is convenient for modifying variables that are defined in a script but that are outside of the current function and are not global variables. For example, consider a script similar to the one in Example 4-1 that keeps a tally of failures during a series of checks and reports the number of problems at the end of the script.

Example 4-1. Use of script scope variables in a script

function checkProcessCount
{
    if ((get-process).Count -gt 50)
    {
        $script:failureCount++
    }
}

$failureCount = 0
checkProcessCount
...
"Script complete with $failureCount errors"

In cases such as running the profile, we don't want a script to run in its own scope and would prefer it to impact the global scope. Instead of running the script by filename alone, it is dot sourced with a period followed by the filename. Running a script in this way tells MSH to load the child scope into the parent scope when the script is complete (see Example 4-2).

Example 4-2. DotSourceExample.msh

$c = 20

Now, we can see the difference between the two methods of running the script:

    MSH D:\MshScripts> .\DotSourceExample.msh
    MSH D:\MshScripts> $c

    MSH D:\MshScripts> . .\DotSourceExample.msh
    MSH D:\MshScripts> $c
    20

What Just Happened?

Scoping applies to all user-defined elements of the MSH language, including variables, functions, and filters. Fortunately, it follows a series of simple rules and is always predictable.

There are four categories of scope: global, local, script, and private.

Global scope

Only one global scope is created per MSH session when the shell is started. Global scopes are not shared between different instances of MSH.

Local scope

A new local scope is always created when a function, filter, or script is run. The new scope has read access to all scopes of its parent, its parent's parent, and so on, up to the global scope. Because scopes are inherited downward in this fashion, children can read from (but not write to) the scope of their parents, yet parents cannot read from the scope of their children.

An alternative way of looking at this is to appreciate the lifetime of a scope (the time from its creation to the point at which it is discarded). Just as new scopes are created when entering a script block (or function, filter, etc.), they are discarded as soon as the script block is finished. Were a parent to try and access variables in a child's scope before the script block had run, the variables wouldn't exist yet; should they try afterward, the scope would have been discarded and all variables within it would be gone.

Script scope

A script scope is created whenever a script file is run, and it is discarded when the script finishes. All script files are subject to this behavior unless they are dot sourced, in which case their script scope is loaded into the scope of their parent when the script is complete. If one dot-sourced script (a.msh) dot sources another (b.msh), the same rules apply: when b.msh completes, its scope is loaded into the script scope of a.msh; when a.msh completes, their combined scopes are loaded into the parent scope.

Private scope

The private and local scopes are very similar, but they have one key difference: definitions made in the private scope are not inherited by any children scopes.

Table 4-1 summarizes the available scopes and their lifetimes.

Table 4-1. Scopes and their lifetimes

Scope name

Lifetime

Global

Entire MSH session

Local

Current script block and any scripts/functions invoked from it

Script

Current script file and any scripts/functions invoked from it

Private

Current script block only; scripts/functions invoked from current block will not inherit variables defined in this scope

There are a few general rules about scoping that are useful to remember:

  • Unless explicitly stated, a variable can be read and changed only within the scope in which it was created.

  • Scopes are inherited from parent to children. Children can access any data in their parents' scope with the exception of privately scoped variables.

  • The local scope is always the current one, and any references are assumed to refer to it. In other words, any reference such as $a is interpreted as $local:a.

  • The global, local, and private scopes are always available. In some cases, such as when working interactively at the prompt, the global and local scopes will be the same.

What About...

... What if I don't want functions to inherit the scope of the block that calls them? Although rarely used, this is the primary function of the private scope, which can be used to hide data from children. Working with the earlier example, if we now define $a as a private variable, subsequent function calls will be unable to retrieve its value:

    MSH D:\MshScripts> $private:a = 5
    MSH D:\MshScripts> showA
    MSH D:\MshScripts>

... How does get-childitem Variable: deal with different scopes? As we've already seen, this special drive shows the variables defined for the current scope. Executing get-childitem Variable: from the prompt will show the content of the global scope. However, running the same command from within a script file or function may return a different list of results that will include all of the global variables plus any others than have been defined in the local scope.

Now, we're going to put variables aside for a while and discuss how the hosting environment handles strings of text.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required