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

 

Берем только нужное

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

Ведь каждый вызванный объект увеличивает количество памяти, требуемое для его хранения. Если взять больше, то в определенный момент получим ошибку "System. OutOfMemoryException". То есть сначала извлечь все параметры объекта и ненужные объекты, а затем отфильтровать то, что действительно необходимо, — плохая идея.

Использование лишних выборок существенно увеличивает время исполнения скрипта и повышает требования к системным ресурсам. Лучше сразу взять то, что планируется обрабатывать, или выводить дальше. Для примера проверь время исполнения двух команд:

PS> Get-Process | Where ($_.ProcessName -eq "explorer")
PS> Get-Process explorer

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

Теперь ситуация, которая не менее редка в сценариях PowerShell. Есть список объектов, и нужно произвести с ними некоторые действия.
Для этих целей используют командлет ForEach-Object (алиас foreach) или стандартный оператор foreach (поэтому их часто путают). Например, очень часто в скриптах извлекают параметры и присваивают их переменным, которые затем последовательно обрабатывают.

PS> $computers = Get-ADComputer
PS> foreach ($computer in $computers) { что-то делаем }

Этот пример можно переписать несколько иначе:

PS> Get-ADComputer | ForEach-Object
{ что-то делаем }

В первом случае мы вначале присваиваем значение переменной, а затем считываем.
Использование каналов (pipelines, "|") и командлета ForEach-Object во втором примере позволит избежать избыточного хранения большого количества данных, так как они будут обрабатываться сразу, по мере поступления.

В итоге вторая команда выполнится быстрее, а ресурсов потребует меньше. При работе с командлетами ActiveDirectory не забываем импортировать нужный модуль:

PS> import-module ActiveDirectory

Аналогичная ситуация, только не используется явно заданная переменная:

PS> foreach ($computer in GetADComputer) { $computer }

Здесь все равно вначале извлекаются все команды, которые сохраняются в переменной (что загружена в память) и затем последовательно выполняются элементы. Однако время работы команды и затраты ресурсов будут все же на порядок больше, чем при использовании каналов. Но не все так гладко. На простых примерах можно прийти к выводу, что от использования оператора foreach лучше отказаться, на самом деле внутренняя оптимизация PS иногда приводит к тому, что в операциях чтения foreach показывает лучшую производительность. Кроме этого, foreach предпочтителен, если объект уже имеется в памяти, например сохранен в переменной, то есть нет нужды его извлекать, а надо просто обработать. В некоторых случаях необходимо получить некоторые свойства и обработать их дважды, но по-разному, или сохранить в файл и просмотреть в консоли. Можно конечно, вызвать команду дважды (будь внимателен, например Get-Process, вызванный дважды, покажет разный результат), или сохранить значение в переменной. Но в PS есть еще одна интересная возможность: направить вывод в два потока.

Для этой цели используется командлет TeeObject. Например, получаем список процессов, сохраняем в файл и выводем на консоль:

PS> Get-Process | Tee-Object
-filepath C:\process.txt

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

PS> Get-Process | Tee-Object -filepath C:\process.txt |
Sort-Object cpu

Чтобы сохранить второй поток в файл, используем командлет Out-File:

PS> Get-Process | Tee-Object -filepath C:\process.txt |
Sort-Object cpu | Out-File C:\process-sort.txt

В качестве входного параметра командлет Tee-Object может принимать другой объект, на который следует указать при помощи ключа "-inputObject".

 

Читаем файлы

Для чтения или разбора файлов используются командлеты GetContent, Select-String, которые проходят файл построчно и возвращают объект. С файлами большого размера могут быть проблемы, но их легко решить, используя дополнительные параметры. Например, в Get-Content можно указать количество строк, считываемых за раз и передаваемых далее по конвейеру (по умолчанию все). Например, по 100 строк:

PS> Get-Content С:\system.log -Read 100

Соответственно увеличение этого числа ускоряет процесс чтения, но и увеличивает необходимые объемы памяти. Причем при использовании Read, скорее всего, потребуется вставка конвейера "| ForEach-Object ($_) |", чтобы в последующем возможно было обработать всю запись. К слову, команда:

PS> Get-Content biglogfile.log -read 1000 | ForEachObject {$_} | Where {$_ -like '*x*'}

выполнится примерно в 3 раза быстрее, чем:

PS> Get-Content biglogfile.log | Where {$_ -like '*x*'}

