Конвейерная обработка данных присутствует во многих командных интерпретаторах, однако PowerShell выгодно отличает от прочих то, что между составными частями конвейера, то есть командами и функциями, передается не текст, который представляет собой результат выполнения предыдущей команды, а объекты. Таким образом, когда мы вводим команду:

Get-Service -Name Spooler | Restart-Service

команда Restart-Service получает в качестве входных данных не три строки текста, как на рисунке 1, а объект типа System.ServiceProcess.ServiceController.

 

Результат выполнения команды Get-Service -Name Spooler
Рисунок 1. Результат выполнения команды Get-Service -Name Spooler

Что это нам дает? Возможность использовать результаты выполнения команд (объекты) по своему усмотрению: сортировать и группировать на основе значений свойств, вызывать методы, передавать в другие команды или собственноручно написанные функции и даже определять, как они должны выглядеть при вводе команд вида Format-*.

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

Получение данных по конвейеру

Хотя выше уже упоминалось, что результатом выполнения команды Get-Service будет объект System.ServiceProcess.ServiceController, стоит уточнить, как мы получили эту информацию, поскольку знание типа возвращаемого командой объекта поможет нам определить, на взаимодействие с какими командами и параметрами мы можем рассчитывать.

Для получения типа объекта, а также списка его свойств и методов мы можем задействовать команду Get-Member. Например, так:

Get-Service -Name Spooler | Get-Member

В самом начале вывода мы увидим строку:

TypeName: System.ServiceProcess.ServiceController

Это и есть тип возвращаемого командой Get-Service объекта.

Теперь нам нужно узнать, какие команды и параметры поддерживают получение данного типа объектов по конвейеру. Для получения списка команд, принимающих объекты типа System.ServiceProcess.ServiceController в качестве значений параметров, пусть и безотносительно возможности получения этими параметрами данных непосредственно по конвейеру, воспользуемся командой:

Get-Command -ParameterType
   System.ServiceProcess.ServiceController

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

 

Результаты работы команды Get-Command
Рисунок 2. Результаты работы команды Get-Command

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

Для того чтобы определить, какие именно параметры принимают объекты служб в виде значений и, что не менее важно, поддерживают ли они получение этих объектов по конвейеру, нам потребуется команда Get-Help. Так как из всего содержимого файла справки для команды Restart-Service нам нужны только сведения о параметрах, мы слегка ограничим ее вывод:

Get-Help -Name Restart-Service
   -Parameter *

Среди всех свойств параметров нас больше всего интересует ‘Accept pipeline input?’. Его значениями могут быть True либо False. Значение True сообщает нам о том, что данный параметр поддерживает получение значений по конвейеру. В скобках после True указывается, каким именно способом он может это делать.

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

ByPropertyName говорит о том, что значением данного параметра будет значение одноименного свойства получаемого по конвейеру объекта.

Возвращаясь к команде Restart-Service, мы видим, что параметров, поддерживающих получение данных по конвейеру, у нее два — InputObject и Name (рисунок 3).

 

Параметры команды Restart-Service
Рисунок 3. Параметры команды Restart-Service

InputObject поддерживает только вариант с передачей всего объекта — ByValue, и, как мы видим, это может быть только объект (или объекты) типа ServiceController.

Параметр Name принимает объект целиком (и в данном случае это должна быть строка — String) или же, если по конвейеру поступает объект иного типа, аргументом для данного параметра будет значение свойства Name входящего объекта.

Однако, присмотревшись к объекту System.ServiceProcess.Service Controller (рисунок 4), мы увидим, что он тоже содержит свойство Name. В связи с этим возникает вопрос: значением какого параметра, InputObject или Name, становятся получаемые по конвейеру данные?

 

Свойства объекта System.ServiceProcess.ServiceController
Рисунок 4. Свойства объекта System.ServiceProcess.ServiceController

Вспомним о таком понятии, как наборы параметров — Parameter Sets. Их существование обусловлено возможностью присутствия в одной команде несовместимых друг с другом параметров.

Например, мы можем получить объект процесса, указав его имя — параметр Name, или идентификатор — параметр Id. Указывать оба параметра в одной команде бессмысленно, поэтому они принадлежат разным наборам параметров, Name и Id соответственно. В данном случае имена наборов повторяют имена входящих в них несовместимых параметров, однако это не является обязательным.

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

И стоит сказать, что наша команда

Get-Service -Name Spooler | Restart-Service

это тот самый случай. Теперь, чтобы узнать, значением какого параметра станет поступающий по конвейеру объект службы, нам нужно определить набор параметров по умолчанию. Для этого нам понадобится команда Get-Command с параметром -Syntax:

Get-Command Restart-Service -Syntax

Выведенные в качестве результата три строки — это три набора параметров команды Restart-Service. И первый из них — набор параметров по умолчанию. Таким образом, мы теперь знаем, что поступающий по конвейеру объект ServiceController становится значением параметра InputObject.

Стоит сказать, что вывод команды Get-Help, например

Get-Help Restart-Service

тоже содержит наборы параметров указанной команды, однако в данном случае нет никакой гарантии в том, что первым указан именно набор параметров по умолчанию. Соответственно, для этих целей лучше задействовать команду Get-Command.

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

Get-Command Restart-Service |
   Select-Object -ExpandProperty
      ParameterSets | Select-Object -Property *

К слову, упомянутые выше имена наборов параметров команды Get-Process — Name и Id — мы получили именно таким образом.

Итак, при передаче объекта службы команде Restart-Service используется параметр -InputObject. Для чего же тогда потребовалось обеспечивать возможностью получения данных по конвейеру и параметр Name, да еще с использованием обоих вариантов, как ByValue, так и ByPropertyName? Для гибкости.

Тот факт, что параметр Name поддерживает прием данных ByValue, позволяет нам передать по конвейеру объект строки, который представляет собой имя службы или нескольких служб. Например:

'Audiosrv’, ‘Spooler’, ‘SysMain’ |
   Restart-Service

Вариант ByPropertyName позволяет нам создать пользовательский объект со свойством Name или же приспособить для этого объект другого типа. Например, мы можем получить сведения об определенных службах при помощи команды Get-CimInstance:

Get-CimInstance -ClassName
   Win32_Service -Filter "Name='Spooler'"

Результатом исполнения данной команды будет объект Microsoft.Manage­ment.Infrastructure.CimInstance, который к ожидаемому параметром InputObject команды Restart-Service объекту типа System.ServiceProcess.ServiceController не имеет никакого отношения. Тем не менее он содержит свойство Name, значением которого является имя службы. И поэтому мы вполне можем использовать следующую команду для выполнения перезагрузки необходимой нам службы:

Get-CimInstance -ClassName
   Win32_Service -Filter "Name='Spooler'"
   | Restart-Service

Что же касается пользовательского объекта, то это может быть любой объект, содержащий в качестве значения свойства Name имя службы. Например, такой как на рисунке 5.

 

Объект, содержащий в качестве значения свойства Name имя службы
Рисунок 5. Объект, содержащий в качестве значения свойства Name имя службы

Вычисляемые свойства

Если мы присмотримся к структуре возвращаемого командой Get-CimInstance объекта службы, то увидим, что он обладает свойством PathName, значением которого является путь к исполняемому файлу. Давайте предположим, что нам необходимо получить об этом файле как можно более подробную информацию.

Для этого нам потребуется команда Get-Item. Однако если мы попробуем передать по конвейеру полученный в результате выполнения команды Get-CimInstance объект команде Get-Item, то получим сообщение об ошибке, как на рисунке 6.

 

Сообщение об ошибке при выполнении команды Get-CimInstance
Рисунок 6. Сообщение об ошибке при выполнении команды Get-CimInstance

Почему так получилось? Если мы заглянем в файл справки команды Get-Item, то увидим, что параметров, способных принимать значения по конвейеру, у нее три: Path, LiteralPath и Credential. Причем оба варианта взаимодействия с конвейером, ByValue и ByPropertyName, поддерживает только параметр Path. Остальные — LiteralPath и Credential — поддерживают только ByPropertyName.

Попытка сопоставления параметрам поступающих по конвейеру данных начинается с варианта ByValue. Из файла справки мы знаем, что параметр Path в качестве значения принимает объект строки, однако по конвейеру поступает объект Microsoft.Management.Infrastructure.CimInstance.

Что же в подобном случае делает командная среда? Убедившись, что входящий объект не содержит таких свойств, как Path, LiteralPath или Credential, что могли бы пригодиться при сопоставлении его параметрам с использованием способа ByPropertyName, она пытается нам помочь и конвертирует поступивший по конвейеру объект в требуемый тип данных — System.String. Что из этого получается, мы можем увидеть на рисунке 7, выполнив следующую команду:

Get-CimInstance -ClassName
   Win32_Service -Filter "Name='Spooler'"
   | ForEach-Object -MemberName ToString

 

Результат работы Get-CimInstance
Рисунок 7. Результат работы Get-CimInstance

Как видите, это не совсем то, что мы хотели бы передать команде Get-Item в качестве значения параметра Path.

