Странное поведение с областью переменных и модулями сценариев 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 как модуль, а просто объявлю функцию и вызову ее, все будет работать так, как я ожидал. Блок сценария влияет только на область действия Test и не изменяет глобальную область.
Я не гуру PowerShell, но может кто-нибудь объяснить мне это поведение?
3 ответа
Я исследовал эту проблему, которая возникла в проекте, над которым я работаю, и обнаружил три вещи:
- Проблема специфична для модулей.
- Если код, который вызывает
scriptBlock
физически находится в любом месте в файле.psm1, мы видим поведение. - Мы также видим поведение, если код, который вызывает
scriptBlock
находится в отдельном файле сценария (.ps1), еслиscriptBlock
был передан из модуля. - Мы не видим поведение, если код, который вызвал
scriptBlock
находится в любом месте файла сценария (.ps1), еслиscriptBlock
не был передан из модуля.
- Если код, который вызывает
scriptBlock
не обязательно будет выполняться в глобальном масштабе. Скорее, он всегда выполняется в любой области, из которой была вызвана функция модуля.- Вопрос не ограничен "." оператор (точечный источник). Я проверил три разных способа вызвать
scriptBlock
: "." оператор, оператор "&" иscriptBlock
объектыinvoke()
метод. В последних двух случаяхscriptBlock
выполняется с неверной родительской областью. Это можно исследовать, пытаясь вызвать, например,{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'"
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"
Я не думаю, что это считается ошибкой команды 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
Похоже, что $ сообщение в переданном скриптовом блоке связано с глобальной областью действия, например:
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, чтобы проверить, смогу ли я это подтвердить.