Разбираем работу конвейера Powershell на примере команд и созданию функций


24 июня 2021


Как работает конвейер в Powershell и что такое pipeline на примерах

Ключевой особенностью Powershell, которая отличает его от других языков, это работа с конвейером. Дело в том, что каждая команда в Powershell возвращает множество объектов, а не один объект типа строка. Такой метод дает дополнительные возможности для работы с языком. Конвейер так же называют pipe или pipeline. В это статье будут рассмотрены примеры работы конвейера, его сравнение с bash, отличие от циклов и создание функции принимающей данные с конвейера.

 

Как работает конвейер

Конвейер (pipline, pipe) - это возможность, которая позволяет удобно обменяться данными между командами. В большинстве языков пайплайн определяется символом  '|'. Значения, которые указаны в левой части, передаются на чтения справа.

Если бы мы использовали конвейер с буквами, то получилось бы следующее:

'а' | 'б' | 'в' = 'абв'

В примере ниже слева указана строка, которая выводится через команду справа:

'привет' | Write-Output

Предыдущий пример идентичен, по функциональности и результату, следующему выполнению:

Write-Output 'привет'

Работа конвейера и обычно команды с параметрами Powershell

Суть в том, что мы можем указать несколько значений в левой части. Каждое из этих значений будет передаваться асинхронно (т.е. отдельно). Значения, которые разделяются запятыми в Powershell, образуют массив:

'привет', 'пока' | Write-Output

В командах без конвейера мы редко можем использовать массивы в качестве значений. Что бы получить результат аналогичный предыдущей команде мы должны вызвать ее дважды:

Write-Output 'привет'
Write-Output 'пока' 

Передача массивов через конвейер Powershell

Учитывая, что 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".

Пример работы этих потоков в обычном виде:

Работа stdin и stdout в Powershell

В 1-ом случае мы используем stdin т.к. напечатали команду с клавиатуры. После вызова команды мы получили stdout (2). При ошибках мы получаем stderr.

В конвейере работает такой же принцип:

Работа stdin и stdout в конвейере Powershell

Мы так же напечатали текст использовав 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)

Благодаря этому мы можем сразу получать нужное свойство не использовав сложные конструкции и регулярные выражения:

Передача массивов ключ-значение через конвейер Powershell

Bash же отправляет в stdout информацию в виде строки:

data='
Name   Status
WinRM  disabled
VSS    enabled
'

Так как это сплошной текст - нам приходится использовать дополнительные механизмы для поиска нужных данных. Например grep:

Сравнение конвейера Powershell и Bash

Есть и другие отличия. Например в Powershell есть переменные, в которые и помещаются данные конвейера: $PSItem или $_. Часть из таких возможностей будет рассмотрена далее.

 

 

Различия с циклом

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

foreach ($item in Get-PSDrive){
  Write-Host $item.Name -ForegroundColor Red
}

Сравнение цикла и пайплайна в Powershell

У нас использован совершенно обычный цикл, который есть во всех языках. Мы объявляем переменную "$item", в которую помещается временное значение. Это значение мы отдельно выводим. Т.е. мы выполняем 3 действия:

  1. Объявление цикла;
  2. Объявление переменной с текущим значением;
  3. Вывод переменной (или другие действия, например сохранение данных в файл).

В Powershell реализован так же команда-цикл "ForEach-Object". Особенность этой команды в том, что используется только часть возможности конвейера и часть от обычного цикла:

Get-PSDrive | ForEach-Object {
  Write-Host $PSItem.Name -ForegroundColor Red
}

Сравнение ForEach-Object и пайплайна в Powershell

Как можно увидеть с примера выше у нас исчез шаг под номером 2. Мы больше не объявляем временную переменную - она формируется автоматически под именем "PSItem". Эта переменная появляется во время передачи данных через pipeline. В нее помещается результат работы "Get-PSDrive".

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

Get-PSDrive | Write-Host $PSItem.Name -ForegroundColor Red

Используем PSItem в конвейере Powershell

В некоторых случаях мы можем не указывать "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 *

Какой параметр используется конвейером Powershell

Строка "Принимать входные данные конвейера?" (или "Accept pipeline input") говорит, может ли в этот параметр ('-Name') попадать значение через пайплайн.  Кроме этого он указывает каким образом это может произойти:

  • ByPropertyName - по свойству (должен быть использован массив ключ-значение);
  • ByValue - по значению (одномерный массив).

Значение '<string[]>' говорит, что все данные будут преобразованы в массив из строк. Если какой-то объект не может быть преобразован - будет ошибка. Более наглядный пример был бы со значением '<int[]>' т.к. в этом случае вы бы не смогли использовать строки.

Не все параметры у команд так очевидны. У некоторых команд можно увидеть работу через конвейер с 'InputObject'. Такой параметр подразумевает более сложную обработку поступающих данных. Процесс привязки параметров так же называется 'parameter binding'.

Простые примеры, где использовался принцип 'ByValue', мы уже рассматривали. Он выглядит так:

