Глава 15. Работа со множеством объектов по одному за раз

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

[Замечание]Замечание

Это чрезвычайно сложная глава и, вероятно, она обманет ваши надежды. Будьте терпеливы, пожалуйста - сами по себе и по отношению к нам - и поверьте, к концу мы всё расставим по местам.

Предпочтительный способ: пакетные командлеты

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


Get-Service | Stop-Service
		

Это пример пакетного управления при помощи командлета. В данном случае Stop-Process специально спроектирован на приём одного объекта процесса из своего конвейера и затем останавливать его. Set-Service, Start-Process, Move-Item и Move-AdObject, всё это примеры командлетов, которые принимают один или более объектов на входе и затем выполняют некую задачу или действие с каждым из них. PowerShell знает как работать с пакетами объектов и способен обрабатывает их для вас при относительно простом синтаксисе.

Такие пакетные командлеты (batches cmdlet, это наше название - и это не официальный термин) это наш предпочтительный способ выполнения задач. Например, давайте предположим, что нам требуется изменить значение типа запуска для множества служб:


Get-Service BITS,Spooler | Set-Service -startuptype Automatic
		

Одним из потенциальных недостатков такого подхода является то, что выполняющие некое действие командлеты часто не производят никакого вывода, указывающего что они выполнили своё задание. Вы не будете обладать никаким визуальным выводом не из какой из предыдущих команд, что может приводить в замешательство. Однако такие командлеты часто обладают параметром -PassThru, который сообщает им выводить все объекты, которые они получают на входе. Вы можете заставить Set-Service выводить те же службы, которые он изменил с тем, чтобы вы были способны убедиться что они были изменены. Вот некий образец применения -PassThru с другим командлетом:


Get-ChildItem .\ | Copy-Item -Destination C:\Drivers -PassThru
		

Данная команда получает все имеющиеся элементы из своего текущего каталога. Эти объекты затем конвейером отправляются в Copy-Item, который далее копирует эти элементы в свой каталог C:\Drivers. Поскольку в самом конце мы помещаем соответствующий параметр -PassThru, на вашем экране будет отражено что он выполняет. Если бы мы не сделали этого, тогда после завершения это просто вернулось бы обратно в приглашение на ввод нашего PowerShell.

Испробуйте это сейчас Скопируйте несколько файлов или папок из одного каталога в другой. Попробуйте это как с, так и без параметра -PassThru и обратите внимание на получаемое отличие.

Способ CIM: активация методов

Прежде чем мы начнём, имеются два момента, о которых нам надлежит знать:

  • Windows Management Instrumentation (WMI, инструментальные средства управления средой Windows) не работают с PowerShell 7. Вы обязаны работать с командами Common Information Model (CIM, Общей информационной модели), которая в целом работает аналогичным образом.

  • Раздел 15.2 предназначен исключительно для тех, кто применяет Windows. Мы делаем всё возможное, чтобы всё, что делается в данной книге переносилось бы между платформами. Но существуют случаи, когда это невозможно, и это один из них.

К сожалению, у нас не всегда имеются командлеты, которые мы можем брать всякий раз, когда требуется некое действие, и это так когда речь идёт об элементах, которыми мы можем управлять через CIM. К примеру, рассмотрим имеющийся класс CIM Win32_NetworkAdapterConfiguration. Этот класс представляет ту конфигурацию, которая связана с сетевым адаптером (адаптеры способны обладать множеством конфигураций, однако в данном случае давайте предположим, что они обладают лишь одной конфигурацией на брата, что распространено в компьютерах клиентов). Скажем, нашей целью является разрешение DHCP во всех сетевых адаптерах Intel наших компьютеров - мы не хотим включать его в своих RAS или прочих виртуальных адаптерах.

[Замечание]Замечание

Мы ознакомим вас с краткой сюжетной линией, признанной помочь вам разобраться с тем, как парни пользуются PowerShell. Некоторые вещи могут вам показаться избыточными, но потерпите - ценен сам опыт.

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


