4.1. Control Access to Variables and FunctionsWe'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.
4.1.1. 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 scopeone in which no local $a is presentit 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 scriptfunction 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 4.1.2. 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. 4.1.2.1. Global scopeOnly one global scope is created per MSH session when the shell is started. Global scopes are not shared between different instances of MSH. 4.1.2.2. Local scopeA 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. 4.1.2.3. Script scopeA 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. 4.1.2.4. Private scopeThe 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.
There are a few general rules about scoping that are useful to remember:
4.1.3. 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. |