$names = @('WinRM','VSS')
$names | Restart-Service
# или
'WinRM','VSS' | Restart-Service

Как передаются массивы через конвейер Powershell

Работа 'ByPropertyName' - подразумевает, что вы используете массив ключ-значение. Например PSCustomObject и в нем есть ключ (в нашем случае ключ 'Name') с аналогичным названием:

$hash = [PSCustomObject]@{
  'Name'='WinRM';
  'State'='Restarted';
}
$hash | Get-Service

Как передаются массивы ключ-значение через конвейер Powershell

Не все команды возвращают PSCustomObject. Это говорит о том, что вы можете использовать и другие типы объектов:

Типы массивов ключ-значение в Powershell

Главный принцип, по которому значение может таким образом успешно попасть в конвейер, это быть итерируемым. В hashtable такого по умолчанию нет.

hashtable

Не все объекты Powershell могут проходить через конвейер. Как написано в документации у всех типов Powershell, кроме hashtable, есть поддержка работы через конвейеры (IEnumerable). При попытке пропустить хэш-таблицу через конвейер возникнет ошибка:

  • Get-Service : Не удается найти службу с именем службы "System.Collections.Hashtable".
$hash = @{
  'Name'='WinRM';
  'State'='Restarted';
}
$hash | Get-Service

Передача hastable через конвейер Powershell

Исправить ее можно несколькими способами. Самый простой - использовать метод 'GetEnumerator()'. Благодаря этому методу хэш-таблица становится итерируемой и вы сможете использовать индексы и ключи в следующем виде:

$hash = @{
  'Name'='WinRM';
  'State'='Restarted';
}
$hash.GetEnumerator() | Get-Service -Name {$PSItem.Value} -ErrorAction SilentlyContinue

Передача hastable через конвейер Powershell с GetEnumerator

Параметр "ErrorAction" нужен т.к. у нас произойдет ошибка из-за ключа 'State'. Значения хэш таблицы передаются не как целый массив (как в случае с PSCustomObject), а по отдельности. Сначала передается ключ 'Name' со значением 'WinRM', а затем 'State' со значением 'Restarted'.

Еще два способа получить только ключи или только значения:

$hash = @{
  'Name'='WinRM';
  'State'='Restarted';
}
$hash.Values | Get-Service -ErrorAction SilentlyContinue
# вывод только ключей (нет смысла применять в этом сценарии)
$hash.Keys

Другие возможности получения значений из хэш-таблиц Powershell

 

Создание функции работающей через Pipeline

Еще один способ понять работу конвейера - это создать собственную команду (функцию). Мы можем объявить два параметра, которые могут быть использованы вместе, так и отдельно:

  • ValueFromPipeline - подразумевает, что вы передаете обычные массивы (например '1','2','3'). Этот параметр может быть установлен только один раз на функцию;
  • ValueFromPipelineByPropertyName - принимает массив ключ-значение и ищет совпадающее свойство (ключ). Этот параметр можно использовать множество раз.

Эти параметры мы можем совмещать, так и указывать отдельно:

function Test-Command {
    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline)]
        $data
    )
    $data
}

Test-Command -data 'Hello there'
'Hello there' | Test-Command

Создание функции принимающей массив через конвейер Powershell

В отличие от предыдущего способа, 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

Создание функции принимающей массив ключ-значение через конвейер Powershell

Мы можем комбинировать параметры 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

Совмещение параметров работающие с конвейером Powershell

Обратите внимание на помеченные фрагменты. Мы должны определять тип данных, который ожидается (случай 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

Создание команды Powershell с begin end и process

Видно, что передача параметров без использования пайплайна вывела '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

Оптимизация создание функции с блоком process Powershell

Массивы

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

Invoke-Command -ComputerName 1,2,3
1,2,3 | Invoke-Command
$obj = [PSCustomObject]@{ComputerName=1}
$obj | Invoke-Command

Используя примеры выше у вас не получится организовать такую возможность. У вас появится ошибка в первом случае т.к. ожидается число, а вы передаете массив:

Создание функции Powershell принимающие массив через конвейер

  • Не удается обработать преобразование аргументов для параметра "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 принимающие массив через конвейер

Если вам нужно передавать именованные параметры, то нужно будет организовывать дополнительные проверки. Я не рекомендую использовать такие сложные конструкции.

Асинхронное выполнение

Конвейер 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

Асинхронное выполнение конвейера в Powershell

Ошибки

При создании конвейеров легко запутаться и получить странную ошибку. Часть из них рассмотрена ниже.

Если вы будете передавать данные с '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.

Ошибки при работе с конвейером Powershell

Если вы укажете код вне блока 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

Ошибки при работе с конвейером Powershell

Другие проблемы можно распознать использовав Trace-Command.

...

Теги: #powershell


Каналы
Telegram FixMyPc Telegram Лента FixMyPC RSS Rss
Популярные тэги
О блоге
Этот блог представляет собой конспекты выученного материала, преобретенного опыта и лучшие практики в системном администрировании и программировании.