DHCPEnabled      : False
IPAddress        : {192.168.10.10, fe80::ec31:bd61:d42b:66f}
DefaultIPGateway :
DNSDomain        :
ServiceName      : E1G60
Description      : Intel(R) PRO/1000 MT Network Connection
Index            : 7
DHCPEnabled      : True
IPAddress        :
DefaultIPGateway :
DNSDomain        :
ServiceName      : E1G60
Description      : Intel(R) PRO/1000 MT Network Connection
Index            : 12
 	   

Для получения данного вывода нам потребуется выполнить запрос на соответствующий объект CIM и отфильтровать его чтобы он содержал в своём описании лишь конфигурации с Intel. Это выполняет приводимая ниже команда (обратите внимание, что символ % действует как символ подстановки внутри соответствующего синтаксиса фильтра WMI):


PS C:\> Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | -Filter "description like '%intel%'" | Format-List
		

Испробуйте это сейчас Вы можете следовать запускаемым нами в данном разделе командам. Чтобы они заработали, возможно, вам придётся слегка видоизменить эти команды. Например, если в вашем компьютере отсутствуют сетевые адаптеры производства Intel, вам придётся соответствующим образом изменить критерий фильтрации.

Когда мы получим такие объекты конфигурации в своём конвейере, мы хотим разрешить в них DHCP (вы можете наблюдать, что один из наших адаптеров не обладает включённым DHCP). Мы можем начать с просмотра какого- то командлета с названием, подобным Enable-DHCP. К сожалению, мы не найдём его, потому как нет такого в природе. Не существует никаких командлетов, которые способны иметь дело с объектами CIM непосредственно в пакетах.

Испробуйте это сейчас На основании того, с чем мы ознакомились на данный момент, какой именно командой вы бы воспользовались для поиска команды, которая содержит в своём названии DHCP?

Наш следующий шаг состоит в том, чтобы посмотреть обладает ли сам объект неким методом, который способен разрешать DHCP. Для выявления этого мы исполняем команду Get-CimClass и раскрываем соответствующее свойство CimClassMethods:


PS C:\> (Get-CimClass Win32_NetworkAdapterConfiguration).CimClassMethods
		

В самом верху мы видим некий метод с названием EnableDHCP (Рисунок 15.1)

 

Рисунок 15-1


Отображение доступных методов

Наш следующий шаг, который пробует множество новичков PowerShell, состоит в отправке в этот метод конвейером полученных объектов конфигурации:


PS C:\> Get-CimInstance win32_networkadapterconfiguration -filter "description like '%intel%'" | EnableDHCP
		

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

Хотя и не существует никакого "пакетного" командлета с названием Enable-DHCP, вы можете применить универсальный командлет с названием Invoke-CimMethod. Данный командлет специально для приёма пакетных или CIM объектов, таких как наши объекты Win32_NetworkAdapterConfiguration и для вызова одного из методов, подключённого к таким объектам. Вот исполняемая нами команда:


PS C:\> Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -filter "description like '%intel%'" | Invoke-CimMethod -MethodName EnableDHCP
		

Нам следует иметь в виду несколько моментов:

  • Значение названия метода не следует в круглых скобках.

  • Значение названия метода не чувствительно к регистру букв.

  • Invoke-CimMethod может принимать лишь один вид объекта WMI за раз. В данном случае, мы отправляем ему только объекты Win32_NetworkAdapterConfiguration, что означает, что это сработает как ожидалось. Нормально отправлять более одного объекта за раз (фактически, это общее место), однако все эти объекты обладают одним и тем же типом.

  • Мы можем применять в Invoke-CimMethod -WhatIf и -Confirm. Однако вы не можете пользоваться ими при вызове метода непосредственно из объекта.

Вывод Invoke-CimMethod очень прост для понимания. Он снабжает нас двумя моментами: неким возвращаемым значением и тем компьютером, в котором он выполнен (когда имя компьютера пустое, он исполнялся в localhost).


ReturnValue PSComputerName
----------- --------------
         84
 	   

