17

注意:我在 Windows Vista 上使用 PowerShell 2.0。

我正在尝试添加对指定构建参数的支持psake,但我遇到了一些奇怪的 PowerShell 变量作用域行为,专门处理使用 Export-ModuleMember 导出的调用函数(这是 psake 公开它的主要方法的方式)。下面是一个简单的 PowerShell 模块来说明(名为 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"

要测试模块,请运行以下命令序列(确保您与 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

我期望的行为是我传递给 Test 的脚本块影响 Test 的本地范围。它正在被“点源化”,因此它所做的任何更改都应该在调用者的范围内。但是,这不是正在发生的事情,它似乎正在影响它被声明的范围。这是输出:

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!

有趣的是,如果我不将 Test 作为模块导出,而只是声明函数并调用它,那么一切都会像我期望的那样工作。脚本块只影响 Test 的作用域,不会修改全局作用域。

我不是 PowerShell 专家,但有人可以向我解释这种行为吗?

4

3 回答 3

10

我不认为 PowerShell 团队认为这是一个错误,但我至少可以阐明它是如何工作的。

在脚本或脚本模块中定义的任何脚本块(以文字形式,不是用类似的东西动态创建的,[scriptblock]::Create())都绑定到该模块的会话状态(或“主”会话状态,如果不在脚本模块内执行。)还有特定于脚本块来自的文件的信息,因此当调用脚本块时,诸如断点之类的东西将起作用。

当您将这样的脚本块作为参数传入脚本模块边界时,即使您从模块内部调用它,它仍然绑定到其原始范围。

[scriptblock]::Create()在这种特定情况下,最简单的解决方案是通过调用(传入作为参数传入的脚本块对象的文本)来创建一个未绑定的脚本块:

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

但是,请记住,现在在另一个方向上可能会出现范围问题。如果该脚本块依赖于能够解析在原始范围内可用的变量或函数,但不是来自您调用它的模块,它将失败。

由于该$properties块的目的似乎是设置变量而不是其他任何东西,我可能会传入一个IDictionaryHashtable对象而不是脚本块。这样,所有的执行都发生在调用者的作用域内,你得到一个简单的、惰性的对象来处理模块内部,不用担心作用域愚蠢:

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"
}

调用者文件:

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
于 2014-12-16T00:00:22.523 回答
8

我一直在调查这个问题,这个问题出现在我正在从事的一个项目中,并发现了三件事:

  1. 该问题特定于模块。
    • 如果调用scriptBlock.psm1 文件中的任何物理位置的代码,我们会看到该行为。
    • 如果调用 的代码scriptBlock位于单独的脚本文件 (.ps1) 中(如果是模块scriptBlock传入的),我们还会看到这种行为。
    • 如果调用 的代码位于脚本文件 (.ps1) 中的任何位置,只要不是从模块传递,我们就看不到该行为。scriptBlockscriptBlock
  2. scriptBlock不一定会在全局范围内执行。相反,它似乎总是在调用模块函数的任何范围内执行。
  3. 问题不仅限于“。” 运算符(点源)。我已经测试了三种不同的调用方式scriptBlock:“。” 运算符、“&”运算符和scriptBlock对象的invoke()方法。在后两种情况下,scriptBlock使用错误的范围执行。这可以通过尝试调用来调查,例如{set-variable -name "message" -scope 1 -value "From scriptBlock"}

我希望这能对这个问题有更多的了解,尽管我还没有走得足够远来提出一个解决方法。

有人还安装了 PowerShell 1 吗?如果是这样,如果您可以检查它是否显示相同的行为将会很有用。

这是我的测试用例的文件。要运行它们,请在同一目录中创建所有四个文件,然后在 PowerShell ISE 命令行中执行“./all_tests.ps1”

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'"

模块.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"
于 2010-05-20T20:20:08.147 回答
4

似乎传入的脚本块中的 $message 与全局范围相关联,例如:

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" 

    # This works. Hmmm
    Write-Host "Aftering running properties, message is: $global:message" 
} 

Export-ModuleMember -Function "Test" 

输出:

Before running defaults, message is: 
Aftering running defaults, message is: Hello, world!
Executing properties, message is 
Aftering running properties, message is: Hello, world!
Aftering running properties, message is: Hello from properties!

这似乎是一个错误。我将查看 PowerShell MVP 列表,看看我是否可以确认这一点。

于 2010-02-03T18:16:37.963 回答