Путь к файлу представлен значением свойства PathName объекта CimInstance. Команда Get-Item для получения пути к нужному файлу использует параметр Path. Что нам нужно сделать, так это каким-либо образом передать свойство PathName под именем Path.

Вариантов здесь достаточно много: от добавления к объекту свойства с тем же значением (листинг 1) до создания уже упомянутого выше пользовательского объекта (листинг 2).

Однако более удобным в данном случае будет использование вычисляемых свойств — Calculated Properties.

Вычисляемые свойства — это метод взаимодействия с данными, используя который вы можете определять новые свойства объектов в процессе их прохождения по конвейеру. При этом вы можете задать нужное имя свойства, выражение, результат которого будет значением этого свойства, а также, при использовании команд Format-*, если потребуется, указать параметры форматирования полученного значения. Например, так, как показано в листинге 3, результат исполнения которого приведен на рисунке 8.

 

Результат исполнения листинга 3
Рисунок 8. Результат исполнения листинга 3

Что же касается нашего случая, то здесь потребуется команда Select-Object (листинг 4), результат исполнения которой показан на рисунке 9.

 

Результат исполнения листинга 4
Рисунок 9. Результат исполнения листинга 4

Если мы присмотримся к результату выполнения команды Select-Object, приведенной в листинге 5, то увидим, что он представляет собой объект с единственным свойством Path — тем, что мы определили в параметре Property (рисунок 10).

 

Результат исполнения листинга 5
Рисунок 10. Результат исполнения листинга 5

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

 

Результат исполнения листинга 6
Рисунок 11. Результат исполнения листинга 6

Блоки сценария

Еще одним вариантом использования значений свойств поступающего по конвейеру объекта в виде входных данных является применение блоков сценария в качестве значений параметров команды.

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

Get-CimInstance -ClassName
   Win32_Service -Filter "Name='Spooler'"
   | Get-Item -Path {$_.PathName}

Внутри блока сценария для ссылки на текущий объект конвейера мы используем переменную $_. Таким образом, мы указываем свойство PathName объекта Microsoft.Management.Infrastructure.CimInstance в качестве значения параметра Path.

Кроме того, начиная с третьей версии Windows PowerShell вместо переменной $_ можно использовать $PSItem, что никоим образом не сказывается на функциональности, однако может показаться более логичным с точки зрения наименования.

Тем не менее кажущаяся простота этого способа многих может ввести в заблуждение. Давайте рассмотрим следующий пример. Допустим, нам нужно запросить WMI-класс Win32_ComputerSystem с компьютеров Comp-1, Comp-2 и Comp-3. Предположим, что информацию об этих компьютерах мы решили получить из службы каталогов Active Directory. Мы можем воспользоваться командой, приведенной в листинге 7.

В этом случае все сработает наилучшим образом. Однако если мы, к примеру, решили запросить несколько классов WMI с локального компьютера, сохранили их имена в переменной и передали ее содержимое по конвейеру, как в листинге 8, то результатом будет сообщение об ошибке, показанное на рисунке 12. И так для каждого класса.

 

Ошибка при выполнении запроса листинга 8
Рисунок 12. Ошибка при выполнении запроса листинга 8

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

Что сейчас попыталась сделать команда Get-CimInstance? Если мы заглянем в файл справки, то увидим, что изо всех поддерживающих получение данных по конвейеру параметров метод ByValue используют только два из них — CimSession и InputObject.

Параметр InputObject работает с объектами типа Microsoft.Management.Infrastructure.CimInstance. Мы же передаем массив строк, так что в данном случае он не используется.

Второй параметр, CimSession, поддерживает только значения типа Microsoft.Management.Infrastructure.CimSession, и, казалось бы, он тоже не должен взаимодействовать с поступающими объектами, однако, как выясняется, строки замечательно преобразуются в объекты CimSession. Проверить это мы можем при помощи команд, приведенных на рисунке 13.

 

Строки преобразуются в объекты CimSession
Рисунок 13. Строки преобразуются в объекты CimSession

Таким образом, когда мы передаем массив строк, который представляет собой список классов WMI, команда Get-CimInstance указывает их в качестве значений параметра CimSession и пытается выполнить запрос компьютеров с именами Win32_ComputerSystem, Win32_OperatingSystem, Win32_BaseBoard и Win32_BIOS.

Что мы можем предпринять? Задать значение параметра CimSession явным образом. Теперь у команды Get-CimInstance не осталось параметров в используемом наборе (Parameter Set, в данном случае это ClassNameSessionSet), которым можно было бы сопоставить входящие данные. Поэтому, с одной стороны, команда не будет самостоятельно распределять поступающие по конвейеру объекты, а с другой мы по-прежнему можем к ним обращаться и использовать в качестве значений нужных нам параметров.