Значение числа ReturnValue сообщает нам значение результата данной операции. Быстрый поиск в вашем любимом механизме поиска для Win32_NetworkAdapterConfiguration снабдит нас страницей документации, и мы затем сможем кликнуть по искомому методу EnableDHCP чтобы увидеть возможные возвращаемые значения и их значения. (Рисунок 15.2) показывает что мы разыскали.

 

Рисунок 15-2


Поиск возвращаемых значений для результатов метода WMI

Значение 0 означает успех, в то время как 84 означает, что данный IP не включён в данной конфигурации адаптера и DHCP не может быть разрешён. Однако какой из битов наших выходных данных относится к какой из наших двух конфигураций сетевых адаптеров Трудно сказать, потому как выходные данные не сообщают нам какой именно объект конфигурации их создаёт. Это прискорбно, но именно так работает CIM.

Invoke-CimMethod срабатывает в большинстве ситуаций в которых мы имеем объект CIM, который обладает методом, который мы желаем выполнить. Он также срабатывает при запросе объектов с удалённых компьютеров. Наше основное правило это "Если вы можете получить нечто при помощи Get-CIMInstance, тогда Invoke-CimMethod способен выполнять его методы".

План резервного копирования: перечисление объектов

К сожалению, мы сталкиваемся с ситуациями, когда у нас есть командлет, который способен производить объекты, но мы не знаем никакого пакетного командлета, в который мы способны конвейером отсылать такие объекты чтобы предпринимать некий вид действий. Также мы сталкивались с ситуациями, при которых командлет не получает из своего конвейера никаких входных данных. В любом случае, вы всё ещё способны выполнять всякую задачу, которую пожелаете осуществить, но вам придётся вернуться к походу с бо́льшей детализацией, предписывая компьютеру перечислять свои объекты и выполнять свою задачу по одному за раз для каждого из объектов. PowerShell предлагает два способа выполнения этого: один заключается в применении командлета, а другой при помощи построения сценария. В данной главе мы сосредоточимся на первом указанном методе, поскольку он самый простой. Вы всегда должны пытаться применять командлет, а не пробовать самостоятельно писать его. Второй подход мы сохраним для Главы 19, в которой мы погрузимся во встроенный язык программирования PowerShell.

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


Get-Help Get-Process -Full
		

Что выдаст нам всё... но пролистайте до раздела с названием "Id". Вы обнаружите, что в некоторых параметрах указано, что они получают входные данные, но в скобках указано ByPropertyName. Это означает, что когда мы конвейером передаём некий объект в этот командлет, а у него имеется название свойства, например, именуемое как Id, данный командлет воспользуется следующим:


-Id <System.Int32[]>
        Specifies one or more processes by process ID (PID). To specify multiple IDs, use commas to separate the IDs.
        To find the PID of a process, type 'Get-Process'.
		
        Required?                    true
        Position?                    named
        Default                      value None
        Accept pipeline input?       True (ByPropertyName)
        Accept wildcard characters?  false
		
    -IncludeUserName <System.Management.Automation.SwitchParameter>
        Indicates that the UserName value of the Process object is returned with results of the command.

        Required?                    true
        Position?                    named
        Default                      value False
        Accept pipeline input?       False
        Accept wildcard characters?  false
		

Однако, что если мы просто хотим переслать конвейером список строк, которым выступают названия тех процессов, которые мы бы желали создать? У нас бы не было такой возможности, потому как наш параметр Name не поддерживает иной тип передачи конвейером, кроме как ByValue. Пройдём далее и попробуем это. Давайте взглянем на команду New-AzKeyVault. Мы поместим свои значения в некий массив и отправим их конвейером в свою команду New-AzKeyVault:


@( "vaultInt1", "vaultProd1", "vaultInt2", "vaultProd2" ) | New-AzKeyVault
		

Это предоставит нам следующий менее- чем- идеальный для прочтения текст:


New-AzKeyVault: 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.
{New-AzKeyVault: Полученный на входе объект не может быть связан ни с какими параметрами для данной команды либо по причине того что эта команда не способна получать входные данные конвейером, либо эти входные данные и их свойства не соответствуют никакому из установленных параметров, принимаемых в качестве входных.}
		

