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?
I have been investigating this problem, which has come up in a project I am working on, and discovered three things:
scriptBlockis physically located anywhere within a .psm1 file, we see the behavior.scriptBlockis located in a separate script file (.ps1), if thescriptBlockwas passed in from a module.scriptBlockis located anywhere in a script file (.ps1), as long as thescriptBlockwas not passed from a module.scriptBlockwill not necessarily execute in the global scope. Rather, it always appears to execute in whatever scope the module function was called from.scriptBlock: the “.” operator, the “&” operator, and thescriptBlockobject’sinvoke()method. In the latter two cases, thescriptBlockexecutes 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
script_infunction.ps1
module.psm1
all_tests.ps1