Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange behavior with Powershell scriptblock variable scope and modules, any suggestions?

NOTE: I'm using PowerShell 2.0 on Windows Vista.

I'm trying to add support for specifying build arguments to psake, but I've run into some strange PowerShell variable scoping behavior dealing specifically with calling functions that have been exported using Export-ModuleMember (which is how psake exposes it's main method). Following is a simple PowerShell module to illustrate (named repoCase.psm1):

function Test {
    param(
        [Parameter(Position=0,Mandatory=0)]
        [scriptblock]$properties = {}
    )

    $defaults = {$message = "Hello, world!"}

    Write-Host "Before running defaults, message is: $message"

    . $defaults

    #At this point, $message is correctly set to "Hellow, world!"
    Write-Host "Aftering running defaults, message is: $message"

    . $properties

    #At this point, I would expect $message to be set to whatever is passed in,
    #which in this case is "Hello from poperties!", but it isn't.  
    Write-Host "Aftering running properties, message is: $message"
}

Export-ModuleMember -Function "Test"

To test the module, run the following sequence of commands (be sure you're in the same directory as the repoCase.psm1):

Import-Module .\repoCase.psm1

#Note that $message should be null
Write-Host "Before execution - In global scope, message is: $message"

Test -properties { "Executing properties, message is $message"; $message = "Hello from properties!"; }

#Now $message is set to the value from the script block.  The script block affected only the global scope.
Write-Host "After execution - In global scope, message is: $message"

Remove-Module repoCase

The behavior I expected was for the script block I passed to Test to affect the local scope of Test. It is being 'dotsourced' in, so any changes it makes should be within the scope of the caller. However, that's not what's happening, it seems to be affecting the scope of where it was declared. Here's the output:

Before execution - In global scope, message is:
Before running defaults, message is:
Aftering running defaults, message is: Hello, world!
Executing properties, message is
Aftering running properties, message is: Hello, world!
After execution - In global scope, message is: Hello from properties!

Interestingly, if I don't export Test as a module and instead just declare the function and invoke it, everything works just like I would expect it to. The script block affects only Test's scope, and does not modify the global scope.

I'm not a PowerShell guru, but can someone explain this behavior to me?

like image 302
Matt Honeycutt Avatar asked Feb 03 '10 16:02

Matt Honeycutt


2 Answers

I don't think this is considered to be a bug by the PowerShell team, but I can at least shed some light on how it works.

Any script block that's defined in a script or script module (in literal form, not dynamically created with something like [scriptblock]::Create()) is bound to the session state of that module (or to the "main" session state, if not executing inside a script module.) There is also information specific to the file that the script block came from, so things like breakpoints will work when the script block is invoked.

When you pass in such a script block as a parameter across script module boundaries, it is still bound to its original scope, even if you invoke it from inside the module.

In this specific case, the simplest solution is to create an unbound script block by calling [scriptblock]::Create() (passing in the text of the script block object that was passed in as a parameter):

. ([scriptblock]::Create($properties.ToString()))

However, keep in mind that there is potential for scope problems in the other direction, now. If that script block relies on being able to resolve variables or functions that were available in the original scope, but not from the module where you've invoked it, it will fail.

Since the intention of the $properties block appears to be to set variables and nothing else, I would probably pass in an IDictionary or Hashtable object instead of a script block. That way all of the execution takes place out in the caller's scope, and you get a simple, inert object to deal with inside the module, with no scope silliness to worry about:

function Test {
    param(
        [ValidateNotNull()]
        [Parameter(Position=0,Mandatory=0)]
        [System.Collections.IDictionary]$properties = @{}
    )

    # Setting the default
    $message = "Hello, world!"

    Write-Host "After setting defaults, message is: $message"

    foreach ($dictionaryEntry in $properties.GetEnumerator())
    {
        Set-Variable -Scope Local -Name $dictionaryEntry.Key -Value $dictionaryEntry.Value
    }

    Write-Host "After importing properties, message is: $message"
}

Caller file:

Import-Module .\repoCase.psm1

Write-Host "Before execution - In global scope, message is: $message"

Test -properties @{ Message = 'New Message' }

Write-Host "After execution - In global scope, message is: $message"

Remove-Module repoCase
like image 76
Dave Wyatt Avatar answered Oct 30 '22 08:10

Dave Wyatt


I have been investigating this problem, which has come up in a project I am working on, and discovered three things:

  1. The issue is specific to modules.
    • If the code that invokes the scriptBlock is physically located anywhere within a .psm1 file, we see the behavior.
    • We also see the behavior if the code that invokes the scriptBlock is located in a separate script file (.ps1), if the scriptBlock was passed in from a module.
    • We do not see the behavior if the code that invoked the scriptBlock is located anywhere in a script file (.ps1), as long as the scriptBlock was not passed from a module.
  2. The scriptBlock will not necessarily execute in the global scope. Rather, it always appears to execute in whatever scope the module function was called from.
  3. The issue is not limited to the "." operator (dotsource). I have tested three different ways to invoke the scriptBlock: the "." operator, the "&" operator, and the scriptBlock object's invoke() method. In the latter two cases, the scriptBlock executes with the wrong parent scope. This can be investigated by trying to invoke, for example {set-variable -name "message" -scope 1 -value "From scriptBlock"}

I hope this sheds some more light on the problem, although I haven't quite gotten far enough to propose a workaround.

Does anyone still have PowerShell 1 installed? If so, it would be useful if you can check whether it displays the same behavior.

Here are the files for my test cases. To run them, create all four files in the same directory, and then execute "./all_tests.ps1" at the PowerShell ISE command line

script_toplevel.ps1

param($script_block)

set-alias "wh" write-host

$message = "Script message"
wh "  Script message before:      '$message'"
. $script_block
wh "  Script message after:       '$message'"

script_infunction.ps1

param($script_block)
set-alias "wh" write-host

function f {
    param($script_block)
    $message = "Function message"
    wh "  Function message before:    '$message'"
    . $script_block
    wh "  Function message after:     '$message'"
}

$message = "Script message"
wh "  Script message before:      '$message'"
f -script_block $script_block
wh "  Script message after:       '$message'"

module.psm1

set-alias "wh" write-host

function simple_test_fun {
    param($script_block)

    $message = "ModFunction message"
    wh "  ModFunction message before: '$message'"
    . $script_block
    wh "  ModFunction message after:  '$message'"
}

function ampersand_test_fun {
    param($script_block)

    $message = "ModFunction message"
    wh "  ModFunction message before: '$message'"
    & $script_block
    wh "  ModFunction message after:  '$message'"
}

function method_test_fun {
    param($script_block)

    $message = "ModFunction message"
    wh "  ModFunction message before: '$message'"
    $script_block.invoke()
    wh "  ModFunction message after:  '$message'"
}

function test_mod_to_script_toplevel {
    param($script_block)

    $message = "ModFunction message"
    wh "  ModFunction message before: '$message'"
    & .\script_toplevel.ps1 -script_block $script_block
    wh "  ModFunction message after:  '$message'"
}

function test_mod_to_script_function {
    param($script_block)

    $message = "ModFunction message"
    wh "  ModFunction message before: '$message'"
    & .\script_infunction.ps1 -script_block $script_block
    wh "  ModFunction message after:  '$message'"
}

export-modulemember -function "simple_test_fun", "test_mod_to_script_toplevel", "test_mod_to_script_function", "ampersand_test_fun", "method_test_fun"

all_tests.ps1

remove-module module
import-module .\module.psm1

set-alias "wh" write-host

wh "Test 1:"
wh "  No problem with . at script top level"
wh "    ScriptBlock created at 'TopScript' scope"
wh "    TopScript -amp-calls-> Script -dot-calls-> ScriptBlock:"
wh
wh "  Expected behavior: Script message after:       'Script block message'"
wh "  Problem behavior:  TopScript message after:    'Script block message'"
wh
wh "Results:"

$global:message = "Global message"
$message = "Top script message"
wh "  Global message before:      '$global:message'"
wh "  TopScript message before:   '$message'"
& .\script_toplevel.ps1 -script_block {$message = "Script block message"}
wh "  TopScript message after:    '$message'"
wh "  Global message after:       '$global:message'"

wh
wh "Test 1 showed expected behavior"
wh
wh
wh "Test 2:"
wh "  No problem with . inside function in script"
wh "    ScriptBlock created at 'TopScript' scope"
wh "    TopScript -amp-calls-> Script -calls-> Function -dot-calls-> ScriptBlock:"
wh
wh "  Expected behavior: Function message after:     'Script block message'"
wh "  Problem behavior:  TopScript message after:    'Script block message'"
wh
wh "Results:"
$global:message = "Global message"
$message = "Top script message"
wh "  Global message before:      '$global:message'"
wh "  TopScript message before:   '$message'"
& .\script_infunction.ps1 -script_block {$message = "Script block message"}
wh "  TopScript message after:    '$message'"
wh "  Global message after:       '$global:message'"

wh
wh "Test 2 showed expected behavior"
wh
wh
wh "Test 3:"
wh "  Problem with with . with function in module"
wh "    ScriptBlock created at 'TopScript' scope"
wh "    TopScript -calls-> ModFunction -dot-calls-> ScriptBlock:"
wh
wh "  Expected behavior: ModFunction message after:  'Script block message'"
wh "  Problem behavior:  TopScript message after:    'Script block message'"
wh
wh "Results:"
$global:message = "Global message"
$message = "Top script message"
wh "  Global message before:      '$global:message'"
wh "  TopScript message before:   '$message'"
simple_test_fun -script_block {$message = "Script block message"}
wh "  TopScript message after:    '$message'"
wh "  Global message after:       '$global:message'"

wh
wh "Test 3 showed problem behavior"
wh
wh
wh "Test 4:"
wh "  Confirm that problem scope is always scope where ScriptBlock is created"
wh "    ScriptBlock created at 'f1' scope"
wh "    TopScript -calls-> f1 -calls-> f2 -amp-calls-> ModFunction -dot-calls-> ScriptBlock:"
wh
wh "  Expected behavior: ModFunction message after:  'Script block message'"
wh "  Problem behavior:  f1 message after:           'Script block message'"
wh
wh "Results:"
$global:message = "Global message"
$message = "Top script message"
wh "  Global message before:      '$global:message'"
wh "  TopScript message before:   '$message'"
function f1 {
    $message = "f1 message"
    wh "  f1 message before:          '$message'"
    f2 -script_block {$message = "Script block message"}
    wh "  f1 message after:           '$message'"
}
function f2 {
    param($script_block)

    $message = "f2 message"
    wh "  f2 message before:          '$message'"
    simple_test_fun -script_block $script_block
    wh "  f2 message after:           '$message'"
}

f1
wh "  TopScript message after:    '$message'"
wh "  Global message after:       '$global:message'"

wh
wh "Test 4 showed problem behavior"
wh
wh
wh "Test 4:"
wh "  Confirm that problem scope is always scope where ScriptBlock is created"
wh "    ScriptBlock created at 'f1' scope"
wh "    TopScript -calls-> f1 -calls-> f2 -amp-calls-> ModFunction -dot-calls-> ScriptBlock:"
wh
wh "  Expected behavior: ModFunction message after:  'Script block message'"
wh "  Problem behavior:  f1 message after:           'Script block message'"
wh
wh "Results:"
$global:message = "Global message"
$message = "Top script message"
wh "  Global message before:      '$global:message'"
wh "  TopScript message before:   '$message'"
function f1 {
    $message = "f1 message"
    wh "  f1 message before:          '$message'"
    f2 -script_block {$message = "Script block message"}
    wh "  f1 message after:           '$message'"
}
function f2 {
    param($script_block)

    $message = "f2 message"
    wh "  f2 message before:          '$message'"
    simple_test_fun -script_block $script_block
    wh "  f2 message after:           '$message'"
}

f1
wh "  TopScript message after:    '$message'"
wh "  Global message after:       '$global:message'"

wh
wh "Test 4 showed problem behavior"
wh
wh
wh "Test 5:"
wh "  Problem with with . when module function invokes script (toplevel)"
wh "    ScriptBlock created at 'TopScript' scope"
wh "    TopScript -calls-> ModFunction -amp-calls-> Script -dot-calls-> ScriptBlock:"
wh
wh "  Expected behavior: ModFunction message after:  'Script block message'"
wh "  Problem behavior:  TopScript message after:    'Script block message'"
wh
wh "Results:"
$global:message = "Global message"
$message = "Top script message"
wh "  Global message before:      '$global:message'"
wh "  TopScript message before:   '$message'"
test_mod_to_script_toplevel -script_block {$message = "Script block message"}
wh "  TopScript message after:    '$message'"
wh "  Global message after:       '$global:message'"

wh
wh "Test 5 showed problem behavior"
wh
wh
wh "Test 6:"
wh "  Problem with with . when module function invokes script (function)"
wh "    ScriptBlock created at 'TopScript' scope"
wh "    TopScript -calls-> ModFunction -amp-calls-> Script -calls-> function -dot-calls-> ScriptBlock:"
wh
wh "  Expected behavior: ModFunction message after:  'Script block message'"
wh "  Problem behavior:  TopScript message after:    'Script block message'"
wh
wh "Results:"
$global:message = "Global message"
$message = "Top script message"
wh "  Global message before:      '$global:message'"
wh "  TopScript message before:   '$message'"
test_mod_to_script_function -script_block {$message = "Script block message"}
wh "  TopScript message after:    '$message'"
wh "  Global message after:       '$global:message'"

wh
wh "Test 6 showed problem behavior"
wh
wh
wh "Test 7:"
wh "  Problem with with & with function in module"
wh "    ScriptBlock created at 'TopScript' scope"
wh "    TopScript -calls-> ModFunction -amp-calls-> Script -calls-> function -dot-calls-> ScriptBlock:"
wh
wh "  Expected behavior: ModFunction message after:  'Script block message'"
wh "  Problem behavior:  TopScript message after:    'Script block message'"
wh
wh "Results:"
$global:message = "Global message"
$message = "Top script message"
wh "  Global message before:      '$global:message'"
wh "  TopScript message before:   '$message'"
ampersand_test_fun -script_block {set-variable -scope 1 -name "message" -value "Script block message"}
wh "  TopScript message after:    '$message'"
wh "  Global message after:       '$global:message'"

wh
wh "Test 7 showed problem behavior"
wh
wh
wh "Test 8:"
wh "  Problem with with invoke() method with function in module"
wh "    ScriptBlock created at 'TopScript' scope"
wh "    TopScript -calls-> ModFunction -amp-calls-> Script -calls-> function -dot-calls-> ScriptBlock:"
wh
wh "  Expected behavior: ModFunction message after:  'Script block message'"
wh "  Problem behavior:  TopScript message after:    'Script block message'"
wh
wh "Results:"
$global:message = "Global message"
$message = "Top script message"
wh "  Global message before:      '$global:message'"
wh "  TopScript message before:   '$message'"
method_test_fun -script_block {set-variable -scope 1 -name "message" -value "Script block message"}
wh "  TopScript message after:    '$message'"
wh "  Global message after:       '$global:message'"

wh
wh "Test 8 showed problem behavior"
like image 23
Dan Menes Avatar answered Oct 30 '22 08:10

Dan Menes