Давайте рассмотрим подробнее то, как мы всё же можем достигать своей цели даже когда наш командлет не поддерживает то, что мы пытаемся сделать.

Заставляем командлеты работать на вас

В данный момент нам придётся принять решение. Вполне вероятно, что вы запускаем свою команде неверно, поэтому нам придётся решить желаем ли мы потратить много времени на решение этого. Также вполне вероятно, что New-AzKeyVault просто не поддерживает то, что бы мы желали выполнить, и в этом случае мы бы затратили много времени чтобы попытаться исправить нечто, чем мы не имеем возможности управлять.

Нам требуется создать текстовый файл со списком значений имён, которые мы бы желали создать. наш vaultsToCreate.txt выглядит так:


vaultInt1
vaultInt2
vaultProd1
vaultProd2
 	   

Наш обычный выбор для этого случая состоит в том, чтобы попробовать разные подходы. Мы намерены попросить у компьютера (ладно, у оболочки) перенумеровать эти объекты (в нашем случае строки) по одной за раз, поскольку наша команда New-AzKeyVault принимает лишь по одному объекту за раз и исполнить New-AzKeyVault для этих объектов. Для этого мы воспользуемся своим командлетом ForEach-Object:


Get-Content -Path vaultsToCreate.txt | ForEach-Object { New-AzKeyVault -ResourceGroupName manning -Location 'UK South' -Name $_ }
		

Для четырёх созданных нами ресурсов мы получим четыре результата, которые выглядят так (здесь приводится лишь часть вывода, ибо получаемые результаты могут быть достаточно длинными):


Vault Name                       : vaultInt1
Resource Group Name              : manning
Location                         : Australia Central
Resource ID                      : /subscriptions/*****/resourceGroups/manning/providers/Microsoft.KeyVault /vaults/vaultInt1
Vault URI                        : https://vaultint1.vault.azure.net/
Tenant ID                        : *********
SKU                              : Standard
Enabled For Deployment?          : False
Enabled For Template Deployment? : False
Enabled For Disk Encryption?     : False
Soft Delete Enabled?             :
 	   

В соответствующей документации мы обнаруживаем, что когда мы получаем обратно подобный приводимому отклик, это означает успех, что подразумевает, что мы достигли своей цели. Однако давайте рассмотрим эту команду более подробно.


Get-Content -Path vaultsToCreate.txt |
 ForEach-Object -Process {
   New-AzKeyVault -ResourceGroupName manning -Location 'UK South' -Name $_
}
		

В данной команде много чего происходит. Первая строка должна иметь смысл: для выборки значений названий своих сейфов (vaults), которые мы поместили в текстовый файл, мы пользуемся Get-Content. Мы конвейером отправляем эти объекты string в свой командлет ForEach-Object:

  • Прежде всего, мы видим название своего командлета: ForEach-Object.

  • Затем, мы применяем параметр -Process для задания блока сценария. Мы изначально не набрали название параметра -Process, потому как это позиционный параметр. Однако этот блок сценария - всё что содержится внутри фигурных скобок - это значение для данного параметра -Process. Мы пошли дальше и включили имя этого параметра при переформатировании своей команды для облегчения восприятия.

  • ForEach-Object исполняет свой блок сценария по разу для каждого из объектов, которые были конвейером отправлены в ForEach-Object. При каждом исполнении блока сценария, соответствующий следующий направляемый конвейером объект помещается в специальный заполнитель $_, который мы видим передаваемым в качестве параметра Name в New-AzKeyVault.

{Прим. пер.: по- существу, здесь вместо объекта мы применяем структуру с признаком (trait), типичный современный подход, например, применяемый в Rust и поясняемый, скажем, в нашем переводе Ускоряем ваш Python при помощи Rust Максвелл Флиттон, декабрь 2021, (с) Packt Publishing.}

Давайте ускорим вещи

