Циклы ForEach в PowerShell: типы и лучшие практики

Когда вы впервые начинаете писать сценарии PowerShell, вы непременно дойдете до момента, когда вам потребуется обработать несколько элементов из коллекции. Именно тогда вам придется изучить циклы foreach в PowerShell и понять, в чем заключается их суть.

Практически все языки программирования имеют конструкцию, называемую циклами; PowerShell здесь не исключение. Один из самых популярных типов циклов в PowerShell – это цикл foreach. В своей основе цикл foreach считывает всю коллекцию элементов и для каждого элемента выполняет определенный код.

Одним из самых запутанных аспектов цикла foreach в PowerShell для начинающих является множество вариантов его использования. Есть не только один способ обработки каждого элемента в коллекции; их три!

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

Основы цикла PowerShell ForEach

Один из самых распространенных типов циклов, которые вы будете использовать в PowerShell, – это цикл типа foreach. Цикл foreach считывает набор объектов (итерирует) и завершается, когда завершается работа с последним из них. Коллекция объектов, которую считывает цикл, обычно представлена массивом или хэш-таблицей.

ПРИМЕЧАНИЕ: Термин “итерация” – это программный термин, который относится к каждому выполнению цикла. Каждый раз, когда цикл завершает цикл, это называется итерацией. Действие запуска цикла по набору объектов обычно называется итерацией по набору.

Возможно, вам нужно создать текстовый файл в разных папках, распределенных по файловой системе. Допустим, пути к папкам таковы: C:\Folder, C:\Program Files\Folder2 и C:\Folder3. Без использования цикла нам пришлось бы ссылаться на команду Add-Content три раза.

Add-Content -Path 'C:\Folder\textfile.txt' -Value 'This is the content of the file'
Add-Content -Path 'C:\Program Files\Folder2\textfile.txt' -Value 'This is the content of the file'
Add-Content -Path 'C:\Folder2\textfile.txt' -Value 'This is the content of the file'

Единственное отличие между каждой из этих командных ссылок – это значение Path. Значение для Path – единственное значение, которое меняется среди каждого из них.

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

$folders = @('C:\Folder','C:\Program Files\Folder2','C:\Folder3')

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

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

Цикл foreach

Первый тип цикла foreach – это оператор. foreach – внутренний ключевой элемент PowerShell, который не является cmdlet или функцией. Оператор foreach всегда используется в форме: foreach ($i in $array).

Используя приведенный выше пример, переменная $i представляет собой итератор или значение каждого элемента в $path, поскольку он проходит через каждый элемент в массиве.

Обратите внимание, что переменная итератора не обязательно должна быть $i. Имя переменной может быть любым.

В приведенном ниже примере вы можете выполнить ту же задачу, что и повторение ссылки Add-Content, сделав следующее:

# Создать массив папок
$folders = @('C:\Folder','C:\Program Files\Folder2','C:\Folder3')

# Выполнить итерацию для создания того же файла в каждой папке
foreach ($i in $folders) {
    Add-Content -Path "$i\SampleFile.txt" -Value "This is the content of the file"
}

Выражение foreach известно как более быстрый альтернативный способ, чем использование ForEach-Object командлета.

The ForEach-Object CmdLet

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

$folders = @('C:\Folder','C:\Program Files\Folder2','C:\Folder3')
$folders | ForEach-Object (Add-Content -Path "$_\SampleFile.txt" -Value "This is the content of the file")

Примечание: Чтобы сделать вещи запутанными, у командлета ForEach-Object есть псевдоним с именем foreach. В зависимости от того, как вызывается термин “foreach”, запускается выражение foreach или выполняется ForEach-Object.

A good way to differentiate these situations is to notice if the term “foreach” is followed by ($someVariable in $someSet). Otherwise, in any other context, the code author is probably using the alias for ForEach-Object.

Метод foreach()

Один из самых новых циклов foreach был представлен в PowerShell v4 и называется методом foreach(). Этот метод существует на массиве или объекте коллекции. Метод foreach() имеет стандартный параметр блока сценария, который содержит действия для выполнения на каждой итерации, как и другие.

$folders = @('C:\Folder','C:\Program Files\Folder2','C:\Folder3')
$folders.ForEach({
	Add-Content -Path "$_\SampleFile.txt" -Value "This is the content of the file"
})

Самое значительное отличие метода foreach() заключается в том, как он работает внутри.