Командлет Get-Content лишь читает файлы, остальная обработка отдана на откуп другим командлетам. Например, Select-String может читать файлы или брать данные из канала, отбирая информацию по шаблону. Например, переберем все скрипты PS в текущем каталоге в поисках подстроки «PowerShell»:

PS> Select-String -path *.ps1 -pattern "PowerShell"

Для примера просмотри вывод, казалось бы, подобной команды:

PS> Get-Content -path *.ps1 | where {$_ -match "PowerShell"}

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

Например, вместо того, чтобы искать все сообщения об ошибках, предупреждения (Warning, Failed и т.п.), проще убрать из вывода Success.

При помощи дополнительного параметра Select-String "–notMatch" это сделать проще, а скрипт будет работать быстрее:

PS> Select-String "Success" *.log –notMatch

Одна строчка в выводе часто не дает достаточно информации о событии, здесь на помощь приходит параметр "–context", который позволяет получить необходимое количество строк до и после совпадения. Например, выведем две строки из журналов до и после события со статусом Failed:

PS> Select-String "Failed" *.log -content 2

По умолчанию поиск регистронезависим, чтобы научить Select-String понимать регистр, используем "-caseSensitive".
В некоторых обзорах, в том числе и написанных сертифицированными Microsoft, командлет SelectString часто сравнивается с юниксовыми утилитами grep/egrep. Благо, реализаций grep для Windows сегодня более чем предостаточно:

GnuWin32 (gnuwin32.sf.net), Windows grep (wingrep.com), GNU Grep For Windows (steve.org.uk/Software/grep), два варианта Grep For Windows (grepforwindows.com, pages.interlog.com/~tcharron/grep.html) и многие другие.

По результатам прогонов grep существенно выигрывает по скорости выполнения у Select-String.

> grep Warning *.log

Но при его использовании дальнейшую обработку данных необходимо производить самостоятельно, ведь на выходе мы получаем «сырые» данные, а не объекты .Net. Если же в этом нет необходимости, то вполне достаточно использовать и grep. Напомню, что в Windows есть утилита findstr.exe, позволяющая находить нужные строки в файлах, но ее функционал жутко урезан, поэтому использование grep предпочтительнее.

В контексте чтения файлов стоит вспомнить и о массивах. В PS вообще упрощена работа с переменными, строками, массивами и хеш-таблицами, нужный тип присваивается автоматически (проверяется GetType().

FullName), размер устанавливается динамически. В итоге работать с ними удобно, нет необходимости в дополнительных проверках.
Но при обработке больших объемов данных вроде бы простой скрипт начинает заметно тормозить. Все дело в том, что при добавлении новых элементов в массив он перестраивается, на что опять же нужны время и ресурсы. Поэтому, если размеры массива известны заранее, то его лучше задать сразу, что ускорит в последующем работу с ним:

PS> $arr = New-Object string[] 300

Проверяем параметры массива:

PS> $arr.GetType().Basetype

Для примера код:

$arr = new-object int[] 1000
for ($i=0; $i –lt 1000; $i++)
{$arr[$i] = $i*2}

выполнится более чем в 10 раз быстрее, по сравнению с:

$arr = @()
for ($i=0; $i –lt 1000; $i++)
{$arr += $i*2}

 

Выражаемся регулярно

В PS реализован механизм Perl-подобных регулярных выражений, что позволяет при необходимости легко найти иголку в стоге сена. Если быть точнее, то PS является оболочкой и использует все, что заложено в технологии .NET Framework (класс System.Text.RegularExpressions.Regex). Для поиска совпадения используется параметр "-match" и его варианты "–cmatch" (case-sensitive, регистрозависимый) и "-imatch" (case insensitive, регистронезависимый). Например, нам нужен список IP-адресов, полученных при помощи ipconfig. Проще простого:

PS> ipconfig | where {$_ -match "\d{3,}"}

В качестве параметра в PS принимается регулярное выражение, которое может содержать все принятые знаки — *, ?, +, \w, \s, \d и так далее. Проверим правильность почтового адреса:

PS> $regex = "^[a-z]+\.[a-z]+@synack.ru$"
> If ($email –notmatch $regex) {
> Write-Error "Invalid e-mail address $email"
>}

Теперь, если почтовый адрес не принадлежит домену synack.ru и не попадает под шаблон (то есть содержит запрещенные знаки), то пользователь получит сообщение об ошибке (о командлете Write-Error читай в мини-статье «Форматируем вывод»).

