70

A sad thing about PowerShell is that function and scriptblocks are dynamically scoped.

But there is another thing that surprised me is that variables behave as a copy-on-write within an inner scope.

$array=@("g")
function foo()
{
    $array += "h"
    Write-Host $array
}

& {
    $array +="s"
    Write-Host $array
}
foo

Write-Host $array

The output is:

g s
g h
g

Which makes dynamic scoping a little bit less painful. But how do I avoid the copy-on-write?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
mathk
  • 7,973
  • 6
  • 45
  • 74

5 Answers5

93

The PowerShell scopes article (about_Scopes) is nice, but too verbose, so this is quotation from my article:

In general, PowerShell scopes are like .NET scopes. They are:

  • Global is public
  • Script is internal
  • Private is private
  • Local is current stack level
  • Numbered scopes are from 0..N where each step is up to stack level (and 0 is Local)

Here is simple example, which describes usage and effects of scopes:

$test = 'Global Scope'
Function Foo {
    $test = 'Function Scope'
    Write-Host $Global:test                                  # Global Scope
    Write-Host $Local:test                                   # Function Scope
    Write-Host $test                                         # Function Scope
    Write-Host (Get-Variable -Name test -ValueOnly -Scope 0) # Function Scope
    Write-Host (Get-Variable -Name test -ValueOnly -Scope 1) # Global Scope
}
Foo

As you can see, you can use $Global:test like syntax only with named scopes, $0:test will be always $null.

John Cummings
  • 1,949
  • 3
  • 22
  • 38
Anton
  • 10,890
  • 8
  • 45
  • 54
77

You can use scope modifiers or the *-Variable cmdlets.

The scope modifiers are:

  • global used to access/modify at the outermost scope (eg. the interactive shell)
  • script used on access/modify at the scope of the running script (.ps1 file). If not running a script then operates as global.

(For the -Scope parameter of the *-Variable cmdlets see the help.)

Eg. in your second example, to directly modify the global $array:

& {
  $global:array +="s"
  Write-Host $array
}

For more details see the help topic about_scopes.

Maximilian Burszley
  • 18,243
  • 4
  • 34
  • 63
Richard
  • 106,783
  • 21
  • 203
  • 265
  • Thanks for the info. I had more a less read the about_scopes topic. One things that I didn't see in this documentation is the fact that variables are dynamically scoped. :( – mathk Feb 17 '12 at 09:48
  • This was perplexing me in the error-checking of a script I had; I had a simple counter that was incremented every time the script encountered an error, that determined information sent in an e-mail. The error count at the end was always zero no mater how many error conditions I set up. Now I understand both why that was and how to fix it. – KeithS Jun 02 '17 at 17:26
  • I think this method only answers the question in just the one or two very restricted cases. Not the general one. – bielawski Aug 17 '19 at 12:33
17

Not just varibles. When this says "item" it means variables, functions, aliases, and psdrives. All of those have scope.

LONG DESCRIPTION  
    Windows PowerShell protects access to variables, aliases, functions, and
    Windows PowerShell drives (PSDrives) by limiting where they can be read and
    changed. By enforcing a few simple rules for scope, Windows PowerShell
    helps to ensure that you do not inadvertently change an item that should
    not be changed.

    The following are the basic rules of scope:

        - An item you include in a scope is visible in the scope in which it
          was created and in any child scope, unless you explicitly make it
          private. You can place variables, aliases, functions, or Windows
          PowerShell drives in one or more scopes.

        - An item that you created within a scope can be changed only in the
          scope in which it was created, unless you explicitly specify a
          different scope.

The copy on write issue you're seeing is because of the way Powershell handles arrays. Adding to that array actually destroys the original array and creates a new one. Since it was created in that scope, it is destroyed when the function or script block exits and the scope is disposed of.

You can explicitly scope varibles when you update them, or you can use [ref] objects to do your updates, or write your script so that you're updating a property of an object or a hash table key of an object or hash table in a parent scope. This does not create a new object in the local scope, it modifies the object in the parent scope.

Thraka
  • 2,065
  • 19
  • 24
mjolinor
  • 66,130
  • 7
  • 114
  • 135
  • Sure function and alias are kindof variable. But I am not concern about that. Especially in some languages function are in different scope then variable (common lisp for example) but are still variable. – mathk Feb 17 '12 at 13:41
  • It might be of some concern if some day you write a Powershell script that creates an alias or function in a child scope. – mjolinor Feb 17 '12 at 13:55
  • But you did not answer the question. @Richard got the correct answer – mathk Feb 17 '12 at 14:10
  • I tried to explain that you cannot avoid the copy on write when you add to an array. You have to control where it's writing the copy to. If you don't tell it otherwise, it's going to write it to the local scope. You can also avoid copy on write by not adding to an array, but by adding or updating a property of an object, or a key of a hash table. – mjolinor Feb 17 '12 at 14:22
  • 2
    But that make powershell a even worse language. If the scope semantic change depending on the type of the variable that is horrible design choice, IMO. Plus it bring back dynamic scope right in your face. :( – mathk Feb 17 '12 at 15:54
  • 2
    Well, it is what it is. I just tried to explain what's happening. The copy-on-write is the nature of the beast. Explicitly scoping the variable isn't avoiding copy-on-write, it's just specifying where the copy is going to get written to. – mjolinor Feb 17 '12 at 16:58
5

While other posts give lots of useful information they seem only to save you from RTFM.
The answer not mentioned is the one I find most useful!

([ref]$var).value = 'x'

This modifies the value of $var no matter what scope it happens to be in. You need not know its scope; only that it does in fact already exist. To use the OP's example:

$array=@("g")
function foo()
{
    ([ref]$array).Value += "h"
    Write-Host $array
}
& {
    ([ref]$array).Value +="s"
    Write-Host $array
}
foo
Write-Host $array

Produces:

g s
g s h
g s h

Explanation:
([ref]$var) gets you a pointer to the variable. Since this is a read operation it resolves to the most recent scope that actually did create that name. It also explains the error if the variable doesn't exist because [ref] can't create anything, it can only return a reference to something that already exists.

.value then takes you to the property holding the variable's definition; which you can then set.

You may be tempted to do something like this because it sometimes looks like it works.

([ref]$var) = "New Value"

DON'T!!!!
The instances where it looks like it works is an illusion because PowerShell is doing something that it only does under some very narrow circumstances such as on the command line. You can't count on it. In fact it doesn't work in the OP example.

bielawski
  • 1,466
  • 15
  • 20
0

A sad thing about PowerShell is that function and scriptblocks are dynamically scoped.

Use .GetNewClosure() to create a closure that captures variables from the immediate local scope:

function New-Closure
{
    $foo = 42
    {$foo}.GetNewClosure()
}

❯ $Closure = New-Closure
❯ & $Closure
# output: 42

The scope is accessible through the Module property:

❯ {"some new scriptblock"}.Module
# no output

❯ $Closure.Module

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     0.0                   __DynamicModule_f89a39f6-f9f7-40ce…

Whether or not functions should also be dynamically-scoped is a language design decision. I can't think of a scripting language that does not allow functions to access dynamic variables from the caller's scope - perhaps you were brought up on Haskell ;-)

Edit: functions can access the scope of the containing module. The unit of powershell deployment is the module. If you don't want to write a module, you can create a dynamic module:

New-Module {
    $foo = 23
    function Get-EnclosedFoo
    {
        $foo
    }
}
# output: dynamic module

Get-EnclosedFoo
# output: 23
FSCKur
  • 920
  • 7
  • 16