Использование метода foreach() значительно быстрее и заметно на больших наборах данных. Рекомендуется использовать этот метод, если это возможно.

Мини-проект: Перебор набора имен серверов

Одно из самых распространенных применений цикла в PowerShell – чтение набора серверов из какого-либо источника и выполнение некоторого действия с каждым из них. Для нашего первого мини-проекта давайте создадим код, который позволит нам считывать имена серверов (по одному на строке) из текстового файла и выполнять пинг для каждого сервера, чтобы определить, в сети он или нет.

List of server names in a text file

Для этого рабочего процесса, какой тип цикла, на ваш взгляд, будет работать лучше?

Обратите внимание, что я упомянул слово “набор”, как “набор серверов”. Это уже подсказка. У вас уже есть определенное количество имен серверов, и вы хотели бы выполнить некоторое действие с каждым из них. Это звучит как отличный случай для применения самого распространенного цикла PowerShell – цикла foreach.

Первая задача – создать набор имен серверов в коде. Самый распространенный способ сделать это – создать массив. К счастью для нас, Get-Content, по умолчанию, возвращает массив, в котором каждый элемент представлен одной строкой в текстовом файле.

Сначала создайте массив всех моих имен серверов и назовите его $servers.

$servers = Get-Content .\servers.txt

Теперь, когда у вас создан массив, вам нужно подтвердить, онлайн ли каждый сервер или нет. Отличным командлетом для проверки подключения к серверу является Test-Connection. Этот командлет выполняет несколько тестов подключения к компьютеру, чтобы определить, онлайн ли он или нет.

Выполнив Test-Connection внутри цикла foreach, чтобы прочитать каждую строку файла, вы можете передать каждое имя сервера, представленное переменной $server, в Test-Connection. Вот как PowerShell перебирает текстовый файл. Пример работы приведен ниже.

foreach ($server in $servers) {
	try {
		$null = Test-Connection -ComputerName $server -Count 1 -ErrorAction STOP
		Write-Output "$server - OK"
	}
	catch {
		Write-Output "$server - $($_.Exception.Message)"
	}
}

Когда выполнится цикл foreach, он будет выглядеть примерно так:

Looping Test-Connection through a list of server names

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

Вы успешно практиковали то, что мы проповедуем, метод DRY.

Примеры использования цикла ForEach в PowerShell

Давайте рассмотрим несколько примеров того, как использовать цикл ForEach. Они основаны на реальных сценариях использования, демонстрируя концепцию, которую вы можете изменить под свои требования.

Пример 1: Создание файла в каждой подпапке в каталоге с использованием оператора ForEach

Этот пример демонстрирует общее использование цикла PowerShell для обработки каждой папки в каталоге.

Предположим, что в папке C:\ARCHIVE_VOLUMES есть десять подпапок. Каждая подпапка представляет собой архивный объем, который ежедневно резервируется. После завершения каждого резервного копирования создается файл с именем BackupState.txt внутри каждой папки, содержащий дату его создания.

Приведен пример того, как это может выглядеть.

Sub-directories under C:\ARCHIVE_VOLUMES

Сценарий ниже выполняет три действия:

  • получает список всех подпапок внутри C:\ARCHIVE_VOLUMES
  • проходит по каждой папке
  • создает текстовый файл с именем BackupState.txt, содержащий текущую дату и время его значения

В этом примере используется оператор foreach.

# Определение верхнего уровня папки
$TOP_FOLDER = "C:\ARCHIVE_VOLUMES"

# Получение всех подпапок рекурсивно
$Child_Folders = Get-ChildItem -Path $TOP_FOLDER -Recurse | Where-Object { $_.PSIsContainer -eq $true }

# Создание текстового файла в каждой подпапке и добавление текущей даты/времени в качестве значения.
foreach ($foldername in $Child_Folders.FullName) {
   (get-date -Format G) | Out-File -FilePath "$($foldername)\BackupState.txt" -Force
}

С использованием cmdlet Get-ChildItem вы можете подтвердить, что файлы были созданы или обновлены в каждой из подпапок.

Get-ChildItem -Recurse -Path C:\ARCHIVE_VOLUMES -Include backupstate.txt | Select-Object Fullname,CreationTime,LastWriteTime,Length

Снимок экрана ниже показывает вывод скрипта, отображающего все файлы BackupState.txt, найденные в каждом подкаталоге.