В своих предыдущих главах мы обсудили применение заданий PowerShell для параллельного запуска команд чтобы вы сохраняли время. Для спрямления такой возможности сохранения времени PowerShell 7 предложил новый параметр в ForEach-Object: -Parallel. С ним лучше разобраться на неком примере с использованием повсеместно знаменитого командлета Measure-Command, который позволяет вам выполнять замеры вещей любого рода., однако мы будем применять его для определения времени длительности исполнения блока сценария. Это выглядит следующим образом:


Measure-Command { <# The script we want to time #> }
		

Итак, давайте попробуем. Для первого раза мы испробуем нечто простое при помощи обычного ForEach-Object:


Get-Content -Path vaultsToCreate.txt | ForEach-Object -Process {
  Write-Output $_
  Start-Sleep 1
}
		

Всё что здесь требуется, так это вывести на печать каждую строку из этого файла и затем заснуть на одну секунду для каждой строки. Если у нас есть в этом файле пять строк, скорее всего, вы сможете предвидеть сколько времени потребует это исполнение, но давайте воспользуемся Measure-Command:


Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -Process {
     Write-Output $_
     Start-Sleep 1
   }
}
		

Когда мы выполним это, мы получим:


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 5
Milliseconds      : 244
Ticks             : 52441549
TotalDays         : 6.06962372685185E-05
TotalHours        : 0.00145670969444444
TotalMinutes      : 0.0874025816666667
TotalSeconds      : 5.2441549
TotalMilliseconds : 5244.1549
 	   

Давайте конкретно рассмотрим значение Seconds, равное 5. Это имеет смысл, не так ли? Раз у нас имеются в нашем файле пять строк, и мы обрабатываем по строке за раз, а при этом мы засыпаем на 1 секунду в каждой, мы ожидаем, что команда исполняется примерно 5 секунд.

Теперь давайте изменим ту же самую команду на применение вместо Process параметра Parallel:


Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -Parallel {
     Write-Output $_
     Start-Sleep 1
   }
}
		

Есть предположения? Давайте запустим:


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 340
Ticks             : 13405417
TotalDays         : 1.55155289351852E-05
TotalHours        : 0.000372372694444444
TotalMinutes      : 0.0223423616666667
TotalSeconds      : 1.3405417
TotalMilliseconds : 1340.5417
		

Секундочку! Это происходит по той причине, что Parallel делает именно то, что подразумевает его название - он запускает все блоки сценариев параллельно, а не последовательно. Поскольку у нас в файле имеются пять элементов и мы исполняем все их параллельно, а также мы заставляем их все заснуть на 1 секунду, вся операция занимает лишь около секунды. Это очень полезно для задач с длительным временем исполнения или в ситуациях, при которых у вас имеется множество задач меньшего размера, которые вы бы хотели исполнять пакетно. Мы даже можем взять свой пример и воспользоваться Parallel ForEache:


Get-Content -Path vaultsToCreate.txt |
 ForEach-Object -Parallel {
   New-AzKeyVault -ResourceGroupName manning -Location 'UK South' -Name $_
}
		

Parallel очень сильный параметр для Parallel ForEach, но в действительности он обладает некоторыми ограничениями, о которых вам надлежит знать. Для начала, по умолчанию, Parallel ForEach запустит параллельно лишь пять блоков сценариев. Это носит название предела дросселирования (throttle limit) и может регулироваться при помощи параметра ThrottleLimit. Вернёмся обратно к тому файлу, которы мы применяли и убедимся что в нём всего 10 строк. Получаемое отличие достаточно заметно:


Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -Process {
     Write-Output $_
     Start-Sleep 1
   }
}
		

Без настройки значения предела дросселирования мы получаем 2 секунды:


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 2
Milliseconds      : 255
Ticks             : 22554472
TotalDays         : 2.6104712962963E-05
TotalHours        : 0.000626513111111111
TotalMinutes      : 0.0375907866666667
TotalSeconds      : 2.2554472
TotalMilliseconds : 2255.4472
 	   

Однако, когда мы поднимем значение предела дросселирования до 10, мы получаем


Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -ThrottleLimit 10 -Process {
     Write-Output $_
     Start-Sleep 1
  }
}
		