$Classes | Get-CimInstance -ClassName
   {$_} -CimSession localhost

Еще одним вариантом будет применение параметра ComputerName. В данном случае мы точно так же предотвращаем самостоятельное сопоставление входящих данных командой Get-CimInstance, используя на этот раз другой набор параметров — ClassNameComputerSet.

$Classes | Get-CimInstance -ClassName
   {$_} -ComputerName localhost

Переменная конвейера

Описанные выше методы работы с конвейером будут полезны, если нам нужен исключительно результат выполнения последней команды. Однако, если мы хотим, чтобы возвращаемый объект включал в себя и результаты промежуточных команд, нам потребуется задействовать параметр PipelineVariable.

Относящийся к набору общих параметров (Common Parameters), PipelineVariable позволяет нам указать имя переменной, в которую будут помещены результаты выполнения отдельной команды. По сути, указанная переменная будет содержать те же самые данные, что были переданы этой командой далее по конвейеру.

Таким образом, указав различные переменные для интересующих нас команд, мы сможем сформировать результирующий объект так, что он будет включать в себя значения свойств как последнего, так и промежуточных объектов конвейера.

Предположим, мы хотим получить объект, содержащий свойства Name, StartMode и State объекта службы Spooler, возвращаемого командой Get-CimInstance, полный путь к исполняемому файлу этой службы, его описание, версию, а также букву диска, на котором расположен данный файл, его объем и доступное свободное пространство в гигабайтах.

