Советы по оптимизации команд и скриптов PowerShell 2.0
Сергей «grinder» Яремчук (grinder@synack.ru)
PowerShell — очень удобный инструмент администратора, предоставляющий практически безграничные возможности по настройке серверов, виртуальных машин, а также сбору информации об их состоянии. Он достаточно прост, чтобы быстро писать скрипты, не вникая в детали. Но как и любой язык программирования, PS имеет свои тонкости и нюансы, не зная о которых, нельзя эффективно его использовать.
Содержание:
- Берем только нужное
- Читаем файлы
- Выражаемся регулярно
- Цигиль-цигиль
- Заключение
- Мини-статья: Форматируем вывод
- Врезка: Работа с журналами сообщений
- Врезка: Логокопатель Windows
- Боковые выносы
Командная оболочка оперирует множеством параметров объектов, к отбору которых необходимо подходить взвешенно, определяясь с их дальнейшей необходимостью. Ведь каждый вызванный объект увеличивает количество памяти, требуемое для его хранения. Если взять больше, то в определенный момент получим ошибку "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 Get-ADComputer) { $computer }
Здесь все равно вначале извлекаются все команды, которые сохраняются в переменной (что загружена в память) и затем последовательно выполняются элементы. Однако время работы команды и затраты ресурсов будут все же на порядок больше, чем при использовании каналов.
Но не все так гладко. На простых примерах можно прийти к выводу, что от использования оператора foreach лучше отказаться, на самом деле, внутренняя оптимизация PS иногда приводит к тому, что в операциях чтения foreach показывает лучшую производительность. Кроме этого, foreach предпочтителен, если объект уже имеется в памяти, например сохранен в переменной, то есть его не нужно извлекать, а просто обработать.
В некоторых случаях необходимо получить некоторые свойства и обработать их дважды, но по-разному, или сохранить в файл и просмотреть в консоли. Можно, конечно, вызвать команду дважды (будь внимателен, например Get-Process, вызванный дважды, покажет разный результат) или сохранить значение в переменной. Но в PS есть еще одна интересная возможность: направить вывод в два потока. Для этой цели используется командлет Tee-Object. Например, получим список процессов, сохраним в файл и выведем на консоль:
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".
Для чтения или разбора файлов используются командлеты Get-Content, Select-String, которые проходят файл построчно и возвращают объект. С файлами большого размера могут быть проблемы, но, используя дополнительные параметры, их легко решить. Например, в Get-Content можно указать количество строк, считываемых за раз и передаваемых далее по конвейеру (по умолчанию все). Например, по 100 строк:
PS> Get-Content С:\system.log -Read 100
Соответственно увеличение этого числа ускоряет процесс чтения, но и увеличивает необходимые объемы памяти. Причем при использовании Read, скорее всего, потребуется вставка конвейера "| ForEach-Object ($_) |", чтобы впоследствии можно было обработать всю запись. К слову, команда:
PS> Get-Content biglogfile.log -read 1000 | ForEach-Object {$_} | \
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, командлет Select-String часто сравнивается с юниксовыми утилитами grep/egrep. Благо, реализаций grep для Windows сегодня более чем предостаточно: , , , два варианта Grep For Windows (, ) и многие другие. По результатам прогонов 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 читай в мини-статье «Форматируем вывод»).
Не буду останавливаться на подробном разборе и перечислении всех возможных параметров, используемых в регулярных выражениях, это достаточно емкая тема (см. статью ""). Кстати, на сегодня доступны специальные утилиты, помогающие составлять регулярное выражение под требуемую оболочку. Например, или .
Кроме поиска совпадения, в 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 сделан очень удобным и функциональным, на нем просто писать скрипты, выбирать и форматировать данные. Но за удобство приходится расплачиваться медлительностью написанных скриптов. Надеюсь, приведенные советы по оптимизации помогут тебе в изучении этой оболочки.
Мини-статья: Форматируем вывод
При большом количестве данных их визуальный анализ становится затруднителен, например, очень трудно найти сообщения об ошибке во множестве записей. Но в PS довольно просто выделить вывод цветом, сделав его удобнее для восприятия. Для этого используется командлет Write-Host, которому в качестве параметра передаем два параметра: цвет фона (-Backgroundcolor) и цвет текста (-Foregroundcolor).
PS> Get-Process | Write-Host -foregroundcolor DarkGreen -backgroundcolor white
В результате получим список процессов на белом фоне зелеными буквами. К сожалению, Write-Host никак не различает вывод других утилит. То есть, если в выводе другого командлета присутствует, например, Error, о цветовой раскраске такого сообщения необходимо позаботиться самостоятельно.
PS> if ($a = "Error"){Write-Host $a -foregroundcolor red}
> else
> {Write-Host $a}
> }
Кроме этого, существуют командлеты для определенного типа вывода, позволяющие «привлечь внимание»: Write-Warning и Write-Error.
PS> Write-Error "Access denied"
Вывод многих команд просто огромен, чтобы последовательно просмотреть все страницы, не прибегая к скроллингу, можно применить командлет Out-Host с параметром «-paging» и спокойно изучать листинг:
PS> Get-Process | Out-Host -paging
Кроме этого, есть и ряд других полезных командлетов: Clear-Host (очистка экрана), Write-Progress (вывод статусбара), Sort-Object (сортировка вывода).
PS> Get-Process | Sort-Object cpu
Теперь мы сразу получим список процессов, отсортированных по проценту использования CPU, и не будем отбирать их вручную. При необходимости обратной сортировки используем параметр "-Descending".
Врезка: Работа с журналами сообщений
После настройки систем и сервисов роль админа сводится к наблюдению за их правильной работой и отслеживанию текущих параметров. В 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}
В 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
В итоге мы можем достаточно просто получить любую информацию по состоянию системы.
INFO
-
Введение в PowerShell — статья «Капитан PowerShell и администрирование будущего» в сентябрьском номере за 2009 год.
-
Удаленное управление при помощи PowerShell рассмотрено в статье «Незримое присутствие» в ][_03_2010.
-
При наборе команд не забывай об автодополнении (клавиша <Tab>), а также возможности копирования и вставки правой кнопкой мышки.
WWW
-
Официальные ресурсы, посвященные PowerShell — ,
-
Специализированные ресурсы: , , , .
- Пакет Unix утилит для Windows —
- Статья «Регулярные выражения Perl» —
Статья опубликована в майском номере журнала «Xakep» за 2010 год.





