Основное предназначение циклов в Powershell, так же как и в других языках, выполнение итераций. Проще говоря они помогают выполнять одну и ту же инструкцию для разных значений. Отличительной чертой циклов в Powershell является их количество, всего их 7:
- Foreach-Object - команда;
- Foreach - выражение;
- Foreach() - метод;
- For - цикл;
- While - цикл;
- Do-While - цикл;
- Do-Until - цикл.
Каждый из них по своему выполняет итерации. Само понятие итераций обозначает повторное выполнение действий, например команд. Иногда можно услышать слова "итерируемый объект" или "итератор", которые обозначают объект проходящий через цикл.
Foreach-Object
Foreach-Object относится к командам, а не циклам. Чаще всего мы с ним работаем через конвейер.
Итерации
Так же, как и во многих других командах Powershell у Foreach-Object есть параметр InputObject, через который помещается объект для перебора. На примере ниже этот объект в виде массива с числами:
ForEach-Object -InputObject @(1,2,3,4) -Process {$PSItem}
$PSitem это переменная, которая хранит текущее значение массива. Мы ее выводим через параметр Process, в котором можно дополнить логику командами или условиями. Переменная $PSitem аналогична такому написанию $_ .
Если вы имеете опыт работы с Powershell, то чаще использовали с конвейером:
Get-Service | ForEach-Object {$PSitem.Name}
По сути в команде выше происходит то же самое, просто в функциях и командах есть возможность установить параметр, который принимает значения по умолчанию из конвейера. Я так же добавил вывод только имени, так как у объекта из Get-Service есть такое свойство. Более подробно это описывалось статьей по созданию команд и функций в Powershell. Параметры, которые принимают значения из конвейера можно увидеть так:
Get-Help ForEach-Object -Parameter *
Когда вам нужно использовать больше логики зажимайте shift + enter для перехода на новую строчку, если работаете из консоли. Скрипт ниже получает список процессов, передает их через конвейер, где через наш командлет происходит анализ время отклика CPU с выводом результата:
Get-Process | ForEach-Object -Process {
if ($PSItem.CPU -ge 100){Write-Host 'Процесс плохо работает' $PSItem.Name}
else {Write-Host 'Нормально работает' $PSItem.Name}
}
Можно использовать и другой синтаксис. Так я получу имена сервисов:
Get-Service | ForEach-Object -MemberName Name
Я не видел, что бы кто-то использовал такой синтаксис, но он возможен. На примере ниже я преобразую строки в массив двумя способами:
"Первый,второй,третий" | ForEach-Object -MemberName Split -ArgumentList ","
"Первый,второй,третий" | ForEach-Object {$PSItem.split(",")}
Split - это метод, который преобразует строку в массив используя указанный разделитель (в нашем случае запятая). Методы и свойства, доступные у объекта, можно увидеть через Get-Member:
"первый" | Get-Member
Сохранение через переменные
Результат этого командлета, так же как и любого другого можно сохранить в переменную и использовать дальше:
$result = ForEach-Object -InputObject @('localhost','127.0.0.1') -Process {Get-Service -ComputerName $PSItem}
$result
Алиасы
У этой команды есть алиасы foreach и знак %:
% -InputObject @(1,2,3) -Process {$PSItem}
@(1,2,3) | % {$PSItem}
@(1,2,3) | foreach {$PSItem}
Обратите внимание, что использование следующего синтаксиса приведет к ошибке:
foreach -InputObject @(1,2,3) -Process ($PSItem)
Связано это с тем, что в Powershell работает командлет и метод с одним именем. Каждый из них имеет свой синтаксис и вызывая foreach без конвейера мы используем метод. Он будет разобран следующим.
ForEach
Этот цикл, в отличие от предыдущего командлета, не будет работать через конвейер. Если вы попытаетесь использовать этот метод через конвейер, то будет использован командлет.
Это самый простой цикл Powershell, который переходит от одного значения объекта к другому. Синтаксис следующий:
ForEach ($item in $array){
Scriptblock
}
- $item - хранит текущий элемент из $array. Эту переменную можно вызвать в ScriptBlock;
- $array - это любая коллекция, из которой нужно получить значения;
- Scriptblock - это область для написания сценария и остальной логики.
Для примера с foreach в Powershell выполним перебор значений массива:
$array = @(1,2,3,4,5)
foreach ($item in $array){
$item
}
Так же как и в предыдущем случае каждый новое действие в ScriptBlock должно выполняться с новой строчки:
$array = @(1,2,3,4,5)
foreach ($item in $array){
$sum = $item + 10
Write-Host "Это сумма двух чисел: " $sum
}
Выше было написано, что мы не можем использовать конвейер, но исключение такое написание:
$(ForEach ($number in 4,5,6 ) { $number * 13}) | Write-Warning
Как вы можете догадаться - это не работа цикла с конвейером, а просто передача массива.
Использование с командами
Как уже писалось выше не стоит использовать метод Powershell foreach через конвейер. Например так мы можем получить список PSProvider:
$psproviders = Get-PSProvider
foreach ($psprovider in $psproviders){$psprovider.Name}
Можно добавлять выражение в сам цикл, но такой подход как минимум понизит читаемость кода. Самое главное экранировать выражение круглыми скобками:
foreach ($psprovider in (Get-PSProvider | where Name -Like *Reg*)){
$psprovider.Name
}
На примере ниже пример использования цикла и командлета с одни и тем же результатом:
$services = Get-Service
# командлет
$services | ForEach-Object -Process {
if ($PSItem.Name -eq 'WinRM'){
Write-Host "Статус сервиса WinRM" $PSItem.Status
}
}
# цикл
foreach ($service in $services){
if ($service.Name -eq 'WinRM'){
Write-Host "Статус сервиса WinRM" $service.Status
}
}
Как видно в случае с командой мы оперируем переменной $PSItem, а с циклом $service, так как мы ее определили еще в начале.
Работа с диапазоном или range
В Powershell можно легко указать диапазон численных значений. Я могу создать массив из чисел с 1 до 10 так:
1..10
Так же можно использовать и в итерациях:
foreach ($i in 1..10){$i}
Continue и break
Одним из отличий работы с foreach от аналогичной команды является возможность использования contnue и break.
Оператор continue минует выполнение оставшейся части скрипта после своего выполнения. На примере ниже есть два массива и если в одном из них будут встречаться одинаковые числа, то они не будут выведены:
$Numbers = 4..7
foreach ($Num in 1..10) {
if ($Numbers -Contains $Num) {
continue
}
$Num
}
В отличие от continue break останавливает итерации полностью. Его удобно использовать, когда мы ищем нужное значение и нам нужно остановить итерации после его нахождения:
$words = 'Pass1','Pass2','Stop','Pass3'
foreach ($word in $words) {
if ($word -eq 'Stop') {
break
}
$word
}
Вложенные
Когда у нас есть массив массивов может потребоваться использовать вложенные циклы Powershell. Само их использование не должно вызывать труда, но если вы попробуете использовать операторы break и continue это сработает иначе:
$array_list = @(
@('ok1','Stop'),
@('ok2', 'ok3')
)
foreach ($array in $array_list){
foreach ($word in $array){
if ($word -eq 'Stop'){Break}
$word
}
}
Как вы видите на примере выше у нас остановился только внутренний цикл. Если нужно избежать таких ситуаций используйте OUTER:
$array_list = @(
@('ok1','Stop'),
@('ok2', 'ok3')
)
:outer foreach ($array in $array_list){
foreach ($word in $array){
if ($word -eq 'Stop'){Break outer}
$word
}
}
Переменные
В этом типе цикла доступны три специальных переменных:
- $foreach.MoveNext() - переход к следующему элементу;
- $foreach.current - текущий элемент;
- $foreach.reset() - обнуляет итерацию. Перебор начнется заново, что приведет к бесконечному циклу.
Такие возможности есть во многих языках программирования, но мне тяжело вспомнить ситуации что бы я их использовал. Для примера ниже мы используем массив чисел с 1 по 10. Самое первое число 1 будет выведено, после чего будет пропущено следующее:
foreach ($Int in 1..10) {
$Int
$foreach.MoveNext() | Out-Null
}
$current просто выведет ту же переменную, объявленную в цикле:
foreach ($services in (Get-Service -Name *WinR*)) {
$foreach.current
$foreach.MoveNext() | Out-Null
}
Сравнение ForEach и команды ForEach-Object
Foreach | ForEach-Object |
---|---|
Загружает все элементы коллекции | Загружает только один элемент в память через конвейер |
Использует больше памяти из-за полной загрузки массивов | Меньшее использование памяти из-за одного элемента |
С небольшим объемом массивов работает быстрее | Работает медленнее |
Нельзя использовать через конвейер. Это не приведет к ошибке, но будет использован алиас командлета | Можно использовать конвейер или параметр InputObject |
Поддерживаются методы break и continue | Нельзя прервать используя методы contiinue и break |
Скорость работы обоих этих методов можно увидеть через следующие скрипты:
1..10 | Measure-Command -Expression {
Get-WMIObject Win32_LogicalDisk | ForEach-Object {
[math]::Round($_.FreeSpace/1GB,2)
}
}
1..10 | Measure-command -Expression {
$disks = Get-WMIObject Win32_LogicalDisk
foreach ($disk in $disks ){
[math]::Round($disk.FreeSpace/1GB,2)
}
}
Метод foreach()
В версии Powershell 4.0 появился метод foreach() для поддержки DSC. Он немного отличается синтаксисом и подходом от описаны выше. Более простой способ понять, как он работает это посмотреть на его синтаксис:
$collection = @(1,2,3,4,5)
$collection.Foreach(
{$PSItem}
)
Как я прочитал этот метод предназначен для работы только с коллекциями и по идеи такой способ должен привести к ошибке:
$chislo = 1
$chislo.Foreach(
{$PSItem + 1}
)
На всякий случай я бы советовал преобразовывать такие данные в массив:
@($chislo).Foreach(
{$PSItem + 1}
)
Работа с командами
Работы с командами не должна вызывать сложности. Для примера так мы получим список имен сервисов, которые остановлены:
(Get-Service).Foreach(
{if ($PSItem.Status -eq 'Stopped'){
Write-Host 'Этот сервис остановлен' $PSItem.Name
}
}
)
Самое главное экранировать результат команды в скобки или выполнять метод для переменной, которая уже хранит значения.
Еще один пример с сервисами:
$services = @('WinR*')
$services.Foreach({
Get-Service $PSItem
}
)
for
В Powershell есть еще один способ итераций через for. Его отличие в том, что мы можем изменять основной объект до выполнения ScriptBlock. Синтаксис следующий:
for (объект; условие; действие){
ScriptBlock
}
Для примера получим числа с 1 по 10:
for ($i=1; $i -le 10; $i++){
Write-Host $i
}
Поясню момент, который мог быть вызван написанием $i++, все следующие действия одинаковы, но не все сработают в этом цикле:
$i = 1
$i++
$i
$b = 1
$b += $b
$b
$c = 1
$c = $c + $c
$c
Вы можете изменять несколько объектов:
$j = 10
for ($i=1; $i -le 10; $i++,$j--){
Write-Host $i $j
}
В итерациях вы можете использовать любой тип данных, не только цифры:
for($s='' ;$s.length -le 10;$s=$s+'a'){
$s
}
Вы можете пропустить любую часть этого цикла, но если ничего не будет указано получится бесконечный цикл:
For( ; ; ) {
"Вечный цикл, нажмите ctrl+c для отмены"
}
Возможно использовать этот цикл с командлетами, но пример высосан из пальца:
for (
$check_site = Invoke-WebRequest -Uri "https://fixmypc.ru";
$check_site.StatusCode -eq 200;
){
Write-Warning 'Коричневый код. Предпринимаем действия'
#Тут какие-то команды
break
}
Операторы break и continue работают так же.
Powershell экспорт и запись в CSV файл
While
Как можно было увидеть выше мы рассматривали циклы, которые перебирали элементы массивов, но существует еще один тип позволяющий управлять этим процессом более гибко. Для примера нам может понадобится выполнять функцию, командлет или скрипт до тех пор пока мы не получим нужны результат и для этого можно использовать While:
$a = 0
While ($a -le 10){
$a
$a += 1
}
Пример выше работает до тех пор, пока переменная $a меньше или равна 10 или, другими словами, пока значение в скобках не станет True.
Более понятный пример это с утилитой ping. Когда мы потеряли доступ к интернету или упал сервер, то мы запускаем команду с ключом -t и она работает до тех пор пока мы не остановим этот процесс руками нажав Ctrl+c. Такая работа в Powershell относится к бесконечным циклам и на примере выглядит так:
While ($True){
ping fixmypc.ru
# sleep это задержка между запросами
sleep 3
# Для остановки нажмите ctrl+c
}
Все эти операции можно выполнить и с foreach, но это плохая практика. Например так можно реализовать аналогичный вечный цикл с for:
foreach ($i in @(1,2)){
ping fixmypc.ru
sleep 3
$foreach.Reset()
}
Break и continue
Ключевой момент работы с while это использование break, который остановит итерации. Мы пинговали сайт, но останавливали итерации руками и что бы этого не делать в дальнейшем нужно добавить условие при котором будет выполнен break:
while ($True){
$result = ping fixmypc.ru
$result
if ($result -like '*TTL*'){
Write-Host "Сайт заработал"
break
}
}
Очень важно хорошо продумывать логику остановки скрипта. Вечные циклы, которые могут работать в полноценных сриптах, программах и планировщиках будет очень тяжело отлавливать.
Такие итерации останавливают либо методом break, либо счетчиком, который был показан в первом примере и ниже. Если мы будем использовать счетчик и continue, который пропускает выполнения условия, то тоже можем получить вечный цикл:
$i = 0
while ($i -le 5){
if ($i -in @(2,3)){
Continue}
$i
$i+=1
}
Я не помню, что бы я хоть раз использовал continue в реальных задачах, но стоит помнить о существовании такой возможности.
Мы можем выполнять команду в условиях и объявлять в ней переменную для дальнейшего использования:
while ($service = Get-Service -Name *WinRm*){
if ($service.Status -eq 'Stopped'){
Write-Host "Сервис остановлен"
Write-Host "Приостановление работы"
break
}
else {
Write-Host "Сервис запущен"
Write-Host "Приостановление работы"
break
}
}
Do и While
Отличие работы этого цикла от предыдущего в том, что он проверяет условия после выполнения операций, а не до. Из-за этого, если условие изначально равно $False, цикл будет выполнен один раз:
$i = 1
Do {
"Итерация i=$i"
$i++
}
While($i -le 10)
Или поставим заведомо ложные условия при котором предыдущий цикл не стал бы работать:
Do {
"Итерация i=$i"
$i++
}
While($false)
Такие итерации можно использовать со случаями, когда от пользователя ожидается выбор действий:
$process = $False
Do {
$process = Read-Host "1. Включить сервер`n2. Выключить сервер`n3. Проверить логи`n4. Сделать что-то еще`nВыберете номер [1-4]"
switch($process){
1 {'Сервер включен'}
2 {'Сервер выключен'}
3 {'Проверено'}
4 {'Сделано вчера'}
Default {
Write-Host "Неверный выбор"
$process = $False
}
}
}
While ($process -eq $false)
Если вам интересны или вы не понимаете switch, то я бы советовал почитать "Как работать с Powershell Switch на примерах".
Do Until
Предыдущий цикл выполняет итерации до тех пор, пока главное условие равняется $True. Цикл Do Until имеет такой же синтаксис, но работает до тех пор пока условие не станет $True, то есть пока оно $False:
$process = $False
$i = 0
Do {
Write-Host "Итерация номер: " $i
$i++
if ($i -eq 10){$process = $True}
}
Until($process)
...
Подписывайтесь на наш Telegram канал
Теги: #powershell