Знакомство с конвейером PowerShell и создание функций

Система: Выполнение команд в PowerShell представляет собой одну из самых важных (и полезных) особенностей оболочки и языка сценариев PowerShell. После того как вы поймете основы ее работы и ее возможности, вы сможете использовать ее мощь в своих собственных функциях. В этом уроке вы собираетесь сделать именно это!

Конвейер PowerShell позволяет объединять команды в единый “конвейер”, что упрощает код, обеспечивает параллельную обработку и многое другое. Если вы готовы узнать о конвейере и создать свои собственные функции для использования конвейера, давайте начнем!

Предварительные требования

Этот пост будет учебным пособием и содержит практические демонстрации. Если вы хотите следовать за уроком, вам понадобится PowerShell версии 3 и выше. В этом уроке будет использоваться Windows PowerShell v5.1.

Понимание конвейера PowerShell

Большинство команд PowerShell принимают входные данные через параметры. Команда получает объект в качестве входных данных и выполняет некоторые действия с ним. Затем по желанию возвращает какой-то объект через вывод.

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

Например, у cmdlet Stop-Service есть параметр, называемый InputObject. Этот параметр позволяет передать конкретный тип объекта в Stop-Service, представляющий службу Windows, которую вы хотели бы остановить.

Чтобы использовать параметр InputObject, вы можете получить объект службы с помощью Get-Service, а затем передать объект параметру InputObject, как показано ниже. Этот способ предоставления ввода в cmdlet Stop-Service через параметр InputObject отлично работает и выполняет свою задачу.

$service = Get-Service -Name 'wuauserv'
Stop-Service -InputObject $service

Этот способ передачи ввода в команду Stop-Service требует выполнения двух отдельных шагов. PowerShell должен сначала выполнить Get-Service, сохранить вывод в переменную, а затем передать это значение в Stop-Service через параметр InputObject.

Теперь сократите приведенный выше фрагмент с помощью нижеприведенного фрагмента, который выполняет ту же задачу. Это намного проще, потому что вам не нужно создавать переменную $services или даже использовать параметр InputObject вообще. Вместо этого PowerShell “знает”, что вы собираетесь использовать параметр InputObject. Он делает это с помощью концепции, называемой привязкой параметров.

Теперь вы “сцепили” команды вместе с оператором |. Вы создали канал.

Get-Service -Name 'wuauserv' | Stop-Service

Но вы не обязаны использовать только две команды для создания канала; вы можете соединить столько, сколько захотите (если параметры команд поддерживают это). Например, в следующем фрагменте кода:

  1. Передает все объекты, которые возвращает командлет Get-Service, в командлет Where-Object.
  2. Затем командлет Where-Object анализирует свойство Status каждого объекта и возвращает только те объекты, у которых значение Running.
  3. Затем каждый из этих объектов отправляется в Select-Object, который возвращает только свойства Name и DisplayName объектов.
  4. Поскольку ни один другой командлет не принимает объекты, которые выводит Select-Object, команда возвращает объекты напрямую на консоль.

Командлеты Where-Object и Select-Object понимают, как обрабатывать входные данные конвейера посредством концепции, называемой привязкой параметров, о которой будет рассказано в следующем разделе.

Get-Service | Where-Object Status -eq Running | Select-Object Name, DisplayName

Дополнительную информацию о конвейерах можно получить, выполнив команду Get-Help about_pipelines.

Привязка параметров конвейера

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

Задача определения того, какой параметр использовать, когда команда получает входные данные через конвейер, называется привязкой параметра. Для успешной привязки объекта, поступающего из конвейера, к параметру параметр(ы) входящей команды должен поддерживать это. Параметры команды поддерживают привязку параметра конвейера одним из двух способов; \texttt{ByValue} и/или \texttt{ByPropertyName}.

\texttt{ByValue}

Параметр команды принимает весь поступающий объект в качестве значения параметра. Параметр \texttt{ByValue} ищет объект определенного типа среди поступающих объектов. Если этот тип объекта совпадает, PowerShell предполагает, что объект предназначен для привязки к этому параметру и принимает его.

