Переменная $_, используемая в функции из модуля, пуста (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 в командную строку и запускаю фрагмент примера, все в порядке.

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


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

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- 
....

person stej    schedule 07.06.2011    source источник


Ответы (3)


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

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

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?)

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

Вопросы и ответы, в которых также представлены ссылка на это сообщение об ошибке — почти та же самая тема, как и некоторые другие статьи блога, с которыми я столкнулся. Но хотя это было сообщено как ошибка, я не согласен.

На странице 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.

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

person Joel B Fant    schedule 18.06.2011
comment
Я столько раз видел проблемы с модулями и их областью действия/окружением, что считаю это особенностью PowerShell, которая была разработана не очень хорошо. Спасибо за ваше объяснение.. - person stej; 22.06.2011
comment
Я бы держался подальше от таких автоматических переменных при их использовании в разных областях. - person JasonMArcher; 24.06.2011
comment
Извините, этот ответ немного неверен, я думаю. @stej действительно должен был наградить меня баллами. :) Вы говорите, что это не имеет ничего общего с областью действия, на самом деле это все связано с областью действия, если быть точным, с деревьями областей действия. Модули имеют собственное дерево областей видимости, отсюда и такое поведение. Он написан открытым текстом в спецификации языка Powershell, как я указал в своем ответе ниже. - person CosmosKey; 27.06.2011
comment
В этом предложении я пояснил, что имел в виду под областью видимости, учитывая, что в то время я не знал о деревьях областей видимости. - person Joel B Fant; 27.06.2011

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

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

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

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

Процитировать текст метода GetNewClosure() в Спецификации языка Powershell (раздел 4.3). .7):

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

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

person CosmosKey    schedule 23.06.2011

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

person mjolinor    schedule 07.06.2011
comment
GetNewClosure() вроде этого Set-DbFile -prop {...}.GetNewClosure() не помогает. Я предполагаю, что за модулями будет какая-то магия. - person stej; 07.06.2011
comment
Отправился на работу, но при случае проведу тесты. Все симптомы, по-видимому, указывают на оценку этого блока скрипта. - person mjolinor; 07.06.2011
comment
$props = & $Properties.getnewclosure() — getnewclosure() вызывает переоценку блока скрипта на основе текущего значения любых переменных, используемых в блоке скрипта. Чтобы переоценить его для каждого нового значения $_, нужно вызвать getnewclosure внутри блока процесса. - person mjolinor; 07.06.2011
comment
Да, & $Properties.GetNewClosure() дает правильные результаты. Спасибо, проголосовал. Я оставлю его открытым, потому что мне очень хотелось бы знать, почему существует разница между функциями в модуле и вне модуля. - person stej; 08.06.2011
comment
Да, я видел это раньше. Нет ответа. Когда я перешел от вложенных функций к невложенным функциям в модуле, у меня были похожие проблемы, и я обошел их, выполнив $Script:Properties. - person Doug Finke; 18.06.2011