Windows PowerShell: Жизненный цикл расширенной функции

OSzone.net » Microsoft » PowerShell » Windows PowerShell: Жизненный цикл расширенной функции
Автор: Дон Джонс
Иcточник: TechNet Magazine
Опубликована: 03.02.2012

Ряд слушателей моих курсов по Windows PowerShell на моем сайте испытывали трудности. Я надеюсь, что более подробный анализ позволит и вам избавиться от непонимания. Мы поговорим о расширенных функциях Windows PowerShell, которые неформально называют «сценарными командлетами». Шаблон функции этого типа выглядит так:

Function Do-Something {
       [CmdletBinding()]
       param(
              [Parameter(Mandatory=$True,
                         ValueFromPipeline=$True)]
              [string[]]$computername
       )
       BEGIN {}
       PROCESS {}
       END {}
}

У этих командлетов есть ряд вводящих в заблуждение особенностей. В частности, в этом комадлете есть входной параметр по имени -computername. Он может принимать входные данные по конвейеру. Это означает, что функцию можно вызывать двумя разными способами. Во-первых, ей можно передавать строки по конвейеру, например из текстового файла, содержащего по одному имени компьютера в строке:

Get-Content names.txt | Do-Something

Также можно просто передать одно или несколько имен компьютеров непосредственно в параметр, не используя конвейер:

Do-Something –computername SERVER1,SERVER2

В первом примере сначала выполняется блок BEGIN функции. Далее выполняется блок PROCESS — по одному разу для каждого поступившего на вход имени компьютера. Переменная $computername в любой момент времени может содержать только одно имя компьютера. Наконец по завершении выполнения указанных блоков выполнятся блок END.

Во втором примере блоки BEGIN и END вообще не выполняются. Блок PROCESS выполняется только раз, а через переменную $computername проходят все имена, переданные во входной параметр.

Такая большая разница в поведении может затруднять выполнения задач подготовки и очистки, которые выполняются в обоих ситуациях. Также могут возникать затруднения с использованием параметров $computername. В первом примере в каждый момент времени он содержит только одно значение, а во втором может быть несколько значений, которые заданы в параметре –computername. Разрешить проблему параметра $computername можно, просто разместив цикл ForEach внутри блока PROCESS:

Function Do-Something {
       [CmdletBinding()]
       param(
              [Parameter(Mandatory=$True,
                         ValueFromPipeline=$True)]
              [string[]]$computername
       )
       BEGIN {}
       PROCESS {
              Foreach ($computer in $computername) {
                     # use $computer here
              }
}
       END {}
}

При таком подходе в каждый момент времени переменная $computer будет содержать лишь одно имя компьютера, поэтому лучше работать именно с ней, а не $computername.

С подготовкой и очисткой дела обстоят сложнее. Нельзя выполнять подготовку в блоке PROCESS. Он выполняется несколько раз при передаче объектов по конвейеру. С другой стороны, подготовку нельзя разместить прямо в блоке BEGIN. Она просто не будет выполнена, если по конвейеру ничего не будет передано. Конечно, существует много способов решения этой проблемы, но такой кажется наилучшим:

Function Do-Something {
       [CmdletBinding()]
       param(
              [Parameter(Mandatory=$True,
                         ValueFromPipeline=$True)]
              [string[]]$computername
       )
       BEGIN {
        $setup_done = $false
        function DoSomethingSetup {
            set-variable -name setup_done -value $true -scope 1
        }
        DoSomethingSetup
    }
       PROCESS {
        if (-not $setup_done) { DoSomethingSetup }
              Foreach ($computer in $computername) {
                     # use $computer here
              }
    }
}

Здесь используется тот факт, что у BEGIN, PROCESS и END общая область действия, а точнее они используют одинаковые переменные. Определив переменную как флаг, можно обеспечить, что ключевой элемент блока BEGIN — моя функция DoSomethingSetup — будет вызвана лишь раз.

Заметьте, что в функции DoSomethingSetup нужно прибегать к особому ухищрению, чтобы присвоить флагу значение $True по завершении подготовки. Так как DoSomethingSetup является функцией, у нее собственная область видимости, и обычно она не может менять значения переменных за пределами этой области. Можно явно изменить нужную переменную с помощью командлета Set-Variable.

Сложнее применять этот прием для задач очистки. В первом примере, где объекты передаются в Do-Something по конвейеру, блок END выполняется. Задачи очистки можно разместить в нем. Однако во втором примере по конвейеру ничего не поступает, и END не выполняется. Блок PROCESS выполняется лишь раз, поэтому он вряд ли «узнает», что END выполняться не будет.

Блок PROCESS не может просто вызвать функцию из блока END. Когда объекты передаются по конвейеру, блок PROCESS выполняется несколько раз и совершенно нежелательно, чтобы столько же раз выполнялся блок END.

Есть несколько хитрых способов решения проблемы, но простой подход всегда оказывается наилучшим: Если нужно выполнить определенную очистку (например, закрыть подключение к базе данных), забудьте о блоках BEGIN и END. Считайте, что блок PROCESS должен выполняться только раз, и вставьте все задания подготовки и очистки в нем.

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

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


Ссылка: http://www.oszone.net/17113/