Сделать это мы можем при помощи следующей команды. Стоит обратить внимание, что вертикальная черта — символ конвейера — позволяет нам продолжить команду на следующей строке без необходимости использования символа обратной кавычки, backtick, он же grave (`). То же самое справедливо и для запятой.

Кроме того, для краткости вместо полного имени параметра PipelineVariable мы будем использовать его псевдоним (alias) — ‘pv’.

При формировании результирующего объекта в листинге 9 мы будем задействовать уже знакомые нам вычисляемые свойства, однако на этот раз, опять же в целях уменьшения объема кода, вместо Name и Expression будем использовать их сокращенные варианты — n и e.

Помощник ForEach-Object

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

Кроме того, так как команда не принимает значения по конвейеру, мы не сможем воспользоваться методом указания значений параметров в виде блоков сценария с использованием переменной $_ для ссылки на текущий объект конвейера. Таким образом, результатом следующей команды будет сообщение об ошибке (рисунок 14):

'Win32_Service' | Get-WmiObject
   -Class {$_}

 

Ошибка при выполнении команды, не поддерживающей конвейер
Рисунок 14. Ошибка при выполнении команды, не поддерживающей конвейер

Тем не менее это не помешает нам воспользоваться преимуществами конвейерной обработки данных. В тех случаях, когда используемые команды не поддерживают получение данных по конвейеру или же методы сопоставления значений определенным параметрам не вполне соответствуют вашим целям, вы можете воспользоваться командой ForEach-Object. Она позволяет получить данные по конвейеру и распределить значения свойств поступающих объектов по соответствующим параметрам.

Таким образом, добавив в предыдущий пример команду ForEach-Object, мы все-таки сможем получить все экземпляры класса Win32_Service, не подвергая структуру команды значительным изменениям.

'Win32_Service' | ForEach-Object
   {Get-WmiObject -Class $_}

Если же нас интересует несколько классов, мы можем указать их в виде массива, как в листинге 10.

Дополнительное преимущество этого метода заключается в том, что, в отличие от использования блоков сценария в качестве значений, здесь нам необязательно предсказывать логику сопоставления командой входящих объектов тем или иным параметрам. Так, в случае с передачей команде Get-CimInstance имен классов, как в листинге 11, использование ForEach-Object позволяет нам не указывать имя компьютера явным образом:

$Classes | ForEach-Object
   {Get-CimInstance -ClassName $_}

Кроме полного имени команды, мы можем задействовать один из ее псевдонимов: foreach или%.

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

$Classes | foreach
   {Get-CimInstance -ClassName $_}
$Classes |% {Get-CimInstance
   -ClassName $_}

И в завершение, чтобы для определения того, что команда не поддерживает работу с конвейером, вам не приходилось просматривать весь файл справки, вы можете воспользоваться сценарием, приведенным в листинге 12. Он позволяет выбрать из входящего массива команды, не поддерживающие получение данных по конвейеру. В качестве примера возьмем команды модуля Microsoft.PowerShell.Management.

Листинг 1. Добавление к объекту свойства с тем же значением
Get-CimInstance -ClassName Win32_Service -Filter "Name='Spooler'" | Add-Member
   -MemberType AliasProperty -Name Path -Value PathName -PassThru | Get-Item
Листинг 2. Создание пользовательского объекта
$Cim = Get-CimInstance -ClassName Win32_Service -Filter "Name=’Spooler’"

$SpoolerObject = [PSCustomObject]@{
Name = $Cim.Name
Path = $Cim.PathName
}
$SpoolerObject | Get-Item
Листинг 3. Пример использования вычисляемых свойств
Get-Process -Name powershell | Format-Table -Property Name,Id, @{Name = ‘TimeRunning’; Expression = {(Get-Date) - $_.StartTime}; FormatString = "d\.hh\:mm\:ss"}
Листинг 4. Использование команды Select-Object
Get-CimInstance -ClassName Win32_Service -Filter "Name=’Spooler’" | Select-Object -Property @{Name = ‘Path’; Expression = {$_.PathName}} | Get-Item
Листинг 5. Пример расширенной команды Select-Object
Get-CimInstance -ClassName Win32_Service -Filter "Name='Spooler'" | Select-Object -Property @{Name = 'Path'; Expression = {$_.PathName}} | Get-Member
Листинг 6. Запрос значения свойства pstypenames
Get-CimInstance -ClassName Win32_Service -Filter "Name=’Spooler’" | Select-Object -Property @{Name = ‘Path’; Expression = {$_.PathName}} | ForEach-Object -MemberName PSTypeNames
Листинг 7. Получение информации о компьютерах из Active Directory
$Computers = Get-ADComputer -Filter {name -like 'Comp-*'}
$Computers | Get-CimInstance -ClassName Win32_ComputerSystem -ComputerName {$_.Name}
Листинг 8. Запрос нескольких классов WMI с локального компьютера
$Classes = 'Win32_ComputerSystem', 'Win32_OperatingSystem', 'Win32_BaseBoard', 'Win32_BIOS'
$Classes | Get-CimInstance -ClassName {$_}
Листинг 9. Формирование результирующего объекта
Get-CimInstance -ClassName Win32_Service -Filter "Name=’Spooler’" -pv CimInstance |
Get-Item -Path {$_.PathName} -pv Item |
Split-Path -Path {$_.FullName} -Qualifier -pv Drive |
Get-Volume -DriveLetter {$_.Substring(0,1)} |
Select-Object -Property @{n = ‘ServiceName’; e = {$CimInstance.Name}},
@{n = ‘ServiceStartMode’; e = {$CimInstance.StartMode}},
@{n = ‘ServiceState’; e = {$CimInstance.State}},
@{n = ‘FileName’; e = {$Item.FullName}},
@{n = ‘FileVersion’; e = {$Item.VersionInfo.FileVersion}},
@{n = ‘FileDescription’; e = {$Item.VersionInfo.FileDescription}},
@{n = ‘DriveLetter’; e = {$Drive}},
@{n = ‘DriveSizeGB’; e = {[math]::Truncate($_.Size/1GB)}},
@{n = ‘SizeRemainingGB’; e = {[math]::Truncate($_.SizeRemaining/1GB)}}

ServiceName : Spooler
ServiceStartMode : Auto
ServiceState : Running
FileName : C:\WINDOWS\System32\spoolsv.exe
FileVersion : 10.0.16299.15 (WinBuild.160101.0800)
FileDescription : Spooler SubSystem App
DriveLetter : C:
DriveSizeGB : 100
SizeRemainingGB : 40
Листинг 10. Указание классов в виде массива
‘Win32_Service’, ‘Win32_Process’, ‘Win32_ComputerSystem’, ‘Win32_OperatingSystem’ | ForEach-Object {Get-WmiObject -Class $_}
Листинг 11. Передача команде Get-CimInstance имен классов
$Classes = 'Win32_ComputerSystem', 'Win32_OperatingSystem', 'Win32_BaseBoard', 'Win32_BIOS'
$Classes | Get-CimInstance -ClassName {$_} -ComputerName localhost
Листинг 12. Определение того, что команда не поддерживает работу с конвейером
$Commands = Get-Command -Module 'Microsoft.PowerShell.Management'
foreach ($Command in $Commands)
{
$Command.Parameters.Values.Attributes |
Where-Object {$_.TypeId.Name -eq 'ParameterAttribute'} |
ForEach-Object {if ($_.ValueFromPipeline -or
$_.ValueFromPipelineByPropertyName){continue}}
Write-Output -InputObject $Command.Name
}