\texttt{Get-ChildItem}cmdlet имеет параметр, называемый \texttt{Path}, который принимает строковый объект и ввод через конвейер по \texttt{ByValue}. Из-за этого выполнение чего-то вроде \texttt{‘C:\Windows’ | Get-ChildItem} возвращает все файлы в каталоге C:\Windows, потому что \texttt{C:\Windows} является строкой.

\texttt{ByPropertyName}

Параметр команды не принимает весь объект, а только одно свойство этого объекта. Он делает это не, глядя на тип объекта, а на имя свойства.

Команда Get-Process имеет параметр Name, который настроен для принятия ввода через конвейер ByPropertyName. Когда вы передаете объект с свойством Name команде Get-Process, например, так: [pscustomobject]@{Name='firefox'} | Get-Process, PowerShell связывает свойство Name во входящем объекте с параметром Name и использует это значение.

Обнаружение параметров команд, поддерживающих конвейер

Как уже упоминалось ранее, не каждая команда поддерживает ввод через конвейер. Автор команды должен создать такую ​​функциональность в процессе разработки. Команда должна иметь по меньшей мере один параметр, который поддерживает конвейер, указанный как ByValue или ByPropertyName.

Как узнать, какие команды и их параметры поддерживают ввод через конвейер? Можно просто попробовать это методом проб и ошибок, но есть лучший способ с использованием системы помощи PowerShell с помощью команды Get-Help.

Get-Help <COMMAND> -Parameter <PARAMETER>

Например, рассмотрим параметр команды Get-ChildItem под названием Path ниже. Вы можете видеть, что он поддерживает оба типа ввода через конвейер.

PowerShell pipeline input is allowed

Как только вы узнаете, какие параметры команды поддерживают ввод через конвейер, вы сможете воспользоваться этой функциональностью, как показано ниже.

# вызов без конвейера
Get-ChildItem -Path 'C:\\Program Files', 'C:\\Windows'

# вызов через конвейер
'C:\\Program Files', 'C:\\Windows' | Get-ChildItem

Но, с другой стороны, параметр DisplayName в команде Get-Service не поддерживает ввод через конвейер.

PowerShell pipeline input is not allowed

Создание своей собственной функции конвейера

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

Давайте, например, рассмотрим существующую функцию с именем Get-ConnectionStatus.

  • У этой функции есть один параметр (не принимающий ввод через конвейер) с именем ComputerName, который позволяет передавать ей одну или несколько строк. Вы можете определить, что параметр ComputerName не принимает ввод через конвейер, потому что он не определен как атрибут параметра ([Parameter()]).
  • Затем функция считывает каждую из этих строк и запускает команду Test-Connection для каждой из них.
  • Для каждой переданной строки имени компьютера затем возвращается объект с свойствами ComputerName и Status.
function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter()] ## нет ввода через конвейер
        [string[]]$ComputerName
    )

    foreach($c in $ComputerName)
    {
        if(Test-Connection -ComputerName $c -Quiet -Count 1)
        {
            $status = 'Ok'
        }
        else
        {
            $status = 'No Connection'
        }

        [pscustomobject]@{
            ComputerName = $c
            Status = $status
        }
    }
}

Затем вы вызываете функцию Get-ConnectionStatus, передавая параметр ComputerName одно или несколько имен хостов или IP-адресов, как показано ниже.

Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'

Шаг 1: Разрешение ввода через конвейер

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

Например, добавьте атрибут параметра ValueFromPipeline в скобках определения [Parameter()], как показано ниже.

 [Parameter(ValueFromPipeline)]
 [string[]]$ComputerName

На этом этапе это технически все, что вам нужно сделать. Функция Get-ConnectionStatus теперь будет привязывать любой переданный ей объект строки к параметру ComputerName. Но, даже если привязка параметров происходит, это не означает, что функция будет выполнять что-то значимое с ними.

Шаг 2: Добавление блока Process

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

Без блока Process PowerShell будет обрабатывать только первый объект, поступающий из конвейера. Блок Process указывает PowerShell продолжать обработку объектов.

function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline)]
        [string[]]$ComputerName
    )

    Process ## Новый блок Process
    {
        foreach($c in $ComputerName)
        {
            if(Test-Connection -ComputerName $c -Quiet -Count 1)
            {
                $status = 'Ok'
            }
            else
            {
                $status = 'No Connection'
            }

            [pscustomobject]@{
                ComputerName = $c
                Status = $status
            }
        }
    } ## Конец блока Process
}

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

