Блок сценария отложенного связывания не работает, когда функция экспортируется из модуля - PullRequest
0 голосов
/ 15 ноября 2018

У меня есть следующая функция:

function PipeScript {
    param(
        [Parameter(ValueFromPipeline)]
        [Object] $InputObject,

        [Object] $ScriptBlock
    )

    process {
        $value = Invoke-Command -ScriptBlock $ScriptBlock
        Write-Host "Script: $value"
    }
}

Когда я определяю эту функцию непосредственно в сценарии и вводе в нее канала, я получаю следующий ожидаемый результат:

@{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }

# Outputs: "Script: Test"

Но когда яопределите эту функцию внутри модуля и экспортируйте ее с помощью Export-ModuleMember -Function PipeScript, затем конвейерная переменная $_ внутри блока скрипта всегда будет null:

Import-Module PipeModule
@{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }

# Outputs: "Script: "

Полное воспроизведение доступно по адресу: https://github.com/lpatalas/DelayBindScriptBlock

Может кто-нибудь объяснить это поведение?

1 Ответ

0 голосов
/ 16 ноября 2018

Наконечник шляпы PetSerAl за всю его помощь.

Вот простое решение , но обратите внимание, что оно запускает блок сценария непосредственно в области действия вызывающего , то есть он фактически «точечные источники», что позволяет модифицировать переменные вызывающего объекта.

В отличие от этого, использование Invoke-Command запускает блок скрипта в child области видимости вызывающей стороны - если это действительно намерение, см. Вариант решения ниже.

"Dot-sourcing" блок сценария - это то, что делают стандартные командлеты, такие как Where-Object и ForEach-Object.

# Define the function in an (in-memory) module.
# An in-memory module is automatically imported.
$null = New-Module {

  function PipeScript {
    param(
      [Parameter(ValueFromPipeline)]
      [Object] $InputObject
      ,
      [scriptblock] $ScriptBlock
    )

    process {

      # Use ForEach-Object to create the automatic $_ variable
      # in the script block's origin scope.
      $value = ForEach-Object -Process $ScriptBlock -InputObject $InputObject

      # Output the value
      "Script: $value"
    }

  }

}

# Test the function:
$var = 42; @{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 43 - the script block ran in the caller's scope.

Вышеприведенные строки вывода Script: Test и 43 впоследствии подтверждаютчто входной объект был замечен как $_, и что точечный источник работал ($var был успешно увеличен в объеме вызывающего абонента).


Вот вариант , черезPowerShell SDK, который запускает блок сценария в дочерней области действия вызывающей стороны .

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

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

$null = New-Module {

  function PipeScript {
    param(
      [Parameter(ValueFromPipeline)]
      [Object] $InputObject
      ,
      [scriptblock] $ScriptBlock
    )

    process {
      # Use ScriptBlock.InvokeContext() to inject a $_ variable
      # into the child scope that the script block runs in:
      # Creating a custom version of what is normally an *automatic* variable
      # seems hacky, but the docs do state:
      # "The list of variables may include the special variables 
      #  $input, $_ and $this." - see https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.invokewithcontext
      $value = $ScriptBlock.InvokeWithContext(
        $null, # extra functions to define (none here)
        [psvariable]::new('_', $InputObject) # actual parameter type is List<PSVariable>
      )
      # Output the value
      "Script: $value"
    }

  }

}

# Test the function:
$var = 42
@{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 42 - unaltered, because the script block ran in a child scope.

Вышеприведенная строка выводит Script: Test, затем следует 42, доказывая, что блок скрипта видел входной объект как $_ и эту переменную $var - хотя видно в блоке сценария не был изменен из-за выполнения в дочерней области.

Метод ScriptBlock.InvokeWithContext() задокументирован здесь .


Что касается , почему ваша попытка не сработала :

  • Как правило, блоки скриптов привязаны к области действия и области действия, в которой они находятся создал (кроме случаев, когда они явно созданы как несвязанные блоки сценариев с [scriptblock]::Create('...')).

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

  • Ваш блок скриптасоздается в домене области действия по умолчанию, и когда определяемая модулем функция вызывает его, $_ ищется в области происхождения , т. е. в (немодульной) вызывающей сторонеОбласть действия , где она не определена, поскольку автоматическая переменная $_ создается PowerShell по требованию в области local , которая находится в области области действия включающего модуля.

  • При использовании .InvokeWithContext() блок скрипта запускается в child scope области вызывающего абонента (как было бы в случае с .Invoke() и Invoke-Command по умолчанию) вкоторый приведенный выше код внедряет пользовательскую переменную $_ , чтобы блок сценария мог ссылаться на нее.


Обеспечение улучшенная поддержка SDK для этих сценариев обсуждается в этот выпуск GitHub .

...