Конвейерная обработка данных присутствует во многих командных интерпретаторах, однако PowerShell выгодно отличает от прочих то, что между составными частями конвейера, то есть командами и функциями, передается не текст, который представляет собой результат выполнения предыдущей команды, а объекты. Таким образом, когда мы вводим команду:
Get-Service -Name Spooler | Restart-Service
команда Restart-Service получает в качестве входных данных не три строки текста, как на рисунке 1, а объект типа System.ServiceProcess.ServiceController.
Рисунок 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.
Рисунок 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).
Рисунок 3. Параметры команды Restart-Service |
InputObject поддерживает только вариант с передачей всего объекта — ByValue, и, как мы видим, это может быть только объект (или объекты) типа ServiceController.
Параметр Name принимает объект целиком (и в данном случае это должна быть строка — String) или же, если по конвейеру поступает объект иного типа, аргументом для данного параметра будет значение свойства Name входящего объекта.
Однако, присмотревшись к объекту System.ServiceProcess.Service Controller (рисунок 4), мы увидим, что он тоже содержит свойство Name. В связи с этим возникает вопрос: значением какого параметра, InputObject или Name, становятся получаемые по конвейеру данные?
Рисунок 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.Management.Infrastructure.CimInstance, который к ожидаемому параметром InputObject команды Restart-Service объекту типа System.ServiceProcess.ServiceController не имеет никакого отношения. Тем не менее он содержит свойство Name, значением которого является имя службы. И поэтому мы вполне можем использовать следующую команду для выполнения перезагрузки необходимой нам службы:
Get-CimInstance -ClassName Win32_Service -Filter "Name='Spooler'" | Restart-Service
Что же касается пользовательского объекта, то это может быть любой объект, содержащий в качестве значения свойства Name имя службы. Например, такой как на рисунке 5.
Рисунок 5. Объект, содержащий в качестве значения свойства Name имя службы |
Вычисляемые свойства
Если мы присмотримся к структуре возвращаемого командой Get-CimInstance объекта службы, то увидим, что он обладает свойством PathName, значением которого является путь к исполняемому файлу. Давайте предположим, что нам необходимо получить об этом файле как можно более подробную информацию.
Для этого нам потребуется команда Get-Item. Однако если мы попробуем передать по конвейеру полученный в результате выполнения команды Get-CimInstance объект команде Get-Item, то получим сообщение об ошибке, как на рисунке 6.
Рисунок 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
Рисунок 7. Результат работы Get-CimInstance |
Как видите, это не совсем то, что мы хотели бы передать команде Get-Item в качестве значения параметра Path.
Путь к файлу представлен значением свойства PathName объекта CimInstance. Команда Get-Item для получения пути к нужному файлу использует параметр Path. Что нам нужно сделать, так это каким-либо образом передать свойство PathName под именем Path.
Вариантов здесь достаточно много: от добавления к объекту свойства с тем же значением (листинг 1) до создания уже упомянутого выше пользовательского объекта (листинг 2).
Однако более удобным в данном случае будет использование вычисляемых свойств — Calculated Properties.
Вычисляемые свойства — это метод взаимодействия с данными, используя который вы можете определять новые свойства объектов в процессе их прохождения по конвейеру. При этом вы можете задать нужное имя свойства, выражение, результат которого будет значением этого свойства, а также, при использовании команд Format-*, если потребуется, указать параметры форматирования полученного значения. Например, так, как показано в листинге 3, результат исполнения которого приведен на рисунке 8.
Рисунок 8. Результат исполнения листинга 3 |
Что же касается нашего случая, то здесь потребуется команда Select-Object (листинг 4), результат исполнения которой показан на рисунке 9.
Рисунок 9. Результат исполнения листинга 4 |
Если мы присмотримся к результату выполнения команды Select-Object, приведенной в листинге 5, то увидим, что он представляет собой объект с единственным свойством Path — тем, что мы определили в параметре Property (рисунок 10).
Рисунок 10. Результат исполнения листинга 5 |
Кроме того, запросив значение свойства pstypenames с помощью команды, приведенной в листинге 6, мы обнаружим, что по сути это тот же пользовательский объект, разве что полученный другим способом (рисунок 11).
Рисунок 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. И так для каждого класса.
Рисунок 12. Ошибка при выполнении запроса листинга 8 |
О чем стоит помнить при использовании данного метода, так это о том, что обычный порядок действий по обработке входных данных и сопоставлению их различным параметрам никто не отменял.
Что сейчас попыталась сделать команда Get-CimInstance? Если мы заглянем в файл справки, то увидим, что изо всех поддерживающих получение данных по конвейеру параметров метод ByValue используют только два из них — CimSession и InputObject.
Параметр InputObject работает с объектами типа Microsoft.Management.Infrastructure.CimInstance. Мы же передаем массив строк, так что в данном случае он не используется.
Второй параметр, CimSession, поддерживает только значения типа Microsoft.Management.Infrastructure.CimSession, и, казалось бы, он тоже не должен взаимодействовать с поступающими объектами, однако, как выясняется, строки замечательно преобразуются в объекты CimSession. Проверить это мы можем при помощи команд, приведенных на рисунке 13.
Рисунок 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.
Get-CimInstance -ClassName Win32_Service -Filter "Name='Spooler'" | Add-Member -MemberType AliasProperty -Name Path -Value PathName -PassThru | Get-Item
$Cim = Get-CimInstance -ClassName Win32_Service -Filter "Name=’Spooler’" $SpoolerObject = [PSCustomObject]@{ Name = $Cim.Name Path = $Cim.PathName } $SpoolerObject | Get-Item
Get-Process -Name powershell | Format-Table -Property Name,Id, @{Name = ‘TimeRunning’; Expression = {(Get-Date) - $_.StartTime}; FormatString = "d\.hh\:mm\:ss"}
Get-CimInstance -ClassName Win32_Service -Filter "Name=’Spooler’" | Select-Object -Property @{Name = ‘Path’; Expression = {$_.PathName}} | Get-Item
Get-CimInstance -ClassName Win32_Service -Filter "Name='Spooler'" | Select-Object -Property @{Name = 'Path'; Expression = {$_.PathName}} | Get-Member
Get-CimInstance -ClassName Win32_Service -Filter "Name=’Spooler’" | Select-Object -Property @{Name = ‘Path’; Expression = {$_.PathName}} | ForEach-Object -MemberName PSTypeNames
$Computers = Get-ADComputer -Filter {name -like 'Comp-*'} $Computers | Get-CimInstance -ClassName Win32_ComputerSystem -ComputerName {$_.Name}
$Classes = 'Win32_ComputerSystem', 'Win32_OperatingSystem', 'Win32_BaseBoard', 'Win32_BIOS' $Classes | Get-CimInstance -ClassName {$_}
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
‘Win32_Service’, ‘Win32_Process’, ‘Win32_ComputerSystem’, ‘Win32_OperatingSystem’ | ForEach-Object {Get-WmiObject -Class $_}
$Classes = 'Win32_ComputerSystem', 'Win32_OperatingSystem', 'Win32_BaseBoard', 'Win32_BIOS' $Classes | Get-CimInstance -ClassName {$_} -ComputerName localhost
$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 }