Передача объектов в конвейер PowerShell

После определения блока Process в функции вы можете вызвать функцию, передавая значения параметра ComputerName через конвейер, как показано ниже.

Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'
## или
'127.0.0.1', '192.168.1.100' | Get-ConnectionStatus

На этом этапе вы можете использовать настоящую мощь конвейера и начать включать в него больше команд. Например, возможно, у вас есть текстовый файл, C:\Test\computers.txt, с строкой IP-адресов, разделенных новой строкой, как показано ниже.

127.0.0.1
192.168.1.100

Затем вы можете использовать cmdlet Get-Content, чтобы прочитать каждый из этих IP-адресов в текстовом файле и передать их непосредственно в функцию Get-ConnectionStatus.

Get-Content -Path C:\Test\computers.txt | Get-ConnectionStatus 

Поднимая эту настройку на шаг дальше, вы можете направлять объекты, которые возвращает Get-ConnectionStatus, непосредственно в cmdlet ForEach-Object.

Код ниже:

  • чтение всех имен компьютеров в текстовом файле и передача их функции Get-ConnectionStatus.
  • Get-ConnectionStatus обрабатывает каждое имя компьютера и возвращает объект с свойствами ComputerName и Status.
  • Get-ConnectionStatus затем передает каждый объект в командлет ForEach-Object, который возвращает единственную строку, окрашенную в цвет циан с читаемым человеком статусом.
Get-Content -Path C:\Test\computers.txt |
Get-ConnectionStatus |
ForEach-Object { Write-Host "$($_.ComputerName) connection status is: $($_.Status)" -ForegroundColor Cyan }

Если передача по конвейеру не была включена для параметра ComputerName или если Get-ConnectionStatus не возвратил объект в блоке Process, PowerShell не вернет статус в консоль до тех пор, пока все объекты (IP-адреса) не будут обработаны.

Привязка конвейера по имени свойства

До сих пор командлет Get-ConnectionStatus настроен принимать ввод конвейера по значению (ValueFromPipeline), принимая массив строк, например '127.0.0.1', '192.168.1.100'. Будет ли эта функция работать ожидаемым образом, если ввод будет получен из CSV-файла вместо текстового файла с IP-адресами?

Возможно, у вас есть файл CSV, который выглядит так, как показано ниже по пути C:\Test\pc-list.csv.

ComputerName,Location
127.0.0.1,London
192.168.1.100,Paris

Обратите внимание, что поле ComputerName в файле CSV имеет то же имя, что и параметр ComputerName Get-ConnnectionStatus.

Если вы попытаетесь импортировать CSV и передать его по конвейеру в Get-ConnectionStatus, функция вернет неожиданный результат в столбце ComputerName.

Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus

ComputerName                                  Status       
------------                                  ------       
@{ComputerName=127.0.0.1; Location=London}    No Connection
@{ComputerName=192.168.1.100; Location=Paris} No Connection

Можете ли вы догадаться, что пошло не так? Ведь имя параметра совпадает, почему тогда конвейер PowerShell не привязал вывод, который возвращает Import-CSV, к параметру ComputerName на Get-ConnectionStatus? Потому что вам нужен атрибут параметра ValueFromPipelineByPropertyName.

На данный момент параметр ComputerName функции имеет определение параметра, которое выглядит как [Parameter(ValueFromPipeline)]. Поэтому вы должны добавить ValueFromPipelineByPropertyName, чтобы установить параметр ComputerName для поддержки ввода ByPropertyName, как показано ниже.

    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

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

PS> Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus

ComputerName  Status       
------------  ------       
127.0.0.1     Ok           
192.168.1.100 No Connection

Резюме

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

Несмотря на то, что функции будут работать без конвейера, они не будут «потоком» объектов от одной команды к другой и упростят код.

Можете ли вы вспомнить о функции, которую вы написали, или, быть может, собираетесь написать, которая может оказаться полезной после внесения изменений для поддержки конвейера?

Source:
https://adamtheautomator.com/ppowershell-pipeline/