Подтвердить что ты не робот

Переменная $_, используемая в функции из модуля, пуста (PowerShell)

Один вопрос для вас здесь;)

У меня есть эта функция:

function Set-DbFile {
    param(
        [Parameter(ValueFromPipeline=$true)]
        [System.IO.FileInfo[]]
        $InputObject,
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [scriptblock]
        $Properties
    )
    process {
        $InputObject | % { 
            Write-Host `nInside. Storing $_.Name
            $props = & $Properties
            Write-Host '  properties for the file are: ' -nonew
            write-Host ($props.GetEnumerator()| %{"{0}-{1}" -f $_.key,$_.Value})
        }
    }
}

Посмотрите на $Properties. Он должен быть оценен для каждого файла, а затем файл и свойства должны быть обработаны далее.

Пример использования может быть:

Get-ChildItem c:\windows |
    ? { !$_.PsIsContainer } |
    Set-DbFile -prop { 
        Write-Host Creating properties for $_.FullName
        @{Name=$_.Name } # any other properties based on the file
    }

Когда я копирую и вставляю функцию Set-dbFile в командную строку и запускаю фрагмент примера, все в порядке.

Однако, когда я храню функцию в модуле, импортирую ее и запускаю пример, переменная $_ пуста. Кто-нибудь знает, почему? И как его решить? (другие решения также приветствуются)


Результаты для функции, определенной в script/напечатанном в командной строке:

Inside. Storing adsvw.ini
Creating properties for C:\windows\adsvw.ini
  properties for the file are: Name-adsvw.ini

Inside. Storing ARJ.PIF
Creating properties for C:\windows\ARJ.PIF
  properties for the file are: Name-ARJ.PIF
....

Результаты для функции, определенной в модуле:

Inside. Storing adsvw.ini
Creating properties for
  properties for the file are: Name-

Inside. Storing ARJ.PIF
Creating properties for
  properties for the file are: Name- 
....
4b9b3361

Ответ 1

Похоже, что GetNewClosure() работает так же хорошо, как и все, но изменяет способ, которым блок script видит эти переменные. Передача $_ в скриптблоке в качестве аргумента также работает.

Это не имеет никакого отношения к обычным проблемам с областью (например, глобальный или локальный), но сначала это выглядит так. Здесь мое очень упрощенное воспроизведение и некоторое объяснение:

script.ps1 для обычного точечного источника:

function test-script([scriptblock]$myscript){
    $message = "inside"
    &{write-host "`$message from $message"}    
    &$myscript
}

Module\MyTest\MyTest.psm1 для импорта:

function test-module([scriptblock]$myscript){
    $message = "inside"
    &{write-host "`$message from $message"}    
    &$myscript
}

function test-module-with-closure([scriptblock]$myscript){
    $message = "inside"
    &{write-host "`$message from $message"}    
    &$myscript.getnewclosure()
}

Вызовы и вывод:

» . .\script.ps1

» import-module mytest

» $message = "outside"

» $block = {write-host "`$message from $message (inside?)"}

» test-script $block
$message from inside
$message from inside (inside?)

» test-module $block
$message from inside
$message from outside (inside?)

» test-module-with-closure $block
$message from inside
$message from inside (inside?)

Итак, я начал охотиться, так как это вызвало мое любопытство, и я нашел несколько интересных вещей.

Этот Q & A, который также содержит ссылку на этот отчет об ошибке в значительной степени является одной и той же темой, как и некоторые другие статьи в блогах, которыми я сталкивался. Но пока это было сообщено как ошибка, я не согласен.

На этой странице about_Scopes можно сказать (w:

...

Restricting Without Scope

  A few Windows PowerShell concepts are similar to scope or interact with 
  scope. These concepts may be confused with scope or the behavior of scope.

  Sessions, modules, and nested prompts are self-contained environments,
  but they are not child scopes of the global scope in the session.

  ...

  Modules:
    ...

    The privacy of a module behaves like a scope, but adding a module
    to a session does not change the scope. And, the module does not have
    its own scope, although the scripts in the module, like all Windows
    PowerShell scripts, do have their own scope. 

Теперь я понимаю поведение, но это было выше и еще несколько экспериментов, которые привели меня к этому:

  • Если мы изменим $message в скриптблоке на $local:message, тогда все 3 теста имеют пустое пространство, потому что $message не определено в локальной области сценариев.
  • Если мы используем $global:message, все 3 теста печатают outside.
  • Если мы используем $script:message, первые 2 теста печатают outside и последние отпечатки inside.

Затем я также прочитал это в about_Scopes:

Numbered Scopes:
    You can refer to scopes by name or by a number that
    describes the relative position of one scope to another.
    Scope 0 represents the current, or local, scope. Scope 1
    indicates the immediate parent scope. Scope 2 indicates the
    parent of the parent scope, and so on. Numbered scopes
    are useful if you have created many recursive
    scopes.
  • Если мы используем $((get-variable -name message -scope 1).value), чтобы попытаться получить значение из непосредственной родительской области, что произойдет? Мы по-прежнему получаем outside, а не inside.

В этот момент мне было достаточно ясно, что сеансы и модули имеют свою собственную область объявления или контекст сортировок, по крайней мере для блоков script. Блоки script действуют как анонимные функции в среде, в которой они объявляются до тех пор, пока вы не назовете GetNewClosure() на них, и в этот момент они интернализуют копии переменных, которые они ссылаются с тем же именем в области, где GetNewClosure() (сначала с использованием локальных жителей, вплоть до глобальных). Быстрая демонстрация:

$message = 'first message'
$sb = {write-host $message}
&$sb
#output: first message
$message = 'second message'
&$sb
#output: second message
$sb = $sb.getnewclosure()
$message = 'third message'
&$sb
#output: second message

Надеюсь, это поможет.

Добавление. Что касается дизайна.

Комментарий JasonMArcher заставил меня задуматься над проблемой дизайна, когда скриптовый блок был передан в модуль. В коде вашего вопроса, даже если вы используете обходной путь GetNewClosure(), вы должны знать имя переменной (ов), где будет выполняться скриптблока, чтобы он работал.

С другой стороны, если вы использовали параметры для скриптового блока и передали ему $_ в качестве аргумента, скриптблоку не нужно знать имя переменной, ему нужно только знать, что аргумент определенного типа будет передаваться. Таким образом, ваш модуль будет использовать $props = & $Properties $_ вместо $props = & $Properties.GetNewClosure(), и ваш скрипт-блок выглядит примерно так:

{ (param [System.IO.FileInfo]$fileinfo)
    Write-Host Creating properties for $fileinfo.FullName
    @{Name=$fileinfo.Name } # any other properties based on the file
}

См. ответ CosmosKey для дальнейшего уточнения.

Ответ 2

Проблема здесь сводится к иерархии областей. Если вы определяете две функции, такие как...

function F1{
    $test="Hello"
    F2
}
function F2{
    $test
}

Затем F2 наследует область переменной F1, поскольку она вызвала из области F1. Если вы определяете функцию F2 в модуле и экспортируете функцию, переменная $test недоступна, так как модуль имеет собственное дерево областей. См. Спецификация языка Powershell (раздел 3.5.6):

В вашем случае текущая переменная node определена в локальной области и, следовательно, она не будет выходить в область видимости модуля, поскольку она находится в другом дереве с другим корнем области (помимо глобальных переменных).

Чтобы процитировать текст метода GetNewClosure() в Спецификации языка Powershell (раздел 4.3.7):

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

... следовательно, GetNewClosure() работает с лечением, поскольку он объединяет локальный размах области/модуля. Надеюсь, это поможет.

Ответ 3

Я думаю, вам нужно вызвать getnewclosure() в этом блоке script, прежде чем запускать его. Вызывается из файла или модуля script, блоки script оцениваются во время компиляции. Когда вы работаете с консоли, нет "времени компиляции". Он оценивался во время выполнения, поэтому он ведет себя по-разному, чем когда он находится в модуле.