наша команда завершается через 1 секунду!


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 255
Ticks             : 12553654
TotalDays         : 1.45296921296296E-05
TotalHours        : 0.000348712611111111
TotalMinutes      : 0.0209227566666667
TotalSeconds      : 1.2553654
TotalMilliseconds : 1255.3654
 	   

Parallel ForEach это очень мощная функциональная возможность PowerShell. Вы сбережёте много времени если будете пользоваться её преимуществом правильным способом.

Общие моменты путаницы

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

Какой путь является верным?

Мы пользуемся термином пакетный командлет (batch cmdlet) или командлет действия (action cmdlet) для всякого командлета, который осуществляет некое действие для группы или собрания объектов за раз. Вместо того чтобы обладать инструкцией для вашего компьютера "пройтись по этому перечню вещей и выполнить это единственное действие для каждой из этих вещей", вы можете отправить в командлет всю группу целиком и сам командлет обработает их.

Microsoft в своих продуктах улучшает представительство таких командлетов, но они покрываю ещё пока не все 100% (и, скорее всего, оно не будет таковым на протяжении ещё многих лет из- за наличия множества сложных продуктов Microsoft). Но когда некий командлет имеется, мы предпочитаем пользоваться им. Тем не менее, прочие разработчики PowerShell предпочитают альтернативные способы, в зависимости от того, что они узнали первым и что они запомнили проще всего. Все последующие действия обладают одним и тем же результатом:


Get-Process -name *B* | Stop-Process                                 # пакетный командлет
Get-Process -name *B* | ForEach-Object { $_.Kill()}                  # вызов ForEach-Object метода Kill()
Get-Process -Name *B* | ForEach-Object -Parallel { Stop-Process $_ } # вызов Stop-Process через ForEach-Object 
		

Давайте рассмотрим как работает каждый из подходов

  • Пакетный командлет. Здесь для выборки всех процессов мы применяем Get-Process с неким B в названии и затем останавливаем их.

  • ForEach-Object вызывает метод Kill(). Этот подход аналогичен пакетному командлету, но вместо применения пакетного командлета мы через конвейер передаём значения процессов в ForEach-Object и просим для каждой службы выполнить Kill().

  • ForEach-Object вызывает Stop-Process, применяя -Parallel.

Чёрт возьми, имеется даже четвёртый подход - воспользоваться языком программирования сценариев PowerShell чтобы выполнить то же самое. В PowerShell вы обнаружите множество способов выполнить практически всё, причём ни один из них не является неправильным. Некоторые легче выучить, запомнить и повторить, нежели иные, поэтому мы сосредоточились на имеющихся в вашем распоряжении методах в том порядке, как мы это делали. Чем следует пользоваться? Это не имеет значения, поскольку не существует единственного верного способа. В конечном итоге, вы даже можете применять их сочетание, в зависимости от тех обстоятельств и возможностей, которые способна предложить вам сама оболочка для осуществления поставленной задачи.

Убывающая отдача для параллельности ForEach

Помните наш пример с Parallel ForEach? Он выглядит следующим образом:


Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -Parallel {
     Write-Output $_
     Start-Sleep 1
   }
}
		

Теперь, допустим, vaultsToCreate.txt обладает внутри себя 100 строками. Следует ли нам установить значение ThrottleLimit в 100 с тем, чтобы наша операция завершилась за 1 секунду? Давайте попробуем:


Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -ThrottleLimit 100 -Parallel {
     Write-Output $_
     Start-Sleep 1
   }
}
		

Это выдаёт нам результат в 3 секунды. Странно:


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 3
Milliseconds      : 525
Ticks             : 35250040
TotalDays         : 4.07986574074074E-05
TotalHours        : 0.000979167777777778
TotalMinutes      : 0.0587500666666667
TotalSeconds      : 3.525004
TotalMilliseconds : 3525.004
 	   

Почему так медленно? Что ж, получается, что узким местом выступает ваша машина, которая способна выполнять только не столь много вещей одновременно, прежде чем начнёт замедляться. Это похоже на Start-ThreadJob, который мы наблюдали в Главе 14. Один процесс способен выполнять лишь столько вещей параллельно, прежде чем начнёт работать медленнее, чем их последовательное исполнение.

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