Не буду останавливаться на подробном разборе и перечислении всех возможных параметров, используемых в регулярных выражениях, это достаточно емкая тема (см. статью «Регулярные выражения Perl» www.xakep.ru/post/19474). Кстати, на сегодня доступны специальные утилиты, помогающие составлять регулярное выражение под требуемую оболочку. Например, RegexBuddy (regexbuddy.com/powershell.html) или RegexMagic (regexmagic.com). Кроме поиска совпадения, в PS реализована еще одна ценная возможность — замена содержимого по шаблону, для чего используется оператор «-replace» (а также «-ireplace» и «-creplace»). Шаблон для замены выглядит так:

-replace "шаблон_текста","шаблон_замены"

Например, прочитаем файл и заменим все строки "Warning", на "!!!Warning":

PS> Get-Content -path system.log | foreach {$_ -replace "Warning", "!!!Warning"}

Если второй параметр не указан, совпавшая запись будет просто удалена. Как и в Perl, захваченные в первой части выражения символы сохраняются в специальных переменных, которые могут быть использованы при замене. Так $0 соответствует всему совпавшему тексту, $1 — первое совпадение, $2 — второе и так далее. То есть предыдущее выражение можно переписать так:

PS> Get-Content -path system.log | foreach {$_ -replace "(Warning)", "!!!$0"}

 

Цигиль-цигиль

Теперь, собственно, как и при помощи чего проверять эффективность написанного скрипта. Разработчики Microsoft не стали усложнять нам жизнь, и в PS из коробки включен специальный командлет Measure-Command, позволяющий замерить время выполнения команды или скрипта.

Из командной строки PS реализован прямой доступ к командам оболочки CMD, объектам COM, WMI и .NET, поэтому очень удобно определять разницу во времени исполнения самых разных утилит. Просто вводим запрос в строке приглашения. Для примера произведем два замера:

PS> Measure-Command {ServerManagerCmd -query}
TotalMilliseconds: 7912,7428
PS> Measure-Command {Get-WindowsFeature}
TotalMilliseconds: 1248,9875

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

 

Заключение

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

 

Links

 

Форматируем вывод

При большом количестве данных их визуальный анализ становится затруднительным: например, очень трудно найти сообщения об ошибке во множестве записей. Но в PS довольно просто выделить вывод цветом, что удобнее для восприятия. Для этого используется командлет Write-Host, которому в качестве параметра передаем два параметра:

цвет фона (-Backgroundcolor) и цвет текста (-Foregroundcolor).
PS> Get-Process | Write-Host -foregroundcolor DarkGreen -backgroundcolor white

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

 

Работа с журналами сообщений

После настройки систем и сервисов роль админа сводится к наблюдению за их правильной работой и отслеживанию текущих параметров. В PS заложен целый ряд *-Eventlog командлетов, позволяющих легко считать записи в журнале событий как на локальной, так и удаленной системе. Данные легко сортируются и отбираются по нужным критериям. Например, чтобы вывести только последние события из журнала безопасности на двух компьютерах, используем параметр "Nevest" с указанием числового аргумента:

PS> Get-Eventlog Security -Nevest 20 -computername localhost, synack.ru

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

PS> Get-Eventlog Security -Message "*failed*"

А вот так можно собрать все данные об успешной регистрации пользователей (события с EventID=4624):

PS> Get-Eventlog Security | Where-Object {$_.EventID -eq 4624}

 

Логокопатель Windows

В PS v2.0 CTP3 появился командлет Get-WinEvent, который в некоторых случаях предоставляет более удобный формат доступа к данным. Получим список провайдеров, отвечающих за обновления:

PS> Get-WinEvent -ListProvider *update*

Microsoft-Windows-WindowsUpdateClient {System, Microsoft-Windows-WindowsUpdateClient/Operational}

В зависимости от установленных ролей и компонентов, список будет разным, но нас интересует провайдер для Windows Update. Теперь смотрим установленные обновления:

PS> $provider = Get-WinEvent -ListProvider
Microsoft-Windows-WindowsUpdateClient
PS> $provider.events | ? {$_.description -match "success"} | select id,description | ft -AutoSize

В итоге мы можем достаточно просто получить любую информацию о состоянии системы.

Оставить мнение

Check Also

Espruino Pico. Учимся программировать USB-микроконтроллер на JavaScript и делаем из него токен авторизации

Несмотря на огромное количество устройств на базе микроконтроллеров, созданных на волне ус…