Странное поведение с областью действия и модулями блока сценария Powershell, какие-либо предложения?

ПРИМЕЧАНИЕ. Я использую PowerShell 2.0 в Windows Vista.

Я пытаюсь добавить поддержку указания аргументов сборки в 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 как модуль, а вместо этого просто объявляю функцию и вызываю ее, все работает так, как я и ожидал. Блок script влияет только на область Test и не изменяет глобальную область.

Я не гуру PowerShell, но может ли кто-нибудь объяснить мне это поведение?


person Matt Honeycutt    schedule 03.02.2010    source источник
comment
Вы сообщили об этом как об ошибке?   -  person stej    schedule 18.05.2010


Ответы (3)


Я исследовал эту проблему, которая возникла в проекте, над которым я работаю, и обнаружил три вещи:

  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.
    • Мы также видим поведение, если код, вызывающий scriptBlock, находится в отдельном файле скрипта (.ps1), если scriptBlock был передан в из модуля.
    • Мы не видим поведение, если код, вызвавший scriptBlock, находится где-нибудь в файле сценария (.ps1), если scriptBlock не был передан из модуля.
  2. scriptBlock не обязательно будет выполняться в глобальной области видимости. Скорее, кажется, что он всегда выполняется в любой области, из которой была вызвана функция модуля.
  3. Проблема не ограничивается "." оператор (точка источника). Я протестировал три различных способа вызова scriptBlock: "." оператор, оператор "&" и метод invoke() объекта scriptBlock. В последних двух случаях scriptBlock выполняется с неправильной областью действия parent. Это можно выяснить, попробовав вызвать, например, {set-variable -name "message" -scope 1 -value "From scriptBlock"}

Я надеюсь, что это прольет свет на проблему, хотя я еще не продвинулся достаточно далеко, чтобы предложить обходной путь.

У кого-нибудь еще установлен PowerShell 1? Если это так, было бы полезно, если бы вы могли проверить, отображает ли он такое же поведение.

Вот файлы для моих тестовых случаев. Чтобы запустить их, создайте все четыре файла в одном каталоге, а затем выполните «./all_tests.ps1» в командной строке PowerShell ISE.

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"
person Dan Menes    schedule 20.05.2010

Я не думаю, что команда PowerShell считает это ошибкой, но я могу, по крайней мере, пролить свет на то, как это работает.

Любой блок скрипта, определенный в скрипте или модуле скрипта (в буквальном смысле, не созданный динамически с помощью чего-то вроде [scriptblock]::Create()), привязан к состоянию сеанса этого модуля (или к «основному» состоянию сеанса, если он не выполняется внутри модуля скрипта. ) Существует также информация, относящаяся к файлу, из которого был получен блок сценария, поэтому такие вещи, как точки останова, будут работать при вызове блока сценария.

Когда вы передаете такой блок сценария в качестве параметра через границы модуля сценария, он по-прежнему привязан к своей исходной области, даже если вы вызываете его из модуля.

В данном конкретном случае самым простым решением является создание несвязанного блока скрипта вызовом [scriptblock]::Create() (передача текста объекта блока скрипта, который был передан в качестве параметра):

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

Однако имейте в виду, что теперь есть потенциал для проблем с областью действия в другом направлении. Если этот блок скрипта зависит от возможности разрешать переменные или функции, которые были доступны в исходной области, но не в модуле, в котором вы его вызвали, произойдет сбой.

Поскольку целью блока $properties, по-видимому, является установка переменных и ничего больше, я бы, вероятно, передал объект IDictionary или Hashtable вместо блока сценария. Таким образом, все выполнение происходит в области действия вызывающей стороны, и вы получаете простой инертный объект для работы внутри модуля, не беспокоясь о глупости области видимости:

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
person Dave Wyatt    schedule 16.12.2014
comment
Спасибо за это объяснение. Некоторое время назад я самостоятельно обнаружил, что выполнение [scriptblock]::Create($block.ToString()) может заставить все работать так, как я хотел, но до сих пор не имел ни малейшего представления почему! Этот трюк часто бывает полезен для DSL PowerShell. - person Soren Bjornstad; 07.05.2019

Похоже, что сообщение $ в переданном блоке сценария привязано к глобальной области видимости, например:

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, чтобы узнать, могу ли я это подтвердить.

person Keith Hill    schedule 03.02.2010
comment
Да, это определенно связано с глобальным охватом. Возможно, это задумано, но если для этого есть веская причина, я хочу знать, что это такое. Спасибо! - person Matt Honeycutt; 03.02.2010
comment
Я разместил и, если я IIRC, не получил ответа от MS. - person Keith Hill; 28.04.2010
comment
Связанная ошибка все еще активна и, похоже, все еще существует для PS 3.0 и PS 4.0. - person wesm; 15.12.2014