Чтобы максимально эффективно использовать мощный инструментарий Windows PowerShell, необходимо знать, как с помощью цикла foreach поочередно обращаться ко всем элементам коллекции, например строкового массива или списка служб Windows. В среде PowerShell реализовано два типа циклов foreach — инструкция foreach и команда ForEach-Object. Они позволяют добиваться аналогичных результатов, но отличаются друг от друга в некотором отношении. В предлагаемой статье я разъясню эти различия, а также покажу, как следует использовать инструкцию foreach и команду ForEach-Object.

Инструкция foreach

В ходе выполнения инструкции foreach оболочка PowerShell «перебирает» элементы коллекции. Для каждого элемента цикл проводится один раз; при этом выполняется блок инструкций, именуемый блоком сценария. Чтобы создать цикл foreach, нужно определить коллекцию, к элементам которой он будет обращаться, задать переменную, которая будет содержать все элементы данной коллекции, и составить блок, который будет выполняться при каждом шаге по элементам коллекции.

Поясним сказанное на примере. В следующей команде объявляется переменная $birds, затем она инициализируется со строковым массивом, после чего эта переменная используется в инструкции foreach:.

$birds = "owl", "crow", "robin", "wren", "jay"
oreach ($bird in $birds)
{
   "$bird = " + $bird.length
}

Инструкция foreach начинается с ключевого слова foreach, за которым следует пара скобок, заключающих три компонента ($bird in $birds). Первый компонент — это переменная цикла, которую мы определяем для использования в инструкции foreach. В данном случае переменная цикла имеет имя $bird, но пользователь может присвоить ей любое имя по своему усмотрению — лишь бы оно соответствовало соглашениям об именовании PowerShell. По мере того как цикл переходит от одного элемента коллекции к другому, переменная цикла всегда содержит текущую величину коллекции. Так, при первой итерации переменная $bird имеет значение owl, при второй — crow и т. д.

Второй компонент в скобках — ключевое слово in. Используйте его «как есть». Третий элемент — собственно коллекция, доступ к которой в данном случае осуществляется с помощью переменной $birds. Далее следует пара фигурных скобок. В них заключен блок сценария, выполняемый при каждом шаге цикла. В нашем примере этот блок содержит только одну инструкцию ("$bird = " + $bird.length), создающую простую строку, которая выводится на консоль. В данном случае переменная $bird получает величину из коллекции, а свойство Length считывает число символов этой величины.

Команда возвращает следующие результаты

owl = 3
crow = 4
robin = 5
wren = 4
jay = 3

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

