Поддержка параметров ScriptBlock лексической области (например, Where-Object)

Рассмотрим следующую произвольную функцию и тестовые примеры:

Function Foo-MyBar {
    Param(
        [Parameter(Mandatory=$false)]
        [ScriptBlock] $Filter
    )

    if (!$Filter) { 
        $Filter = { $true } 
    }

    #$Filter = $Filter.GetNewClosure()

    Get-ChildItem "$env:SYSTEMROOT" | Where-Object $Filter   
}

##################################

$private:pattern = 'T*'

Get-Help Foo-MyBar -Detailed

Write-Host "`n`nUnfiltered..."
Foo-MyBar

Write-Host "`n`nTest 1:. Piped through Where-Object..."
Foo-MyBar | Where-Object { $_.Name -ilike $private:pattern  }

Write-Host "`n`nTest 2:. Supplied a naiive -Filter parameter"
Foo-MyBar -Filter { $_.Name -ilike $private:pattern }

В тесте 1 мы пропускаем результаты Foo-MyBar через фильтр Where-Object, который сравнивает возвращенные объекты с шаблоном, содержащимся в частной переменной $private:pattern. В этом случае это правильно возвращает все файлы/папки в C:\, которые начинаются с буквы T.

В тесте 2 мы передаем тот же скрипт фильтрации непосредственно в качестве параметра в Foo-MyBar. Однако к тому времени, когда Foo-MyBar запускает фильтр, $private:pattern не попадает в область действия, и поэтому он не возвращает никаких элементов.

Я понимаю, почему это так, потому что ScriptBlock, переданный в Foo-MyBar, не является замыканием, поэтому не закрывается над переменной $private:pattern, и эта переменная теряется.

Из комментариев я заметил, что ранее у меня был ошибочный третий тест, который пытался пройти {...}.GetNewClosure(), но это не закрывало переменные с частной областью действия. Спасибо @PetSerAl за помощь в разъяснении этого. .

Вопрос в том, как Where-Object фиксирует значение $private:pattern в тесте 1 и как добиться такого же поведения в наших собственных функциях/командлетах?

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

Замечу, что если я раскомментирую строку $Filter = $Filter.GetNewClosure() внутри Foo-MyBar, то она никогда не вернет никаких результатов, потому что $private:pattern теряется.

(Как я уже сказал выше, функция и параметр здесь произвольны, как кратчайшее воспроизведение моей реальной проблемы!)


person jimbobmcgee    schedule 16.08.2018    source источник
comment
Можете ли вы перепроверить Тест 3? Насколько я знаю, GetNewClosure не фиксирует private переменных.   -  person user4003407    schedule 16.08.2018
comment
@PetSerAl - возможно, вы правы, хотя теперь я наблюдаю другое поведение в одном окне ISE (не на вкладке) по сравнению с другим. В окне ISE, в котором я тестировал, $private:pattern закрывается, но я не могу воссоздать его в новом окне. Однако вопрос остается в силе, учитывая, что Тест 1 все еще работает, как описано.   -  person jimbobmcgee    schedule 16.08.2018
comment
@PetSerAl - хорошо, Remove-Variable pattern сбросьте его - я должен был определить $pattern в какой-то момент ранее. Я обновлю, чтобы удалить ссылку на тест 3   -  person jimbobmcgee    schedule 16.08.2018
comment
Простой способ решить эту проблему - поместить Foo-MyBar в отдельный модуль.   -  person user4003407    schedule 16.08.2018


Ответы (2)


Приведенный пример не работает, потому что вызов функции по умолчанию войдет в новую область. Where-Object по-прежнему будет вызывать сценарий фильтра, не вводя его, но область действия функции не имеет переменной private.

Есть три способа обойти это.

Поместите функцию в другой модуль, чем вызывающий

Каждый модуль имеет SessionState, у которого есть собственный стек SessionStateScope. Каждый ScriptBlock привязан к SessionState, который был проанализирован.

Если вы вызываете функцию, определенную в модуле, новая область создается внутри SessionState этого модуля, но не на верхнем уровне SessionState. Поэтому, когда Where-Object вызывает скрипт фильтра без входа в новую область, он делает это в текущей области для SessionState, к которой привязан этот ScriptBlock.

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

Вызовите функцию с точечным оператором источника

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

. { 'same scope' }
. Foo-MyBar

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

Написать командлет

Скомпилированные команды (командлеты) не создают новую область при вызове. Вы также можете использовать API, похожие на то, что использует Where-Object (хотя и не точно такие же).

