Ключевой особенностью Powershell, которая отличает его от других языков, это работа с конвейером. Дело в том, что каждая команда в Powershell возвращает множество объектов, а не один объект типа строка. Такой метод дает дополнительные возможности для работы с языком. Конвейер так же называют pipe или pipeline. В это статье будут рассмотрены примеры работы конвейера, его сравнение с bash, отличие от циклов и создание функции принимающей данные с конвейера.
Как работает конвейер
Конвейер (pipline, pipe) - это возможность, которая позволяет удобно обменяться данными между командами. В большинстве языков пайплайн определяется символом '|'. Значения, которые указаны в левой части, передаются на чтения справа.
Если бы мы использовали конвейер с буквами, то получилось бы следующее:
'а' | 'б' | 'в' = 'абв'
В примере ниже слева указана строка, которая выводится через команду справа:
'привет' | Write-Output
Предыдущий пример идентичен, по функциональности и результату, следующему выполнению:
Write-Output 'привет'
Суть в том, что мы можем указать несколько значений в левой части. Каждое из этих значений будет передаваться асинхронно (т.е. отдельно). Значения, которые разделяются запятыми в Powershell, образуют массив:
'привет', 'пока' | Write-Output
В командах без конвейера мы редко можем использовать массивы в качестве значений. Что бы получить результат аналогичный предыдущей команде мы должны вызвать ее дважды:
Write-Output 'привет'
Write-Output 'пока'
Учитывая, что Powershell называют языком команд (или сценариев), нам просто удобнее использовать конвейер совмещая несколько команд. Чаще всего он используется в связке с "Select-Object" (выбор 'колонки') и/или с "Where-Object" (условие).
Так мы получим только день из сегодняшней даты:
Get-Date | Select Day
stdin, stdout и stderr
Если вам сложно понять работу конвейера с примеров выше вы можете ознакомиться с понятием потоков (streams). Потоки реализованы в *nix системах и Windows. В Powershell реализовано 7 потоков, но основная концепция понятна в 3 потоках, которые так же реализованы в *nix системах. Это:
- stdin - ввод данных. Этот поток используется, когда вы печатаете текст или, например, когда результат работы одной команды принимает следующая. На уровне Powershell этого потока нет (в документациях не указывается, но скорее всего он реализован на уровне ОС);
- stdout - вывод результата. Этот результат может быть направлен на экран консоли, в файл, в другую команду и т.д. В powershell этот поток называется "Success Stream";
- stderr - вывод ошибок. В Powershell называется "Error Stream".
Пример работы этих потоков в обычном виде:
В 1-ом случае мы используем stdin т.к. напечатали команду с клавиатуры. После вызова команды мы получили stdout (2). При ошибках мы получаем stderr.
В конвейере работает такой же принцип:
Мы так же напечатали текст использовав stdin. Затем был использован конвейер, который направил результат первой команды ( stdout) в stdin другой команды (3). Так как команда 'echo' тоже использует stdout - он был выведен на экран.
Разница конвейеров bash и Powershell
Разница работы конвейеров в типе объектов, которые через них проходят. В Powershell, в подавляющем большинстве, возвращается (попадают в stdout) массивы следующего вида:
# массив ключ-значение
$data = [PSCustomObject]@{Name='WinRM'; status='disabled'}
# или массив массивов
$data = @(
@[PSCustomObject]@{Name='WinRM'; status='disabled'}
@[PSCustomObject]@{Name='VSS'; status='enabled'}
)
# обычные массивы
$data = @(1,2,3)
Благодаря этому мы можем сразу получать нужное свойство не использовав сложные конструкции и регулярные выражения:
Bash же отправляет в stdout информацию в виде строки:
data='
Name Status
WinRM disabled
VSS enabled
'
Так как это сплошной текст - нам приходится использовать дополнительные механизмы для поиска нужных данных. Например grep:
Есть и другие отличия. Например в Powershell есть переменные, в которые и помещаются данные конвейера: $PSItem или $_. Часть из таких возможностей будет рассмотрена далее.
Различия с циклом
Пайп схож с циклами, но более автоматизирован. Что бы с помощью цикла получить название дисков, а затем изменить цвет вывода, мы должны сделать следующее:
foreach ($item in Get-PSDrive){
Write-Host $item.Name -ForegroundColor Red
}
У нас использован совершенно обычный цикл, который есть во всех языках. Мы объявляем переменную "$item", в которую помещается временное значение. Это значение мы отдельно выводим. Т.е. мы выполняем 3 действия:
- Объявление цикла;
- Объявление переменной с текущим значением;
- Вывод переменной (или другие действия, например сохранение данных в файл).
В Powershell реализован так же команда-цикл "ForEach-Object". Особенность этой команды в том, что используется только часть возможности конвейера и часть от обычного цикла:
Get-PSDrive | ForEach-Object {
Write-Host $PSItem.Name -ForegroundColor Red
}
Как можно увидеть с примера выше у нас исчез шаг под номером 2. Мы больше не объявляем временную переменную - она формируется автоматически под именем "PSItem". Эта переменная появляется во время передачи данных через pipeline. В нее помещается результат работы "Get-PSDrive".
Конвейер убирает еще, как минимум, один шаг. Нам не нужно объявлять цикл - мы просто вызываем переменную:
Get-PSDrive | Write-Host $PSItem.Name -ForegroundColor Red
В некоторых случаях мы можем не указывать "PSItem" вовсе. Обычно это команды одного типа, например, работы с сервисом:
Get-Service -Name 'WinRM' | Restart-Service
'WinRM' | Restart-Service
Конвейер - отличный выбор когда вам нужно сделать что-то единожды. Если вы пишете скрипт, а тем более используете 'where-object' на больших данных, всегда лучше использовать циклы. Это будет работать быстрее.
Как принимаются данные с конвейера
Если мы выполним следующую команду, то увидим, что у нее есть множество свойств (левая колонка), которые мы можем вернуть:
Get-Service -Name 'WinRM' | Select *
Вопрос в том, как конвейер узнает, что нам нужно использовать свойство "Name" в "Restart-Service"? В одной команде множество свойств и значений, а другой множество параметров и они могут не совпадать. У нас так же не возвращаются какие-либо ошибки.
Что бы понять как будут распределены свойства и значения нам нужно посмотреть справку по этой команде:
Get-Help Restart-Service -Parameter *
Строка "Принимать входные данные конвейера?" (или "Accept pipeline input") говорит, может ли в этот параметр ('-Name') попадать значение через пайплайн. Кроме этого он указывает каким образом это может произойти:
- ByPropertyName - по свойству (должен быть использован массив ключ-значение);
- ByValue - по значению (одномерный массив).
Значение '<string[]>' говорит, что все данные будут преобразованы в массив из строк. Если какой-то объект не может быть преобразован - будет ошибка. Более наглядный пример был бы со значением '<int[]>' т.к. в этом случае вы бы не смогли использовать строки.
Не все параметры у команд так очевидны. У некоторых команд можно увидеть работу через конвейер с 'InputObject'. Такой параметр подразумевает более сложную обработку поступающих данных. Процесс привязки параметров так же называется 'parameter binding'.
Простые примеры, где использовался принцип 'ByValue', мы уже рассматривали. Он выглядит так:
$names = @('WinRM','VSS')
$names | Restart-Service
# или
'WinRM','VSS' | Restart-Service
Работа 'ByPropertyName' - подразумевает, что вы используете массив ключ-значение. Например PSCustomObject и в нем есть ключ (в нашем случае ключ 'Name') с аналогичным названием:
$hash = [PSCustomObject]@{
'Name'='WinRM';
'State'='Restarted';
}
$hash | Get-Service
Не все команды возвращают PSCustomObject. Это говорит о том, что вы можете использовать и другие типы объектов:
Главный принцип, по которому значение может таким образом успешно попасть в конвейер, это быть итерируемым. В hashtable такого по умолчанию нет.
hashtable
Не все объекты Powershell могут проходить через конвейер. Как написано в документации у всех типов Powershell, кроме hashtable, есть поддержка работы через конвейеры (IEnumerable). При попытке пропустить хэш-таблицу через конвейер возникнет ошибка:
- Get-Service : Не удается найти службу с именем службы "System.Collections.Hashtable".
$hash = @{
'Name'='WinRM';
'State'='Restarted';
}
$hash | Get-Service
Исправить ее можно несколькими способами. Самый простой - использовать метод 'GetEnumerator()'. Благодаря этому методу хэш-таблица становится итерируемой и вы сможете использовать индексы и ключи в следующем виде:
$hash = @{
'Name'='WinRM';
'State'='Restarted';
}
$hash.GetEnumerator() | Get-Service -Name {$PSItem.Value} -ErrorAction SilentlyContinue
Параметр "ErrorAction" нужен т.к. у нас произойдет ошибка из-за ключа 'State'. Значения хэш таблицы передаются не как целый массив (как в случае с PSCustomObject), а по отдельности. Сначала передается ключ 'Name' со значением 'WinRM', а затем 'State' со значением 'Restarted'.
Еще два способа получить только ключи или только значения:
$hash = @{
'Name'='WinRM';
'State'='Restarted';
}
$hash.Values | Get-Service -ErrorAction SilentlyContinue
# вывод только ключей (нет смысла применять в этом сценарии)
$hash.Keys
Создание функции работающей через Pipeline
Еще один способ понять работу конвейера - это создать собственную команду (функцию). Мы можем объявить два параметра, которые могут быть использованы вместе, так и отдельно:
- ValueFromPipeline - подразумевает, что вы передаете обычные массивы (например '1','2','3'). Этот параметр может быть установлен только один раз на функцию;
- ValueFromPipelineByPropertyName - принимает массив ключ-значение и ищет совпадающее свойство (ключ). Этот параметр можно использовать множество раз.
Эти параметры мы можем совмещать, так и указывать отдельно:
function Test-Command {
[CmdletBinding()]
param
(
[Parameter(ValueFromPipeline)]
$data
)
$data
}
Test-Command -data 'Hello there'
'Hello there' | Test-Command
В отличие от предыдущего способа, ValueFromPipelineByPropertyName мы можем использовать множество раз. Самое главное использовать правильные названия переменных и ключей:
function Test-Command {
[CmdletBinding()]
param
(
[Parameter(ValueFromPipelineByPropertyName)]
$name,
[Parameter(ValueFromPipelineByPropertyName)]
$age
)
Write-Host "Пользователю $name $age лет"
}
Test-Command -name 'Masha' -age 39
[PSCustomObject]@{name='Dimitry'; age=18} | Test-Command
Мы можем комбинировать параметры ValueFromPipelineByPropertyName и ValueFromPipeline. Я бы рекомендовал избегать таких способов т.к. в них легко запутаться:
function Get-Srv {
[CmdletBinding()]
param
(
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]$Name,
[Parameter(ValueFromPipelineByPropertyName)]
[string]$ip
)
Write-Host "Сервер $Name работает $ip"
}
Get-Srv -Name 'SRV01'
'SRV01' | Get-Srv
[PSCustomObject]@{Name='SRV02';ip='1.0.0.1'} | Get-Srv
'SRV01', 'SRV02' | Get-Srv
Обратите внимание на помеченные фрагменты. Мы должны определять тип данных, который ожидается (случай 1 и 2) иначе конвейер не будет обрабатывать некоторые данные корректно. Так же, в случае 3, у нас выводится только последний элемент. Это происходит из-за отсутствия Process.
Блок Process
При создании функций в Powershell вы можете использовать блоки: begin, process и end. В большинстве случаев это не требуется и вы можете писать функцию по принципам описанным выше. В случае конвейеров эти блоки приобретают дополнительный смысл:
- begin - значение, переданное через конвейер, этому блоку не известно. Оно будет равно $null либо другому значению по умолчанию, которое вы установите;
- process - обрабатывает каждый элемент переданные через конвейер отдельно;
- end - обрабатывает только последний элемент переданный через конвейер. Если вы не указывали какие-то блоки, как в примере выше, по умолчанию используется именно этот блок. Из-за этого выводится только последний элемент массива.
Пример работы всех блоков:
function Test-Block {
[CmdletBinding()]
param
(
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[int]$param
)
begin {
# Повторится 1 раз.
Write-Host "Блок Begin. Передано значение $param" -ForegroundColor DarkBlue
}
process {
# Повторится для каждого элемента.
Write-Host "Блок Process. Передано значение $param" -ForegroundColor DarkBlue
}
end {
# Повторится 1 раз.
Write-Host "Блок End. Передано значение $param" -ForegroundColor DarkBlue
}
}
Test-Block -param 1
1, 2, 3 | Test-Block
[PSCustomObject]@{param=1} | Test-Block
Видно, что передача параметров без использования пайплайна вывела '1' во всех блоках.
Там, где использовался конвейер, появился 0. Это связано с тем, что блоку begin не известна эта переменна, т.е. это значение равно $Null. Т.к. эта переменная должна хранить число, а не строку, этот тип преобразовывается в 0:
Write-Host ([int]$sssddasda)
Если для вас это слишком сложная конструкция, то вы можете использовать только блок Process. В случае обычных использований команды он вызывается единожды, а в случае конвейера - для каждого значения отдельно. Важно отметить, что вы не должны писать код вне Process:
function Test-Block {
[CmdletBinding()]
param
(
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[int]$param
)
process {
# Повторится для каждого элемента.
Write-Host "Блок Process. Передано значение $param" -ForegroundColor DarkBlue
}
}
Test-Block -param 1
1, 2, 3 | Test-Block
[PSCustomObject]@{param=1} | Test-Block
Массивы
Вы наверняка использовали команды, которые позволяют использовать следующую конструкцию:
Invoke-Command -ComputerName 1,2,3
1,2,3 | Invoke-Command
$obj = [PSCustomObject]@{ComputerName=1}
$obj | Invoke-Command
Используя примеры выше у вас не получится организовать такую возможность. У вас появится ошибка в первом случае т.к. ожидается число, а вы передаете массив:
- Не удается обработать преобразование аргументов для параметра "param". Не удается преобразовать значение.
Вы можете это исправить объявив в функции, что вы принимаете массив строк и реализовав цикл:
function Test-Array {
[CmdletBinding()]
param
(
[Parameter(ValueFromPipeline)]
# создаем массив строк используя []
[string[]]$Name
)
process {
# проходим по массиву
foreach ($item in $Name){
"Подключаемся к компьютеру $item"
}
}
}
Test-Array -Name 1,2,3
1,2,3 | Test-Array
Если вам нужно передавать именованные параметры, то нужно будет организовывать дополнительные проверки. Я не рекомендую использовать такие сложные конструкции.
Асинхронное выполнение
Конвейер Powershell передает значения асинхронно. Это значит, что несколько функций будут работать друг с другом еще до завершения своей работы. Это так же может быть важно в отдельных случаях.
В примере ниже видно, что мы передаем значения из массива постепенно, а не целиком. У нас чередуется выполнение функций (a,b,a...):
function a {
begin {
Write-Host 'Начало функции A'
}
process {
Write-Host "Блок Progress функции A: $_"; $_
}
end {
Write-Host 'Конец блока A'
}
}
function b {
begin {
Write-Host 'Начало функции B'
}
process {
Write-Host "Блок Progress функции B: $_"; $_
}
end {
Write-Host 'Конец функции B'
}
}
function c {
Write-Host 'Функция C'
}
1..3 | a | b | c
Ошибки
При создании конвейеров легко запутаться и получить странную ошибку. Часть из них рассмотрена ниже.
Если вы будете передавать данные с 'ValueFromPipelineByPropertyName', при этом ни один из ключей не совпадает, у вас появятся следующие ошибки (они выводятся и при других обстоятельствах тоже):
- Не удается привязать объект ввода к любым параметрам команды, так как команда не принимает входные данные конвейера, либо входные данные и их свойства не совпадают с любыми из параметров, принимающих входные данные конвейера;
- The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
Если вы укажете код вне блока Process, то так же получите странную ошибку:
- Get-Process : Не удается вычислить параметр "Name", так как его аргумент задан как блок сценария, а входные данные отсутствуют. Блок сценария не может быть вычислен без входных данных.
- Get-Process : Cannot evaluate parameter 'Name' because its argument is specified as a script block and there is no input. A script block cannot be evaluated without input
Другие проблемы можно распознать использовав Trace-Command.
...
Подписывайтесь на наш Telegram канал
Теги: #powershell