$count = 0
$birds = "owl", "crow", "robin", `
   "wren", "jay"
foreach ($bird in $birds)
{
   $count += 1
   "$bird = " + $bird.length
   Write-Host
}
"Total number of birds is $count."

Первая инструкция в этом блоке сценария увеличивает значение переменной $count на единицу. Переменная $count определяется в первой строке и используется для подсчета промежуточной суммы элементов коллекции. Вторая инструкция формирует строку и передает ее на консоль, как в предыдущем примере. А третья инструкция — это команда Write-Host, которая попросту добавляет к выходным данным пустую строку.

При выполнении каждой итерации цикла выполняются все три инструкции блока сценария. Но код, следующий за блоком сценария, выполняется только раз — по завершении последней итерации цикла. Этот код использует переменную $count в выходных результатах. В данном случае значение этой переменной составляет 5. Данное значение присвоено переменной в ходе последней итерации, как показано в следующих результатах

owl = 3
crow = 4
robin = 5
wren = 4
jay = 3
Total number of birds is 5.

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

foreach ($svc in Get-Service)
{
   $svc.name + ": " +
   $svc.canstop.tostring ().toupper ()
}

В данном фрагменте кода третий компонент в скобках — команда Get-Service. Эта команда возвращает коллекцию объектов, где один объект представляет отдельную службу на локальной системе. Объект service назначается переменной $svc при каждой итерации цикла. В цикле инструкция foreach использует переменную $svc для получения имени службы (с помощью свойства Name объекта service) и добавляет к ней двоеточие. Далее инструкция foreach использует переменную $svc для обращения к свойству CanStop объекта service, которое возвращает логическое значение, указывающее, можно ли остановить данную службу после ее запуска. Наконец, инструкция foreach вызывает методы ToString и ToUpper для форматирования полученного значения. Но перед тем как переводить строку в верхний регистр с помощью метода ToUpper, необходимо перевести значение свойства CanStop в строковый формат с помощью метода ToString, поскольку метод ToUpper применяется только к строковым значениям. Если вы не хотите переводить строку результата в верхний регистр, можете обойтись без вызова метода ToString, как и метода ToUpper. Результаты представлены на экране 1.

Экран 1. Составление списка служб и получение значений для их величины CanStop

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

Всякий раз при определении коллекции в инструкции foreach мы, в сущности, реализуем конвейер. В приведенном выше примере конвейер формируется из выходных данных команды Get-Service. Но можно создавать и более сложные конвейеры, например такие:

foreach ($svc in Get-Service |
where {$_.status -eq 'running' })
{
   $svc.name + ": " +
   $svc.canstop.tostring (). toupper ()
}

В этой команде выходные данные Get-Service передаются по конвейеру команде Where-Object (вызываемой с помощью псевдонима where), которая выбирает из значений, возвращаемых командой Get-Service, только те объекты service, свойство Status которых определяется как running. Для обращения к текущему значению в конвейере команда Where-Object использует встроенную переменную $_. На экране 2 представлены примерные результаты, возвращаемые с помощью данной команды.

Экран 2. Составление списка только работающих служб

Возможно, вы пришли к выводу, что заключать весь конвейер в скобки не всегда удобно. Вероятно, более целесообразно передавать объекты service переменной, а затем вызывать эту переменную в инструкции foreach:

$svcs = Get-Service |
where {$_.status -eq 'running'}
foreach ($svc in $svcs)
{
   $svc.name + ": " +
   $svc.canstop.tostring ().
   toupper ()
}

Нетрудно убедиться, что для вызова коллекции инструкция foreach использует переменные $svc. Эта команда возвращает те же результаты, что и предыдущая.

Команда ForEach-Object

Мы рассмотрели, как используется инструкция foreach для перебора элементов коллекции, но это еще не все. Наряду с данной инструкцией в оболочке PowerShell реализована команда ForEach-Object. И чтобы жизнь не казалась пользователям совсем уж безмятежной, слово foreach служит псевдонимом для обращения к упомянутой команде.

ForEach-Object получает коллекцию из конвейера и, подобно инструкции foreach, поочередно обращается ко всем ее элементам. Поясним сказанное на примере. Следующая команда возвращает те же результаты (показанные на экране 2), что были получены в результате выполнения двух предыдущих команд:

Get-Service |
where {$_.status -eq 'running'} |
foreach {
   $_.name + ": " +
   $_.canstop.tostring ().toupper ()
}

Эта инструкция начинается с передачи по конвейеру выходных данных Get-Service команде Where-Object. Далее коллекция, возвращенная командой Where-Object, передается по конвейеру команде ForEach-Object (для обращения к последней используется псевдоним foreach). Обратите внимание, что за псевдонимом foreach следует только блок сценария — никакого кода в скобках нет. Следствие этого различия между инструкцией foreach и командой ForEach-Object состоит в том, что необходимость определения переменной элементов отпадает. Вместо нее используется встроенная переменная $_. Во всех прочих отношениях оставшаяся часть блока сценария ничем не отличается от кода, использовавшегося в двух предыдущих примерах (отметим, что открывающую фигурную скобку следует размещать на той же строке, где указывается псевдоним foreach, иначе PowerShell будет рассматривать первую строку как завершенную инструкцию).

Но как же тогда оболочка PowerShell отличает ключевое слово foreach от псевдонима foreach? Если foreach стоит в начале инструкции, PowerShell рассматривает ее как ключевое слово и обрабатывает следующий за ним код как инструкцию foreach. Если же это слово указывается в другом месте, PowerShell рассматривает его как псевдоним команды ForEach-Object.

В среде PowerShell предусмотрен еще один псевдоним для ссылки на команду ForEach-Object; это знак процента (%). Так, инструкция

Get-Service |
where {$_.status -eq
'running'} |
% {
   $_.name + ": "+
   $_.canstop.tostring ().
toupper ()
}

возвращает те же результаты, что и в предыдущем примере, хотя в ней вместо foreach используется символ %.

Различия

Итак, для получения одних и тех же результатов можно использовать как инструкцию foreach, так и команду ForEach-Object, но надо иметь в виду, что между ними существует несколько различий. Во-первых, как мы уже убедились, команда несколько проще, поскольку в случае ее применения не нужно создавать специальную переменную цикла — вместо нее используется встроенная переменная $_.

Второе отличие сводится к тому, каким образом среда PowerShell обрабатывает эти две инструкции. Когда PowerShell обрабатывает инструкцию PowerShell, она, перед тем как приступить к работе с отдельными величинами, генерирует всю коллекцию. Когда же оболочка обрабатывает команду ForEach-Object, она работает с каждым значением в момент соответствующего шага по конвейеру; таким образом, в каждый момент времени используются меньшие объемы памяти. В случаях, когда важно экономно расходовать ресурсы памяти, более целесообразно использовать команду.

Третье различие состоит в следующем. Если выходные данные команды ForEach-Object можно передать далее по конвейеру, то при использовании инструкции foreach такой возможности нет. В следующем фрагменте кода выходные данные команды ForEach-Object передаются команде Sort-Object:

Get-Service |
where {$_.status -eq 'running'} |
foreach {
   $_.name + ": " +
   $_.canstop.tostring ().toupper ()
} | sort -descending

Команда Sort-Object (вызываемая с использованием псевдонима sort) сортирует выходные данные конвейера в порядке убывания, как показано на экране 3.

Экран 3. Список работающих служб в обратном порядке

Еще одно преимущество команды ForEach-Object по сравнению с инструкцией foreach состоит в том, что команда поддерживает три типа блоков сценария, как видно из следующего фрагмента кода:

Get-Service |
where {$_.status -eq 'running'} |
foreach {
   $count = 0} {
   $_.name + ": " +
      $_.canstop.tostring ().toupper ()
   $count ++} {Write-Host
   "$count services are running."
   Write-Host
}

Первый блок сценария присваивает переменной $count значение 0. Эта переменная отслеживает число элементов в коллекции. Второй блок сценария получает значения свойств Name и CanStop для каждой службы и увеличивает значение переменной $count на 1. Третий блок сценария содержит указание распечатать сообщение, содержащее общее число служб, которое получено на основе последнего значения переменной $count.

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

Экран 4. Получение числа работающих служб

На следующих уроках

Инструкция foreach и команда ForEach-Object — мощные инструменты для работы с коллекциями. Любой из них можно использовать для создания циклов, в ходе которых выполняется набор инструкций для каждого элемента коллекции. При составлении сценариев PowerShell придется часто пользоваться инструкциями foreach. И, как будет видно из следующих статей, вы сможете создавать гораздо более сложные команды, нежели те, что были показаны выше.

Роберт Шелдон (contact@rhsheldon.com) — технический консультант и автор книг по технологиям Microsoft Windows и базам данных

Поделитесь материалом с коллегами и друзьями

Купить номер с этой статьей в PDF