Документация метода

Всегда помните, что передача в Get-Member конвейером объектов извлекает методы:


Get-Process | Get-Member
		

Встроенная система подсказок PowerShell не документирует методы объектов. Например, когда вы получаете перечень для объекта процесса, вы можете обнаружить, что присутствуют методы с названиями Kill и Start:


TypeName: System.Diagnostics.Process
Name                       MemberType     Definition
----                       ----------     ----------
BeginErrorReadLine         Method         void BeginErrorReadLine()
BeginOutputReadLine        Method         void BeginOutputReadLine()
CancelErrorRead            Method         void CancelErrorRead()
CancelOutputRead           Method         void CancelOutputRead()
Close                      Method         void Close()
CloseMainWindow            Method         bool CloseMainWindow()
Dispose                    Method         void Dispose(), void IDisposable.Dispose()
Equals                     Method         bool Equals(System.Object obj)
GetHashCode                Method         int GetHashCode()
GetLifetimeService         Method         System.Object GetLifetimeService()
GetType                    Method         type GetType()
InitializeLifetimeService  Method         System.Object InitializeLifetimeService()
Kill                       Method         void Kill(), void Kill(bool entireProcessTree)
Refresh                    Method         void Refresh()
Start                      Method         bool Start()
ToString                   Method         string ToString()
WaitForExit                Method         void WaitForExit(), bool WaitForExit(int milliseconds)
WaitForInputIdle           Method         bool WaitForInputIdle(), bool  WaitForInputIdle(int milliseconds)
 	   

Чтобы отыскать для них документацию, сосредоточьтесь на TypeName, которым в данном случае выступает System.Diagnostics.Process. В механизме поиска отыщите это название типа целиком и вы обычно проследует через официальную документацию разработчика для данного типа, что после этого приведёт вас к необходимой документации для этого конкретного метода.

Путаница ForEach-Object

Командлет ForEach-Object обладает сложной пунктуацией синтаксиса, а добавление в некий метод собственного синтаксиса может создавать уродливую командную строку. Мы собрали некоторые советы для преодоления умственных тупиков:

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

  • Заключённый в фигурные скобки блок сценария выполняется по разу для каждого передаваемого конвейером в его командлет объекта.

  • Внутри конкретного блока сценария значение $_ представляет текущий объект из его конвейера.

  • Для работы с переданным в конвейере объектом целиком применяйте сам $_; для работы с индивидуальными методами или свойствами вслед за $_ разделяйте их точкой.

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

Лабораторные занятия

[Замечание]Замечание

Для данных лабораторных занятий вам требуется машина с PowerShell v7 или выше.

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

  1. Какой метод объекта DirectoryInfo (производимого Get-ChildItem) удалит этот каталог?

  2. Какой метод объекта Process (производимого Get-Process) прекратил бы этот процесс?

  3. Напишите три команды, которые можно было бы применять для удаления всех файлов и каталогов, которые обладают в своём названии deleteme, в предположении, что в своём названии обладают этим множество файлов и каталогов.

  4. Допустим, у вас имеется текстовый файл с названиями компьютеров, но вы хотите отображать все их в заглавном регистре. Каким бы выражением PowerShell вы бы воспользовались?

Ответы лабораторных занятий

  1. Отыщите методы, подобным образом:

    Get-ChildItem | Get-Member -MemberType Method

    Вы должны обнаружить метод Delete().

  2. Отыщите методы, подобным образом:

    get-process | Get-Member -MemberType Method

    Вы должны обнаружить метод Kill(). Вы можете удостовериться проверив документацию MSDN для данного типа объекта процесса. Естественно, вам нет необходимости активировать этот метод, поскольку имеется командлет, Stop-Process, который выполнит за вас ту же работу.

  3. Get-ChildItem *deleteme* | Remove-Item -Recurse -Force

    Remove-Item *deleteme* -Recurse -Force

    Get-ChildItem *deleteme* | foreach {$_.Delete()}

  4. Get-content computers.txt | foreach {$_.ToUpper()}