Вот грубая реализация того, как вы могли бы реализовать Where-Object с помощью общедоступного API.

using System.Management.Automation;

namespace MyModule
{
    [Cmdlet(VerbsLifecycle.Invoke, "FooMyBar")]
    public class InvokeFooMyBarCommand : PSCmdlet
    {
        [Parameter(ValueFromPipeline = true)]
        public PSObject InputObject { get; set; }

        [Parameter(Position = 0)]
        public ScriptBlock FilterScript { get; set; }

        protected override void ProcessRecord()
        {
            var filterResult = InvokeCommand.InvokeScript(
                useLocalScope: false,
                scriptBlock: FilterScript,
                input: null,
                args: new[] { InputObject });

            if (LanguagePrimitives.IsTrue(filterResult))
            {
                WriteObject(filterResult, enumerateCollection: true);
            }
        }
    }
}
person Patrick Meinecke    schedule 16.08.2018
comment
Не могу поверить, что я упустил из виду расширение PSCmdlet, это должен быть принятый ответ, +1 - person Mathias R. Jessen; 17.08.2018
comment
@mklement0 в сценарии, когда вызывающая сторона извне точки модуля получает функцию из модуля, текущая область, скорее всего, будет самой высокой областью действия для состояния сеанса. По сути, точечный поиск чего-либо из другого состояния сеанса не приведет к его вызову в вашей текущей области, а в текущей области состояния сеанса (модуля), к которому привязана команда. Дополнительная литература - person Patrick Meinecke; 17.08.2018
comment
Благодарю за разъяснение; Назвать такое поведение удивительным — ничего не сказать. Пересмотрите целевую область внутри модуля: может ли это когда-либо быть чем-то другим, кроме области верхнего уровня модуля? Ссылка на ваш блог не работает, потому что компонент пути не весь в нижнем регистре; вот рабочая ссылка: seeminglyscience.github.io /powershell/2017/09/30/ - person mklement0; 17.08.2018
comment
@mklement0 хороший вопрос! Да, может, но гораздо менее вероятно. Допустим, у ModuleA есть FunctionA и FunctionB, а у ModuleB есть FunctionC. Если FunctionA вызывает FunctionC и FunctionC точечные источники FunctionB, то FunctionB будет вызываться в области действия, созданной вызовом FunctionA. Надеюсь, это хотя бы смутно понятно... масштабы... сложны. Также спасибо за исправление URL! Автокоррекция снова срабатывает. - person Patrick Meinecke; 18.08.2018
comment
Просто чтобы усложнить задачу: области видимости представляют собой не стек, а дерево, потому что одна родительская область может иметь несколько активных дочерних областей одновременно. - person user4003407; 18.08.2018
comment
@mklement0 For example: each instance of f have its own -Scope 0 $a, but -Scope 1 $a делится между ними. - person user4003407; 19.08.2018
comment
Пересмотрев это, я понял, что реализация командлета в этом ответе в принципе работает, но аргумент ScriptBlock не поддерживает использование $_ для ссылки на текущий объект конвейера (вместо этого объект передается через позиционный аргумент). Обходной путь громоздкий — см. это обсуждение на GitHub и решение, написанное на PowerShell , этот ответ. - person mklement0; 23.01.2019

как Where-Object фиксирует значение $private:pattern в тесте 1

Как видно из исходного кода кода для Where-Object в PowerShell Core, PowerShell внутренне вызывает скрипт фильтра, не ограничивая его собственной локальной областью (_script — это закрытое резервное поле для параметра FilterScript, обратите внимание, что аргумент useLocalScope: false передается в DoInvokeReturnAsIs()):

protected override void ProcessRecord()
{
    if (_inputObject == AutomationNull.Value)
        return;

    if (_script != null)
    {
        object result = _script.DoInvokeReturnAsIs(
            useLocalScope: false, // <-- notice this named argument right here
            errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe,
            dollarUnder: InputObject,
            input: new object[] { _inputObject },
            scriptThis: AutomationNull.Value,
            args: Utils.EmptyArray<object>());

        if (_toBoolSite.Target.Invoke(_toBoolSite, result))
        {
            WriteObject(InputObject);
        }
    }
    // ...
}

как добиться такого же поведения в наших собственных функциях/командлетах?

Мы этого не делаем – DoInvokeReturnAsIs() (и аналогичные средства вызова блоков сценариев) помечены internal и, следовательно, могут вызываться только типами, содержащимися в сборке System.Management.Automation.

person Mathias R. Jessen    schedule 16.08.2018