A text file is created in each sub-directory

Пример 2: Чтение содержимого каждого текстового файла в подкаталогах

Далее, чтобы продемонстрировать использование цикла PowerShell для каждого файла в каталоге, ниже приведенный скрипт будет читать каждый файл BackupState.txt, созданный в Примере 1.

  • Рекурсивно найти все файлы BackupState.txt внутри каждого подкаталога.
  • Используя оператор foreach, читайте каждый текстовый файл, чтобы получить значение “последнего времени резервного копирования”.
  • Отобразите результат на экране.
## Найти все файлы BackupState.txt в C:\ARCHIVE_VOLUMES
$files = Get-ChildItem -Recurse -Path C:\ARCHIVE_VOLUMES -Include 'BackupState.txt' | Select-Object DirectoryName,FullName

## Прочитать содержимое каждого файла
foreach ($file in $files) {
    Write-Output ("$($file.DirectoryName) last backup time - " + (Get-Content $file.FullName))
}

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

Using foreach to loop through files and read its contents.

Пример 3: Получение служб и их запуск с использованием командлета ForEach-Object

Системным администраторам часто требуется получать состояние служб и запускать либо ручной, либо автоматический рабочий процесс для устранения неполадок в службах. Давайте рассмотрим примерный скрипт с использованием командлета ForEach-Object.

Для этого примера ниже приведен скрипт, который выполнит следующее:

  • Получить список служб, настроенных на автоматический запуск, но в настоящее время не находящихся в запущенном состоянии.
  • Далее элементы списка передаются в командлет ForEach-Object, чтобы попытаться запустить каждую службу.
  • A message of either success or failed is displayed depending on the result of the Start-Service command.
## Получение списка автоматических служб, которые остановлены.
$services = Get-Service | Where-Object {$.StartType -eq 'Automatic' -and $.Status -ne 'Running'}

## Передача каждого объекта службы в конвейер и их обработка с помощью командлета Foreach-Object
$services | ForEach-Object {
    try {
        Write-Host "Attempting to start '$($.DisplayName)'"
        Start-Service -Name $.Name -ErrorAction STOP
        Write-Host "SUCCESS: '$($.DisplayName)' has been started"
    } catch {
        Write-output "FAILED: $($.exception.message)"
    }
}

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

Using the ForEach-Object loop to start services

Пример 4: Чтение данных из CSV с использованием метода ForEach()

Использование данных из файлов CSV популярно среди системных администраторов. Размещение записей в файле CSV упрощает выполнение массовых операций с использованием комбинации Import-CSV  и ForEach. Эта комбинация обычно используется для создания нескольких пользователей в Active Directory.

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

"Firstname","Lastname"
"Grimm","Reaper"
"Hell","Boy"
"Rick","Rude"

Теперь переходим к следующему блоку сценария. Сначала импортируйте CSV-файл, передав путь к содержимому в cmdlet Import-CSV. Затем, используя метод foreach(), перебирайте имена и создавайте новых пользователей в Active Directory.

# Импорт списка Имени и Фамилии из CSV-файла
$newUsers = Import-Csv -Path .\Employees.csv

Add-Type -AssemblyName System.Web

# Обработка списка
$newUsers.foreach(
    {
        # Генерация случайного пароля
        $password = [System.Web.Security.Membership]::GeneratePassword((Get-Random -Minimum 20 -Maximum 32), 3)
        $secPw = ConvertTo-SecureString -String $password -AsPlainText -Force

        # Формирование имени пользователя
        $userName = '{0}{1}' -f $_.FirstName.Substring(0, 1), $_.LastName

        # Создание новых атрибутов пользователя
        $NewUserParameters = @{
            GivenName       = $_.FirstName
            Surname         = $_.LastName
            Name            = $userName
            AccountPassword = $secPw
        }

        try {
            New-AdUser @NewUserParameters -ErrorAction Stop
            Write-Output "User '$($userName)' has been created."
        }
        catch {
            Write-Output $_.Exception.Message
        }
    }
)

При выполнении этого кода у вас теперь должен быть создан пользователь AD для каждой строки в CSV-файле!

Summary

Логика цикла foreach в PowerShell ничем не отличается от других языков программирования. Она изменяется только в том, как она используется и какой вариант цикла foreach выбран для конкретной задачи.

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

Дополнительное чтение

Source:
https://adamtheautomator.com/powershell-foreach/