В Powershell существует несколько уровней ошибок и несколько способов их обработать. Проблемы одного уровня (Non-Terminating Errors) можно решить с помощью привычных для Powershell команд. Другой уровень ошибок (Terminating Errors) решается с помощью исключений (Exceptions) стандартного, для большинства языков, блока в виде Try, Catch и Finally.
Как Powershell обрабатывает ошибки
До рассмотрения основных методов посмотрим на теоретическую часть.
Автоматические переменные $Error
В Powershell существует множество переменных, которые создаются автоматически. Одна из таких переменных — $Error хранит в себе все ошибки за текущий сеанс PS. Например так я выведу количество ошибок и их сообщение за весь сеанс:
Get-TestTest
$Error
$Error.Count
При отсутствии каких либо ошибок мы бы получили пустой ответ, а счетчик будет равняться 0:
Переменная $Error являет массивом и мы можем по нему пройтись или обратиться по индексу что бы найти нужную ошибку:
$Error[0]
foreach ($item in $Error){$item}
Свойства объекта $Error
Так же как и все что создается в Powershell переменная $Error так же имеет свойства (дополнительную информацию) и методы. Названия свойств и методов можно увидеть через команду Get-Member:
$Error | Get-Member
Например, с помощью свойства InvocationInfo, мы можем вывести более структурный отчет об ошибки:
$Error[0].InvocationInfo
Методы объекта $Error
Например мы можем очистить логи ошибок используя clear:
$Error.clear()
Критические ошибки (Terminating Errors)
Критические (завершающие) ошибки останавливают работу скрипта. Например это может быть ошибка в названии командлета или параметра. В следующем примере команда должна была бы вернуть процессы «svchost» дважды, но из-за использования несуществующего параметра ‘—Error’ не выполнится вообще:
'svchost','svchost' | % {Get-Process -Name $PSItem} --Error
Не критические ошибки (Non-Terminating Errors)
Не критические (не завершающие) ошибки не остановят работу скрипта полностью, но могут вывести сообщение об этом. Это могут быть ошибки не в самих командлетах Powershell, а в значениях, которые вы используете. На предыдущем примере мы можем допустить опечатку в названии процессов, но команда все равно продолжит работу:
'svchost111','svchost' | % {Get-Process -Name $PSItem}
Как видно у нас появилась информация о проблеме с первым процессом ‘svchost111’, так как его не существует. Обычный процесс ‘svchost’ он у нас вывелся корректно.
Параметр ErrorVariable
Если вы не хотите использовать автоматическую переменную $Error, то сможете определять свою переменную индивидуально для каждой команды. Эта переменная определяется в параметре ErrorVariable:
'svchost111','svchost' | % {Get-Process -Name $PSItem } -ErrorVariable my_err_var
$my_err_var
Переменная будет иметь те же свойства, что и автоматическая:
$my_err_var.InvocationInfo
Обработка некритических ошибок
У нас есть два способа определения последующих действий при ‘Non-Terminating Errors’. Это правило можно задать локально и глобально (в рамках сессии). Мы сможем полностью остановить работу скрипта или вообще отменить вывод ошибок.
Приоритет ошибок с $ErrorActionPreference
Еще одна встроенная переменная в Powershell $ErrorActionPreference глобально определяет что должно случится, если у нас появится обычная ошибка. По умолчанию это значение равно ‘Continue’, что значит «вывести информацию об ошибке и продолжить работу»:
$ErrorActionPreference
Если мы поменяем значение этой переменной на ‘Stop’, то поведение скриптов и команд будет аналогично критичным ошибкам. Вы можете убедиться в этом на прошлом скрипте с неверным именем процесса:
$ErrorActionPreference = 'Stop'
'svchost111','svchost' | % {Get-Process -Name $PSItem}
Т.е. скрипт был остановлен в самом начале. Значение переменной будет храниться до момента завершения сессии Powershell. При перезагрузке компьютера, например, вернется значение по умолчанию.
Ниже значение, которые мы можем установить в переменной $ErrorActionPreference:
- Continue — вывод ошибки и продолжение работы;
- Inquire — приостановит работу скрипта и спросит о дальнейших действиях;
- SilentlyContinue — скрипт продолжит свою работу без вывода ошибок;
- Stop — остановка скрипта при первой ошибке.
Самый частый параметр, который мне приходится использовать — SilentlyContinue:
$ErrorActionPreference = 'SilentlyContinue'
'svchost111','svchost' | % {Get-Process -Name $PSItem}
Использование параметра ErrorAction
Переменная $ErrorActionPreference указывает глобальный приоритет, но мы можем определить такую логику в рамках команды с параметром ErrorAction. Этот параметр имеет больший приоритет чем $ErrorActionPreference. В следующем примере, глобальная переменная определяет полную остановку скрипта, а в параметр ErrorAction говорит «не выводить ошибок и продолжить работу»:
$ErrorActionPreference = 'Stop'
'svchost111','svchost' | % {Get-Process -Name $PSItem -ErrorAction 'SilentlyContinue'}
Кроме ‘SilentlyContinue’ мы можем указывать те же параметры, что и в переменной $ErrorActionPreference.
Значение Stop, в обоих случаях, делает ошибку критической.
Обработка критических ошибок и исключений с Try, Catch и Finally
Когда мы ожидаем получить какую-то ошибку и добавить логику нужно использовать Try и Catch. Например, если в вариантах выше мы определяли нужно ли нам отображать ошибку или останавливать скрипт, то теперь сможем изменить выполнение скрипта или команды вообще. Блок Try и Catch работает только с критическими ошибками и в случаях если $ErrorActionPreference или ErrorAction имеют значение Stop.
Например, если с помощью Powershell мы пытаемся подключиться к множеству компьютеров один из них может быть выключен — это приведет к ошибке. Так как эту ситуацию мы можем предвидеть, то мы можем обработать ее. Процесс обработки ошибок называется исключением (Exception).
Синтаксис и логика работы команды следующая:
try {
# Пытаемся подключиться к компьютеру
}
catch [Имя исключения 1],[Имя исключения 2]{
# Раз компьютер не доступен, сделать то-то
}
finally {
# Блок, который выполняется в любом случае последним
}
Блок try мониторит ошибки и если она произойдет, то она добавится в переменную $Error и скрипт перейдет к блоку Catch. Так как ошибки могут быть разные (нет доступа, нет сети, блокирует правило фаервола и т.д.) то мы можем прописывать один блок Try и несколько Catch:
try {
# Пытаемся подключится
}
catch ['Нет сети']['Блокирует фаервол']{
# Записываем в файл
}
catch ['Нет прав на подключение']{
# Подключаемся под другим пользователем
}
Сам блок finally — не обязательный и используется редко. Он выполняется самым последним, после try и catch и не имеет каких-то условий.
Catch для всех типов исключений
Как и было показано выше мы можем использовать блок Catch для конкретного типа ошибок, например при проблемах с доступом. Если в этом месте ничего не указывать — в этом блоке будут обрабатываться все варианты ошибок:
try {
'svchost111','svchost' | % {Get-Process -Name $PSItem -ErrorAction 'Stop'}
}
catch {
Write-Host "Какая-то неисправность" -ForegroundColor RED
}
Такой подход не рекомендуется использовать часто, так как вы можете пропустить что-то важное.
Мы можем вывести в блоке catch текст ошибки используя $PSItem.Exception:
try {
'svchost111','svchost' | % {Get-Process -Name $PSItem -ErrorAction 'Stop'}
}
catch {
Write-Host "Какая-то неисправность" -ForegroundColor RED
$PSItem.Exception
}
Переменная $PSItem хранит информацию о текущей ошибке, а глобальная переменная $Error будет хранит информацию обо всех ошибках. Так, например, я выведу одну и ту же информацию:
$Error[0].Exception
Создание отдельных исключений
Что бы обработать отдельную ошибку сначала нужно найти ее имя. Это имя можно увидеть при получении свойств и методов у значения переменной $Error:
$Error[0].Exception | Get-Member
Так же сработает и в блоке Catch с $PSItem:
Для вывода только имени можно использовать свойство FullName:
$Error[0].Exception.GetType().FullName
Далее, это имя, мы вставляем в блок Catch:
try {
'svchost111','svchost' | % {Get-Process -Name $PSItem -ErrorAction 'Stop'}
}
catch [Microsoft.PowerShell.Commands.ProcessCommandException]{
Write-Host "Произошла ошибка" -ForegroundColor RED
$PSItem.Exception
}
Так же, как и было описано выше мы можем усложнять эти блоки как угодно указывая множество исключений в одном catch.
Выброс своих исключений
Иногда нужно создать свои собственные исключения. Например мы можем запретить добавлять через какой-то скрипт названия содержащие маленькие буквы или сотрудников без указания возраста и т.д. Способов создать такие ошибки — два и они тоже делятся на критические и обычные.
Выброс с throw
Throw — выбрасывает ошибку, которая останавливает работу скрипта. Этот тип ошибок относится к критическим. Например мы можем указать только текст для дополнительной информации:
$name = 'AD.1'
if ($name -match '.'){
throw 'Запрещено использовать точки в названиях'
}
Если нужно, то мы можем использовать исключения, которые уже были созданы в Powershell:
$name = 'AD.1'
if ($name -like '*.*'){
throw [System.IO.FileNotFoundException]'Запрещено использовать точки в названиях'
}
Использование Write-Error
Команда Write-Error работает так же, как и ключ ErrorAction. Мы можем просто отобразить какую-то ошибку и продолжить выполнение скрипта:
$names = @('CL1', 'AD.1', 'CL3')
foreach ($name in $names){
if ($name -like '*.*'){
Write-Error -Message 'Обычная ошибка'
}
else{
$name
}
}
При необходимости мы можем использовать параметр ErrorAction. Значения этого параметра были описаны выше. Мы можем указать значение ‘Stop’, что полностью остановит выполнение скрипта:
$names = @('CL1', 'AD.1', 'CL3')
foreach ($name in $names){
if ($name -like '*.*'){
Write-Error -Message 'Обычная ошибка' -ErrorAction 'Stop'
}
else{
$name
}
}
Отличие команды Write-Error с ключом ErrorAction от обычных команд в том, что мы можем указывать исключения в параметре Exception:
Write-Error -Message 'Обычная ошибка' -ErrorAction 'Stop'
Write-Error -Message 'Исключение' -Exception [System.IO.FileNotFoundException] -ErrorAction 'Stop'
В Exception мы так же можем указывать сообщение. При этом оно будет отображаться в переменной $Error:
Write-Error -Exception [System.IO.FileNotFoundException]'Моё сообщение'
…
Теги:
#powershell
#ошибки
Вообще я редко вижу смысл в том чтобы отлавливать ошибки в скриптах, но недавно ко мне попалась задача, где необходимо было обработать ошибки в скрипте PowerShell. Дело в том что данный скрипт использовался как часть работы System Center Orchestrator. Для этого я использовал Try/Catch/Finaly . Но все по порядку.
Немного про ошибки
Ошибки можно условно разделить на две больших категории.
- Прерывающие
- Не прерывающие
Первые завершают выполнение конвейера с ошибкой. Т.е. дальнейшие команды в конвейере не выполняются. Например, вы не правильно указали имя команды. Вторая категория лишь генерирует ошибку, но конвейер продолжает выполняться далее. Например, вы запросили содержимое не существующего каталога.
В любом случае всю информацию о всех ошибках можно поглядеть в переменной $error. Последняя ошибка идет с индексом 0, т.е. $error[0] — покажет последнюю ошибку. А $error[0].Exception описание последней ошибки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
PS C:> Get-Command NoCommand,Dir Get-Command : Имя «NoCommand» не распознано как имя командлета, функции, файла сценария или выполняемой программы. Пров ерьте правильность написания имени, а также наличие и правильность пути, после чего повторите попытку. строка:1 знак:1 + Get-Command NoCommand,Dir + ~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : ObjectNotFound: (NoCommand:String) [Get-Command], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand CommandType Name ModuleName —————— —— ————— Alias dir -> Get-ChildItem PS C:> $error[0] Get-Command : Имя «NoCommand» не распознано как имя командлета, функции, файла сценария или выполняемой программы. Пров ерьте правильность написания имени, а также наличие и правильность пути, после чего повторите попытку. строка:1 знак:1 + Get-Command NoCommand,Dir + ~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : ObjectNotFound: (NoCommand:String) [Get-Command], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand PS C:> $error[0].Exception Имя «NoCommand» не распознано как имя командлета, функции, файла сценария или выполняемой программы. Проверьте правильн ость написания имени, а также наличие и правильность пути, после чего повторите попытку. |
Этот же пример, но в виде рисунка для наглядности.
Для того чтобы выяснить была ли в какой-то части кода ошибка необходимо использовать Try. Это помогает избавиться от неожиданного и некорректного завершение вашего скрипта. Позволяя так же корректно завершить его работу.
Синтаксис выглядит в общем случае так.
Try { часть кода в которой ищем ошибку } Catch { [тип ошибки, которую ищем] код, который будет выполнен когда ошибка будет найдена } Finally { код, который будет выполнен в любом случае } |
Однако использование Finally и определение типа ошибки — опционально и может не использоваться.
Для проверки напишем вот такой код где используем Try в PowerShell для того чтобы обработать ошибку деления на ноль и вывести информацию об ошибке.
try { [float](4/0) } catch { Write-Host «Error!!!» Write-Host $error[0].Exception } |
Как видно я произвожу деление на ноль в блоке try, т.е. совершаю прерывающую ошибку. Только прерывающие ошибку будут отловлены. А далее в блоке catch произвожу уже необходимые отладочные действия, т.е. произвожу обработку ошибки.. В моей исходной задаче я в данном блоке заносил информацию об ошибках в специальный лог файл, но давайте попробуем запустить мой данный пример.
PS C:> .test.ps1 Error!!! System.Management.Automation.RuntimeException: Попытка деления на нуль. —-> System.DivideByZeroException: Попытка делен ия на нуль. —— Конец трассировки внутреннего стека исключений —— в System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exce ption) в System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame) в System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame) в System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame) |
Увы в блоке try в PowerShell должна присутствовать прерывающая ошибка.
Преобразуем не прерывающую ошибку в прерывающую
Существует общий параметр для всех командлетов в PowerShell -ErrorAction. Данный параметр может принимать четыре значения
- Continue — выводит ошибку и продолжает выполнение
- SilentlyContinue — не выводит ошибку и продолжает выполнение
- Stop — завершает выполнение
- Inquire — спрашивает у пользователя как поступить
В нашем случае подходит действие stop. Если использовать параметр со значением -ErrorAction Stop, то при возникновении ошибки при выполнении команды, данная ошибка прерывает выполнение команды. Ниже пример скрипта, использующего -ErrorAction Stop.
try { Dir NoFolder -ErrorAction Stop } catch { Write-Host «Error!!!» Write-Host $error[0].Exception } |
Ниже результат выполнения данного скрипта
PS C:> .test.ps1 Error!!! System.Management.Automation.ItemNotFoundException: Не удается найти путь «C:NoFolder», так как он не существует. в System.Management.Automation.SessionStateInternal.GetChildItems(String path, Boolean recurse, C mdletProviderContext context) в Microsoft.PowerShell.Commands.GetChildItemCommand.ProcessRecord() |
В моей исходной задаче try в PowerShell необходимо было использовать для всех команд в скрипте. Поскольку обработка ошибки была общей для любой ошибки в скрипте весь скрипт можно поместить в один Try, конечно это не совсем правильно, но зато просто. Чтобы каждый раз не писать -ErrorAction Stop. Можно воспользоваться переменной $ErrorActionPreference, которая имеет те же значения и сходна по действию, однако распространяет свое действие на все командлеты в скрипте.
$ErrorActionPreference = «stop» try { Dir Folder Get-Process -ComputerName TestPC } catch { Write-Host «Error!!!» Write-Host $error[0].Exception } |
Вместо заключения
Конечно, по началу вы мало будете задумываться об поиске ошибок, используя множество условных конструкций вы их минимизируете. Однако использование try в PowerShell позволит минимизировать случаи неожиданного завершения скрипта.
description | Locale | ms.date | online version | schema | title |
---|---|---|---|---|---|
Describes how to use the `try`, `catch`, and `finally` blocks to handle terminating errors. |
en-US |
11/12/2021 |
https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_try_catch_finally?view=powershell-7.2&WT.mc_id=ps-gethelp |
2.0.0 |
about Try Catch Finally |
Short description
Describes how to use the try
, catch
, and finally
blocks to handle
terminating errors.
Long description
Use try
, catch
, and finally
blocks to respond to or handle terminating
errors in scripts. The Trap
statement can also be used to handle terminating
errors in scripts. For more information, see about_Trap.
A terminating error stops a statement from running. If PowerShell does not
handle a terminating error in some way, PowerShell also stops running the
function or script using the current pipeline. In other languages, such as C#,
terminating errors are referred to as exceptions.
Use the try
block to define a section of a script in which you want
PowerShell to monitor for errors. When an error occurs within the try
block,
the error is first saved to the $Error
automatic variable. PowerShell then
searches for a catch
block to handle the error. If the try
statement does
not have a matching catch
block, PowerShell continues to search for an
appropriate catch
block or Trap
statement in the parent scopes. After a
catch
block is completed or if no appropriate catch
block or Trap
statement is found, the finally
block is run. If the error cannot be handled,
the error is written to the error stream.
A catch
block can include commands for tracking the error or for recovering
the expected flow of the script. A catch
block can specify which error types
it catches. A try
statement can include multiple catch
blocks for different
kinds of errors.
A finally
block can be used to free any resources that are no longer needed
by your script.
try
, catch
, and finally
resemble the try
, catch
, and finally
keywords used in the C# programming language.
Syntax
A try
statement contains a try
block, zero or more catch
blocks, and zero
or one finally
block. A try
statement must have at least one catch
block
or one finally
block.
The following shows the try
block syntax:
The try
keyword is followed by a statement list in braces. If a terminating
error occurs while the statements in the statement list are being run, the
script passes the error object from the try
block to an appropriate catch
block.
The following shows the catch
block syntax:
catch [[<error type>][',' <error type>]*] {<statement list>}
Error types appear in brackets. The outermost brackets indicate the element is
optional.
The catch
keyword is followed by an optional list of error type
specifications and a statement list. If a terminating error occurs in the
try
block, PowerShell searches for an appropriate catch
block. If
one is found, the statements in the catch
block are executed.
The catch
block can specify one or more error types. An error type is a
Microsoft .NET Framework exception or an exception that is derived from a .NET
Framework exception. A catch
block handles errors of the specified .NET
Framework exception class or of any class that derives from the specified
class.
If a catch
block specifies an error type, that catch
block handles that
type of error. If a catch
block does not specify an error type, that catch
block handles any error encountered in the try
block. A try
statement can
include multiple catch
blocks for the different specified error types.
The following shows the finally
block syntax:
finally {<statement list>}
The finally
keyword is followed by a statement list that runs every time the
script is run, even if the try
statement ran without error or an error was
caught in a catch
statement.
Note that pressing CTRL+C stops the pipeline. Objects
that are sent to the pipeline will not be displayed as output. Therefore, if
you include a statement to be displayed, such as «Finally block has run», it
will not be displayed after you press CTRL+C, even if the
finally
block ran.
Catching errors
The following sample script shows a try
block with a catch
block:
try { NonsenseString } catch { "An error occurred." }
The catch
keyword must immediately follow the try
block or another catch
block.
PowerShell does not recognize «NonsenseString» as a cmdlet or other item.
Running this script returns the following result:
When the script encounters «NonsenseString», it causes a terminating error. The
catch
block handles the error by running the statement list inside the block.
Using multiple catch statements
A try
statement can have any number of catch
blocks. For example, the
following script has a try
block that downloads MyDoc.doc
, and it contains
two catch
blocks:
try { $wc = new-object System.Net.WebClient $wc.DownloadFile("http://www.contoso.com/MyDoc.doc","c:tempMyDoc.doc") } catch [System.Net.WebException],[System.IO.IOException] { "Unable to download MyDoc.doc from http://www.contoso.com." } catch { "An error occurred that could not be resolved." }
The first catch
block handles errors of the System.Net.WebException and
System.IO.IOException types. The second catch
block does not specify an
error type. The second catch
block handles any other terminating errors that
occur.
PowerShell matches error types by inheritance. A catch
block handles errors
of the specified .NET Framework exception class or of any class that derives
from the specified class. The following example contains a catch
block that
catches a «Command Not Found» error:
catch [System.Management.Automation.CommandNotFoundException] {"Inherited Exception" }
The specified error type, CommandNotFoundException, inherits from the
System.SystemException type. The following example also catches a Command
Not Found error:
catch [System.SystemException] {"Base Exception" }
This catch
block handles the «Command Not Found» error and other errors that
inherit from the SystemException type.
If you specify an error class and one of its derived classes, place the catch
block for the derived class before the catch
block for the general class.
[!NOTE]
PowerShell wraps all exceptions in a RuntimeException type. Therefore,
specifying the error type System.Management.Automation.RuntimeException
behaves the same as an unqualified catch block.
Using Traps in a Try Catch
When a terminating error occurs in a try
block with a Trap
defined within
the try
block, even if there is a matching catch
block, the Trap
statement
takes control.
If a Trap
exists at a higher block than the try
, and there is no matching
catch
block within the current scope, the Trap
will take control, even if
any parent scope has a matching catch
block.
Accessing exception information
Within a catch
block, the current error can be accessed using $_
, which
is also known as $PSItem
. The object is of type ErrorRecord.
try { NonsenseString } catch { Write-Host "An error occurred:" Write-Host $_ }
Running this script returns the following result:
An Error occurred:
The term 'NonsenseString' is not recognized as the name of a cmdlet, function,
script file, or operable program. Check the spelling of the name, or if a path
was included, verify that the path is correct and try again.
There are additional properties that can be accessed, such as ScriptStackTrace,
Exception, and ErrorDetails. For example, if we change the script to the
following:
try { NonsenseString } catch { Write-Host "An error occurred:" Write-Host $_.ScriptStackTrace }
The result will be similar to:
An Error occurred:
at <ScriptBlock>, <No file>: line 2
Freeing resources using finally
To free resources used by a script, add a finally
block after the try
and
catch
blocks. The finally
block statements run regardless of whether the
try
block encounters a terminating error. PowerShell runs the finally
block
before the script terminates or before the current block goes out of scope.
A finally
block runs even if you use CTRL+C to stop the
script. A finally
block also runs if an Exit keyword stops the script from
within a catch
block.
See also
- about_Break
- about_Continue
- about_Scopes
- about_Throw
- about_Trap
Продолжаем тему обработки ошибок в PowerShell, начатую в предыдущей статье. Сегодня речь пойдет об обработке прерывающих ошибок (исключений). Надо понимать, что сообщение об ошибке — это не то же самое, что исключение. Как вы помните, в PowerShell есть два типа ошибок — непрерывающие и прерывающие.
Непрерывающие ошибки позволяют продолжить работу, тогда как прерывающие останавливают выполнение команды. Остановка приводит к исключению (exception), которое и можно отлавливать и обрабатывать.
Примечание. Таким же образом можно обрабатывать и непрерывающие ошибки. Изменение параметра ErrorAction на Stop прервет выполнение команды и произведет исключение, которое можно уловить.
Для наглядности сгенерируем ошибку, попытавшись прочитать файл, на который у нас нет прав. А теперь обратимся к переменной $Error и выведем данные об исключении. Как видите, данное исключение имеет тип UnauthorizedAccessException и относится к базовому типу System.SystemException.
Для обработки исключений в PowerShell есть несколько способов, которые мы сегодня и рассмотрим.
Try/Catch/Finally
Конструкция Try/Catch/Finally предназначена для обработки исключений, возникающих в процессе выполнения скрипта. В блоке Try располагается исполняемый код, в котором должны отслеживаться ошибки. При возникновении в блоке Try прерывающей ошибки оболочка PowerShell ищет соответствующий блок Catch для обработки этой ошибки, и если он найден, то выполняет находящиеся в нем инструкции. Блок Catch может включать в себя любые команды, необходимые для обработки возникнувшей ошибки иили восстановления дальнейшей работы скрипта.
Блок Finally располагается обычно в конце скрипта. Команды, находящиеся в нем, выполняются в любом случае, независимо от возникновения ошибок. Была ли ошибка перехвачена и обработана блоком Catch или при выполнении скрипта ошибок не возникало вообще, блок Finally будет выполнен. Присутствие этого блока в скрипте необязательно, основная его задача — высвобождение ресурсов (закрытие процессов, очистка памяти и т.п.).
В качестве примера в блок Try поместим код, который читает файлы из указанной директории. При возникновении проблем блок Catch выводит сообщение об ошибке, после чего отрабатывает блок Finally и работа скрипта завершается:
try {
Get-Content -Path ″C:Files*″ -ErrorAction Stop
}
catch {
Write-Host ″Some error was found.″
}
finally {
Write-Host ″Finish.″
}
Для блока Catch можно указать конкретный тип ошибки, добавив после ключевого слова Catch в квадратных скобках название исключения. Так в следующем примере укажем в качестве типа исключение System.UnauthorizedAccessException, которое возникает при отсутствии необходимых прав доступа к объекту:
try {
Get-Content -Path ″C:Files*″ -ErrorAction Stop
}
catch [System.UnauthorizedAccessException]
{
Write-Host ″File is not accessible.″
}
finally {
Write-Host ″Finish.″
}
Если для блока Catch указан тип ошибки, то этот блок будет обрабатывать только этот тип ошибок, или тип ошибок, наследуемый от указанного типа. При возникновении другого типа ошибка не будет обработана. Если же тип не указан, то блок будет обрабатывать любые типы ошибок, возникающие в блоке Try.
Блок Try может включать несколько блоков Catch, для разных типов ошибок, соответственно можно для каждого типа ошибки задать свой обработчик. Например, возьмем два блока Catch, один для ошибки при отсутствии прав доступа, а второй — для любых других ошибок, которые могут возникнуть в процессе выполнения скрипта:
try {
Get-Content -Path ″C:Files*″ -ErrorAction Stop
}
catch [System.UnauthorizedAccessException]
{
Write-Host ″File is not accessible.″
}
catch {
Write-Host ″Other type of error was found:″
Write-Host ″Exception type is $($_.Exception.GetType().Name)″
}
finally {
Write-Host ″Finish.″
}
Trap
Еще один способ обработки ошибок — это установка ловушки (trap). В этом варианте обработчик ошибок задается с помощью ключевого слова Trap, определяющего список команд, которые должны выполниться при возникновении прерывающей ошибки и исключения. Когда происходит исключение, то оболочка PowerShell ищет в коде инструкции Trap для данной ошибки, и если находит, то выполняет их.
Возьмем наиболее простой пример ловушки, которая обрабатывает все произошедшие исключения и выводит сообщение об ошибке:
trap {
Write-Host ″Error was found.″
}
Get-Content -Path C:File* -ErrorAction Stop
Так же, как и для trycatch, ключевое слово Trap позволяет задавать тип исключения, указав его в квадратных скобках. К примеру, можно задать в скрипте несколько ловушек, одну нацелив на конкретную ошибку, а вторую на обработку оставшихся:
trap [System.Management.Automation.ItemNotFoundException]
{
Write-Host ″File is not accessible.″
break
}
trap {
Write-Host ″Other error was found.″
continue
}
Get-Content -Path C:File* -ErrorAction Stop
Вместе с Trap можно использовать ключевые слова Break и Continue, которые позволяют определить, должен ли скрипт продолжать выполняться после возникновения прерывающей ошибки. По умолчанию при возникновении исключения выполняются команды, входящие в блок Trap, выводится информация об ошибке, после чего выполнение скрипта продолжается со строки, вызвавшей ошибку:
trap {
Write-Host ″Error was found.″
}
Write-Host ″Before error.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error.″
Если использовать ключевое слово Break, то при возникновении ошибки будет выполнены команды в блоке Trap, после чего выполнение скрипта будет прервано:
trap {
Write-Host ″Error was found.″
break
}
Write-Host ″Before error.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error.″
Как видно из примера, команда, следующая за исключением, не отработала и сообщение ″After error.″ выведено не было.
Если же в Trap включено ключевое слово Continue, то выполнение скрипта будет продолжено, так же как и в случае по умолчанию. Единственное отличие в том, что с Continue ошибка не записывается в поток Error и не выводится на экран.
trap {
Write-Host ″Error was found.″
continue
}
Write-Host ″Before error.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error.″
Область действия
При отлове ошибок с помощью Trap надо помнить о такой вещи, как область действия (scope). Оболочка PowerShell является глобальной областью, в которую входят все процессы, запущенный скрипт получает собственную область, а если внутри скрипта определены функции, то внутри каждой определена своя, частная область действия. Это создает своего рода родительско-дочернюю иерархию.
При появлении исключения оболочка ищет ловушку в текущей области. Если в ней есть ловушка, она выполняется, если нет — то производится выход в вышестоящую область и поиск ловушки там. Когда ловушка находится, то происходит ее выполнение. Затем, если ловушка предусматривает продолжение работы, то оболочка возобновляет выполнение кода, начиная с той строки, которая вызвала исключение, но при этом оставаясь в той же области, не возвращаясь обратно.
Для примера возьмем функцию Content, внутри которой будет выполняться наш код. Область действия внутри функции назовем scope 1, а снаружи scope 2:
trap {
Write-Host ″Error was found.″
continue
}
function Content {
Write-Host ″Before error, scope1.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error, scope 1.″
}
Content
Write-Host ″After error, scope 2.″
При возникновении ошибки в функции Content оболочка будет искать ловушку внутри нее. Затем, не найдя ловушку внутри функции, оболочка выйдет из текущей области и будет искать в родительской области. Ловушка там есть, поэтому будет выполнена обработка ошибки, после чего Continue возобновит выполнение скрипта, но уже в родительской области (scope 2), не возвращаясь обратно в функцию (scope 1).
А теперь немного изменим скрипт, поместив ловушку внутрь функции:
function Content {
trap {
Write-Host ″Error was found.″
continue
}
Write-Host ″Before error, scope 1.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error, scope 1.″
}
Content
Write-Host ″After error, scope 2.″
Как видите, теперь при возникновении ошибки внутри функции будет произведена обработка ошибки, после чего выполнение будет продолжено с той строки, в которой произошла ошибка — не выходя из функции.
Заключение
Если сравнить Try/Catch и Trap, то у каждого метода есть свои достоинства и недостатки. Конструкцию Try/Catch можно более точно нацелить на возможную ошибку, так как Catch обрабатывает только содержимое блока Try. Эту особенность удобно использовать при отладке скриптов, для поиска ошибок.
И наоборот, Trap является глобальным обработчиком, отлавливая ошибки во всем скрипте независимо от расположения. На мой взгляд это более жизненный вариант, который больше подходит для постоянной работы.
description | Locale | ms.date | online version | schema | title |
---|---|---|---|---|---|
Describes how to use the `try`, `catch`, and `finally` blocks to handle terminating errors. |
en-US |
11/12/2021 |
https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_try_catch_finally?view=powershell-7.3&WT.mc_id=ps-gethelp |
2.0.0 |
about Try Catch Finally |
Short description
Describes how to use the try
, catch
, and finally
blocks to handle
terminating errors.
Long description
Use try
, catch
, and finally
blocks to respond to or handle terminating
errors in scripts. The Trap
statement can also be used to handle terminating
errors in scripts. For more information, see about_Trap.
A terminating error stops a statement from running. If PowerShell does not
handle a terminating error in some way, PowerShell also stops running the
function or script using the current pipeline. In other languages, such as C#,
terminating errors are referred to as exceptions.
Use the try
block to define a section of a script in which you want
PowerShell to monitor for errors. When an error occurs within the try
block,
the error is first saved to the $Error
automatic variable. PowerShell then
searches for a catch
block to handle the error. If the try
statement does
not have a matching catch
block, PowerShell continues to search for an
appropriate catch
block or Trap
statement in the parent scopes. After a
catch
block is completed or if no appropriate catch
block or Trap
statement is found, the finally
block is run. If the error cannot be handled,
the error is written to the error stream.
A catch
block can include commands for tracking the error or for recovering
the expected flow of the script. A catch
block can specify which error types
it catches. A try
statement can include multiple catch
blocks for different
kinds of errors.
A finally
block can be used to free any resources that are no longer needed
by your script.
try
, catch
, and finally
resemble the try
, catch
, and finally
keywords used in the C# programming language.
Syntax
A try
statement contains a try
block, zero or more catch
blocks, and zero
or one finally
block. A try
statement must have at least one catch
block
or one finally
block.
The following shows the try
block syntax:
The try
keyword is followed by a statement list in braces. If a terminating
error occurs while the statements in the statement list are being run, the
script passes the error object from the try
block to an appropriate catch
block.
The following shows the catch
block syntax:
catch [[<error type>][',' <error type>]*] {<statement list>}
Error types appear in brackets. The outermost brackets indicate the element is
optional.
The catch
keyword is followed by an optional list of error type
specifications and a statement list. If a terminating error occurs in the
try
block, PowerShell searches for an appropriate catch
block. If
one is found, the statements in the catch
block are executed.
The catch
block can specify one or more error types. An error type is a
Microsoft .NET Framework exception or an exception that is derived from a .NET
Framework exception. A catch
block handles errors of the specified .NET
Framework exception class or of any class that derives from the specified
class.
If a catch
block specifies an error type, that catch
block handles that
type of error. If a catch
block does not specify an error type, that catch
block handles any error encountered in the try
block. A try
statement can
include multiple catch
blocks for the different specified error types.
The following shows the finally
block syntax:
finally {<statement list>}
The finally
keyword is followed by a statement list that runs every time the
script is run, even if the try
statement ran without error or an error was
caught in a catch
statement.
Note that pressing CTRL+C stops the pipeline. Objects
that are sent to the pipeline will not be displayed as output. Therefore, if
you include a statement to be displayed, such as «Finally block has run», it
will not be displayed after you press CTRL+C, even if the
finally
block ran.
Catching errors
The following sample script shows a try
block with a catch
block:
try { NonsenseString } catch { "An error occurred." }
The catch
keyword must immediately follow the try
block or another catch
block.
PowerShell does not recognize «NonsenseString» as a cmdlet or other item.
Running this script returns the following result:
When the script encounters «NonsenseString», it causes a terminating error. The
catch
block handles the error by running the statement list inside the block.
Using multiple catch statements
A try
statement can have any number of catch
blocks. For example, the
following script has a try
block that downloads MyDoc.doc
, and it contains
two catch
blocks:
try { $wc = new-object System.Net.WebClient $wc.DownloadFile("http://www.contoso.com/MyDoc.doc","c:tempMyDoc.doc") } catch [System.Net.WebException],[System.IO.IOException] { "Unable to download MyDoc.doc from http://www.contoso.com." } catch { "An error occurred that could not be resolved." }
The first catch
block handles errors of the System.Net.WebException and
System.IO.IOException types. The second catch
block does not specify an
error type. The second catch
block handles any other terminating errors that
occur.
PowerShell matches error types by inheritance. A catch
block handles errors
of the specified .NET Framework exception class or of any class that derives
from the specified class. The following example contains a catch
block that
catches a «Command Not Found» error:
catch [System.Management.Automation.CommandNotFoundException] {"Inherited Exception" }
The specified error type, CommandNotFoundException, inherits from the
System.SystemException type. The following example also catches a Command
Not Found error:
catch [System.SystemException] {"Base Exception" }
This catch
block handles the «Command Not Found» error and other errors that
inherit from the SystemException type.
If you specify an error class and one of its derived classes, place the catch
block for the derived class before the catch
block for the general class.
[!NOTE]
PowerShell wraps all exceptions in a RuntimeException type. Therefore,
specifying the error type System.Management.Automation.RuntimeException
behaves the same as an unqualified catch block.
Using Traps in a Try Catch
When a terminating error occurs in a try
block with a Trap
defined within
the try
block, even if there is a matching catch
block, the Trap
statement
takes control.
If a Trap
exists at a higher block than the try
, and there is no matching
catch
block within the current scope, the Trap
will take control, even if
any parent scope has a matching catch
block.
Accessing exception information
Within a catch
block, the current error can be accessed using $_
, which
is also known as $PSItem
. The object is of type ErrorRecord.
try { NonsenseString } catch { Write-Host "An error occurred:" Write-Host $_ }
Running this script returns the following result:
An Error occurred:
The term 'NonsenseString' is not recognized as the name of a cmdlet, function,
script file, or operable program. Check the spelling of the name, or if a path
was included, verify that the path is correct and try again.
There are additional properties that can be accessed, such as ScriptStackTrace,
Exception, and ErrorDetails. For example, if we change the script to the
following:
try { NonsenseString } catch { Write-Host "An error occurred:" Write-Host $_.ScriptStackTrace }
The result will be similar to:
An Error occurred:
at <ScriptBlock>, <No file>: line 2
Freeing resources using finally
To free resources used by a script, add a finally
block after the try
and
catch
blocks. The finally
block statements run regardless of whether the
try
block encounters a terminating error. PowerShell runs the finally
block
before the script terminates or before the current block goes out of scope.
A finally
block runs even if you use CTRL+C to stop the
script. A finally
block also runs if an Exit keyword stops the script from
within a catch
block.
See also
- about_Break
- about_Continue
- about_Scopes
- about_Throw
- about_Trap
You have had it happen before. You run a PowerShell script, and suddenly the console is full of errors. Did you know you can handle these errors in a much better way? Enter PowerShell try catch blocks!
Using error handling with PowerShell try catch blocks allows for managing and responding to these terminating errors. In this post, you will be introduced to PowerShell try catch blocks and learn how to handle specific exception messages.
Understanding PowerShell Try Catch Syntax
The PowerShell try catch block syntax is straightforward. It is composed of two sections enclosed in curly brackets. The first identified section is the try
block, and the second section is the catch
block.
try { # Command(s) to try } catch { # What to do with terminating errors }
The try block can have as many statements in it as you want; however, keep the statements to as few as possible, probably just a single statement. The point of error handling is to work with one statement at a time and deal with anything that occurs from the error.
Here is an example of an error occurring in the PowerShell console. The command is creating a new file using the New-Item cmdlet and specifying a non-existent folder for Path.
If this command was in a script, the output wastes some screen space, and the problem may not be immediately visible. Using a PowerShell try catch block, you can manipulate the error output and make it more readable.
Here is the same New-Item command in a try catch block. Note that line 5 uses the -ErrorAction parameter with a value of Stop to the command. Not all errors are considered “terminating,” so sometimes you need to add this bit of code to terminate into the catch block properly.
try { New-Item -Path C:doesnotexist ` -Name myfile.txt ` -ItemType File ` -ErrorAction Stop } catch { Write-Warning -Message "Oops, ran into an issue" }
Instead of a block of red angry-looking text, you have a simple warning message that it ran into an issue. The non-existent Path name along with forcing -ErrorAction Stop drops the logic into the catch block and displays the custom warning.
Adding the $Error Variable to Catch Output
While more readable, this is not very helpful. All you know is the command did not complete successfully, but you don’t know why. Instead of displaying my custom message, you can display the specific error message that occurred instead of the entire exception block.
When an error occurs in the try block, it is saved to the automatic variable named $Error. The $Error variable contains an array of recent errors, and you can reference the most recent error in the array at index 0.
try { New-Item -Path C:doesnotexist ` -Name myfile.txt ` -ItemType File ` -ErrorAction Stop } catch { Write-Warning $Error[0] }
The warning output is now more descriptive showing that the command failed because it couldn’t find part of the path. This message was a part of our original error message but is now more concise.
Alternatively, you can save the incoming error to a variable using $_
. The dollar sign + underscore in PowerShell indicates the current item in the pipeline. In this case, the current item is the error message coming out of the try block.
Once you have saved this incoming message, you use it in a custom output message, like so:
try { New-Item -Path C:doesnotexist ` -Name myfile.txt ` -ItemType File ` -ErrorAction Stop } catch { $message = $_ Write-Warning "Something happened! $message" }
Adding Exception Messages to Catch Blocks
You can also use multiple catch blocks to handle specific errors differently. This example displays two different custom messages:
- One for if the path does not exist
- One for if an illegal character is used in the name
Note that the following screenshot shows the script running twice with two different commands in the try block. Each command, catch block, and the orange and green arrows indicate the final output.
Looking at lines 14-16, there is a third catch block without an exception message. This is a “catch-all” block that will run if the error does not match any other exceptions. If you are running this script and seeing the message in the last catch block, you know the error is not related to an illegal character in the file name or part of the path not being valid.
Now how do you find the exception messages to use in the first two catch blocks? You find it by looking at the different information attached to the $Error
variable. After a failed command occurs, you can run $Error[0].Exception.GetType().FullName to view the exception message for the last error that occurred.
Going back to the PowerShell console, rerun the New-Item
command with a non-existent path, then run the $Error command to find the exception message.
The red text immediately following the failed command also contains the exception message but does not contain which module it is from. Looking at the $Error variable shows the full message to be used for a catch block.
Getting Exception Messages in PowerShell 7
PowerShell version 7 introduced the Get-Error
command. This command displays the most recent error from the current session. After encountering an error, run Get-Error to show the exception type to use in the catch
block.
Summary
Using PowerShell try catch blocks gives additional power to handle errors in a script and take different actions based on the error. The catch block can display more than just an error message. It can contain logic that will resolve the error and continue executing the rest of the script.
Do you have any tips on using try/catch blocks? Leave a comment below or find me on Twitter or LinkedIn for additional discussion.
Enjoyed this article? Check out more of my PowerShell articles here!
References:
Microsoft Docs: About Try Catch Finally
Microsoft Docs: About Automatic Variables ($Error)
Microsoft Blog: Understanding Non-Terminating Errors in PowerShell
Введение
Эта статья адресована тем, кто уже познакомился с основами PowerShell, запускал какие-то скрипты со stackexchange и, вероятно, имеет свой текстовый файл с теми или иными сниппетами облегчающими повседневную работу. Целью её написания есть уменьшение энтропии, увеличение читаемости и поддерживаемости кода PowerShell используемого в вашей компании и, как следствие, повышение продуктивности администратора работающего с ним.
На своём предыдущем месте работы я в силу специфики задач и несовершенства мира, сильно прокачал навык работы с PowerShell. После смены работы нагрузки такого рода сильно снизились и всё что было вот-вот еще на кончиках пальцев стало всё глубже тонуть под опытом решения новых задач на новых технологиях. От того эта статья претендует быть лишь тем, чем себя объявляет, раскрывая список тем, который на мой взгляд был бы полезен мне самому лет 7 назад, тогда, когда моё знакомство с этим инструментом только начиналось.
Если вы не понимаете почему PowerShell — объектно-ориентированный шелл, какие от этого появляются бонусы и зачем это вообще надо, я, не смотря на возгласы хейтеров посоветую вам хорошую книгу быстро вводящую в суть этой среды — Попов Андрей Владимирович, Введение в Windows PowerShell. Да, она про старую версию PS, да, язык обрел некоторые расширения и улучшения, но эта книга хороша тем, что описывая ранний этап развития этой среды невольно делает акцент лишь фундаментальных вещах. Синтаксический сахар, которым обросла среда я думаю вы быстро и без того воспримите поняв как работает сама концепция. Прочтение этой книги займет у вас буквально пару вечеров, возвращайтесь после прочтения.
Книга также доступна на сайте автора, правда я не уверен в том насколько лицензионно чисто такое использование: https://andpop.ru/courses/winscript/books/posh_popov.pdf
Стайл гайды
Оформление скриптов согласно стайлгайдам хорошая практика во всех случаях её применения, вряд ли тут может быть два мнения. Некоторые экосистемы позаботились об этом на уровне родного тулинга, из очевидного в голову приходит pep8 в сообществе Python и go fmt в Golang. Это бесценные инструменты экономящие время, к сожалению отсутствующие в стандартной поставке PowerShell, а от того переносящие проблему на нашу с вами голову. Единственным на текущий момент способом решения проблемы единого форматирования кода является вырабатывание рефлексов путем многократного написания кода удовлетворяющего стайлгайдам (на самом деле нет).
Стайлгайды в силу отсутствия официально утвержденных и подробно описанных компанией Микрософт были рождены в сообществе во времена PowerShell v3 и с тех пор развиваются в открытом виде на гитхабе: PowerShellPracticeAndStyle. Это заслуживающий внимания репозиторий для любого, кто хоть раз пользовался кнопкой «Save» в PowerShell ise.
Если попытаться сделать выжимку, то вероятно сведется она к следующим пунктам:
- В PowerShell используется PascalCase для именования переменных, командлетов, имен модулей и практически всего, за исключением операторов;
- Операторы языка, такие как
if
,switch
,break
,process
,-match
пишутся сугубо строчными буквами; - Фигурные скобки расставляются единственно верным способом, иначе еще называемым стилем Кернигана и Ричи ведущим свою историю из книги The C Programming Language;
- Не используйте алиасы нигде кроме интерактивного сеанса консоли, не пишите в файл скрипта никаких
ps | ? processname -eq firefox | %{$ws=0}{$ws+=$_.workingset}{$ws/1MB}
; - Указывайте явно имена параметров, поведение командлетов и их сигнатура может поменяться, плюс к этому, человеку незнакомому с конкретным командлетом это добавит контекста;
- Оформляйте параметры вызова скриптов, а не пишите внутри скрипта функцию и последней строчкой вызов этой функции с необходимостью изменять значения глобальных переменных вместо указания параметров;
- Указывайте [CmdletBinding()] — это подарит вашему командлету
-Verbose
и-Debug
флаги и много других полезных фичей. Несмотря на твердость позиции некоторых пуристов в сообществе, я не сторонник указывания этого атрибута в простых инлайн-функциях и фильтрах состоящих из буквальных нескольких строк; - Пишите comment-based справку: одно предложение, ссылку на тикет, пример вызова;
- Указывайте необходимую версию PowerShell в секции
#requires
; - Используйте
Set-StrictMode -Version Latest
, это поможет вам избежать проблем описанных ниже; - Обрабатывайте ошибки;
- Не спешите переписывать всё на PowerShell. PowerShell — это в первую очередь шелл и вызывать бинари — его прямая задача. Нет ничего плохого в том, чтобы заиспользовать robocopy в скрипте, она, конечно, не rsync, но тоже очень хороша.
Ниже пример того как оформить справку скрипта. Скрипт кадрирует изображение приводя его к квадрату и выполняет ресайз, думаю у вас есть задача делать аватарки для пользователей (нехватает разве что поворота по данным exif). В разделе .EXAMPLE
есть пример использования, попробуйте. В силу того, что PowerShell выполняется средой CLR, той же что и прочие dotnet языки, у него есть возможность использовать всю мощь библиотек dotnet:
<#
.SYNOPSIS
Resize-Image resizes an image file
.DESCRIPTION
This function uses the native .NET API to crop a square and resize an image file
.PARAMETER InputFile
Specify the path to the image
.PARAMETER OutputFile
Specify the path to the resized image
.PARAMETER SquareHeight
Define the size of the side of the square of the cropped image.
.PARAMETER Quality
Jpeg compression ratio
.EXAMPLE
Resize the image to a specific size:
.Resize-Image.ps1 -InputFile "C:userpic.jpg" -OutputFile "C:userpic-400.jpg"-SquareHeight 400
#>
# requires -version 3
[CmdletBinding()]
Param(
[Parameter( Mandatory )]
[string]$InputFile,
[Parameter( Mandatory )]
[string]$OutputFile,
[Parameter( Mandatory )]
[int32]$SquareHeight,
[ValidateRange( 1, 100 )]
[int]$Quality = 85
)
# Add System.Drawing assembly
Add-Type -AssemblyName System.Drawing
# Open image file
$Image = [System.Drawing.Image]::FromFile( $InputFile )
# Calculate the offset for centering the image
$SquareSide = if ( $Image.Height -lt $Image.Width ) {
$Image.Height
$Offset = 0
} else {
$Image.Width
$Offset = ( $Image.Height - $Image.Width ) / 2
}
# Create empty square canvas for the new image
$SquareImage = New-Object System.Drawing.Bitmap( $SquareSide, $SquareSide )
$SquareImage.SetResolution( $Image.HorizontalResolution, $Image.VerticalResolution )
# Draw new image on the empty canvas
$Canvas = [System.Drawing.Graphics]::FromImage( $SquareImage )
$Canvas.DrawImage( $Image, 0, -$Offset )
# Resize image
$ResultImage = New-Object System.Drawing.Bitmap( $SquareHeight, $SquareHeight )
$Canvas = [System.Drawing.Graphics]::FromImage( $ResultImage )
$Canvas.DrawImage( $SquareImage, 0, 0, $SquareHeight, $SquareHeight )
$ImageCodecInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
Where-Object MimeType -eq 'image/jpeg'
# https://msdn.microsoft.com/ru-ru/library/hwkztaft(v=vs.110).aspx
$EncoderQuality = [System.Drawing.Imaging.Encoder]::Quality
$EncoderParameters = New-Object System.Drawing.Imaging.EncoderParameters( 1 )
$EncoderParameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter( $EncoderQuality, $Quality )
# Save the image
$ResultImage.Save( $OutputFile, $ImageCodecInfo, $EncoderParameters )
Приведенный выше скрипт начинается с многострочного комментария <# ... #>
, в том случае если этот комментарий идет первым и содержит определенные ключевые слова, PowerShell догадается автоматически построить справку к скрипту. Такого рода справка буквально так и называется — справка основанная на коментарии:
Мало того, при вызове скрипта будут работать подсказки по параметрам, будь то консоль PowerShell, будь то редактор кода:
Еще раз обращу внимание на то, что пренебрегать ей не стоит. Если не знаете что туда написать, напишите что-нибудь, сходите до кулера и по возвращении у вас точно будет понимание того, что нужно в написанном изменить. Это работает. Не стоит фанатично заполнять все ключевые слова, PowerShell разработан быть самодокументируемым и если вы дали осмысленные и полные имена параметрам, короткого предложения в разделе .SYNOPIS
и одного примера вполне хватит.
Strict mode
PowerShell, как и многие другие скриптовые языки обладает динамической типизацией. У такого вида типизации есть много сторонников: написать простую, но мощную высокоуровневую логику — дело пары минут, но когда ваше решение начнет подбираться к тысяче строк, вы обязательно столкнетесь с хрупкостью такого подхода.
Автовывод типов неизменно на этапе тестирования формировавший массив в том месте где вы всегда получали набор элементов, обязательно подложит свинью в случае, если получит один элемент и вот уже в следующем условии, вместо проверки количества элементов вы получите количество символов или иной атрибут, в зависимости от типа элемента. Логика скрипта сломается, при этом среда исполнения сделает вид что все хорошо.
Установка строгого режима помогает избежать части такого рода проблем, но и требует от вас чуть больше кода, вроде инициализации переменных и явного приведения.
Включается этот режим командлетом Set-StrictMode -Version Latest
, хотя есть другие варианты «строгости», мой выбор — использовать последний.
В примере ниже строгий режим отлавливает обращение к несуществующему свойству. Так как внутри папки находится лишь один элемент, тип переменной $Input
в результате выполнения будет FileInfo
, а не ожидаемый массив соответствующих элементов:
Для избежания такой проблемы, следует результат выполнения командлета явно привести к массиву:
$Items = @( Get-ChildItem C:Userssnd3rNextcloud )
Возьмите за правило всегда устанавливать строгий режим, это позволит вам избежать неожиданных результатов выполнения ваших скриптов.
Обработка ошибок
ErrorActionPreference
Просматривая чужие скрипты, например на гитхабе, я часто вижу либо полное игнорирование механизма обработки ошибок, либо явное включение режима тихого продолжения работы в случае возникновения ошибки. Вопрос обработки ошибок, безусловно, не самый простой в программировании вообще и в скриптах в частности, но игнорирования он определенно не заслуживает. По-умолчанию, PowerShell в случае возникновения ошибки выводит её и продолжает работу (я немного упростил концепцию, но ниже будет ссылка на гит-книгу по этой теме). Это удобно, в случае если вам срочно нужно распространить обновление широкоиспользуемой в домене программы на все машины, не дожидаясь пока она разольется на все точки деплоя sccm или распространится иным используемым у вас способом. Неприятно прерывать и перезапускать процесс в случае если одна из машин выключена, это правда.
С другой стороны, если вы делаете сложное резервное копирование системы состоящей из более чем одного файла данных более чем одной части информационной системы, вам нужно быть уверенным что ваша резервная копия консистентна и что все необходимые наборы данных были скопированы без ошибок.
Для изменения поведения командлетов в случае возникновения ошибки существует глобальная переменная $ErrorActionPreference
, со следующим списком возможных значений: Stop, Inquire, Continue, Suspend, SilentlyContinue.
Я рекомендую всегда использовать значение Stop
, когда количество скриптов или их сложность перестают сохраняться на стеке в голове, лучше быть уверенным, что в любюй непредвиденной ситуации скрипт остановит свою работу, а не наломает дров «по-тихому», закончив выполнение «успешно».
Помимо остановки скрипта в случае, если что-то пошло не так есть еще одно обязательное условие его применения — обработка исключительных ситуаций. Для этого есть конструкция try/catch
, но работает только в том случае, если ошибка вызывает остановку выполнения. Не обязательно остановка должна быть включена на уровне всего скрипта, ErrorAction
можно устанавливать и на уровне командлета параметром:
Get-ChildItem 'C:System Volume Information' -ErrorAction 'Stop'
Собственно такая возможность и определяет две логичные стратегии: разрешать все ошибки «по-умолчанию» и выставлять ErrorAction
только для критичных мест, где их и обрабатывать; либо включать на уровне всего скрипта путем задания значения глобальной переменной и задавать -ErrorAction 'Continue'
на некритичных операциях. Я всегда выбираю второй вариант, не спешу вам его навязывать, рекомендую лишь один раз разобраться в этом вопросе и использовать этот полезный инструмент.
try/catch
В обработчике ошибок можно делать матчинг по типу исключения и оперировать потоком исполнения или, например, добавлять чуть больше информации. Не смотря на то, что используя операторы try/catch/throw/trap
можно выстроить весь поток выполнения скрипта, следует категорически этого избегать, так как такой способ оперирования выполнением мало того, что считается крайним антипаттерном, из разряда «goto-лапши», так еще и сильно просаживает производительность.
#requires -version 3
$ErrorActionPreference = 'Stop'
# создание объекта логгера, код которого смотрите ниже,
# инкапсулирующего знание о пути к логу и формату записи
$Logger = Get-Logger "$PSScriptRootLog.txt"
# глобальная ловушка ошибок
trap {
$Logger.AddErrorRecord( $_ )
exit 1
}
# счётчик попыток подключения
$count = 1;
while ( $true ) {
try {
# попытка подключения
$StorageServers = @( Get-ADGroupMember -Identity StorageServers | Select-Object -Expand Name )
} catch [System.Management.Automation.CommandNotFoundException] {
# выбрасываемое наружу исключение в силу того, что нет смысла продолжать выполнение без установки модуля
throw "Командлет Get-ADGroupMember недоступен, требуется добавить фичу Active Directory module for PowerShell; $( $_.Exception.Message )"
} catch [System.TimeoutException] {
# переход к следующей итерации цикла в случае если количество попыток не превышено
if ( $count -le 3 ) { $count++; Start-Sleep -S 10; continue }
# остановка выполнения и выбрасывание исключения наружу в силу невозможности получения необходимых данных
throw "Подключение к серверу небыло установленно из-за ошибки таймаута соединения, было произведено $count попыток; $( $_.Exception.Message )"
}
# выход из цикла в случае отсутствия исключительных ситуаций
break
}
Стоит отметить оператор trap
— это глобальная ловушка ошибок. Она ловит все что не было обработано на более низких уровнях, либо выброшено наружу из обработчика исключения в силу невозможности самостоятельного исправления ситуации.
Помимо описанного выше объектно-ориентированного подхода исключений, PowerShell предоставляет и более привычные, совместимые с другими «классическими» шеллами концепции, например потоков ошибок, кодов возврата и переменных накапливающих ошибки. Всё это безусловно удобно, иногда безальтернативно, но выходит за рамки этого, в целом обзорного, топика. К счастью на эту тему есть хорошая открытая книга на github.
Код логгера, который я использую когда нет уверенности что в системе будет PowerShell 5 (где можно описать более удобно класс логгера), попробуйте его, он может быть вам полезен в силу своей простоты и краткости, дополнительные методы вы, уверен, добавите без труда.:
# Фабрика логгеров "для бедных", совместимая с PowerShell v3
function Get-Logger {
[CmdletBinding()]
param (
[Parameter( Mandatory = $true )]
[string] $LogPath,
[string] $TimeFormat = 'yyyy-MM-dd HH:mm:ss'
)
$LogsDir = [System.IO.Path]::GetDirectoryName( $LogPath )
New-Item $LogsDir -ItemType Directory -Force | Out-Null
New-Item $LogPath -ItemType File -Force | Out-Null
$Logger = [PSCustomObject]@{
LogPath = $LogPath
TimeFormat = $TimeFormat
}
Add-Member -InputObject $Logger -MemberType ScriptMethod AddErrorRecord -Value {
param(
[Parameter( Mandatory = $true )]
[string]$String
)
"$( Get-Date -Format 'yyyy-MM-dd HH:mm:ss' ) [Error] $String" | Out-File $this.LogPath -Append
}
return $Logger
}
Повторю идею — не игнорируйте обработку ошибок. Это сэкономит ваше время и нервы в длительной перспективе.
Не думайте, что выполнение скрипта несмотря ни на что — хорошо. Хорошо — это вовремя упасть не наломав дров.
Инструменты
Начать улучшение инструментов работы с PowerShell стоит безусловно с эмулятора консоли. Я часто слышал от сторонников альтернативных ос, что консоль в windows плоха и что это вообще не консоль, а дос и проч. Мало кто адекватно мог сформулировать свои претензии на этот счет, но если кому-то удавалось, то на деле оказывалось что все проблемы можно решить. Подробнее о терминалах и новой консоли в windows на хабре уже было, там всё более чем ок.
Первым делом стоит установить Conemu или его сборку Cmder, что не особо важно, так как на мой взгляд по настройкам в любом случае стоит пробежаться. Я обычно выбираю cmder в минимальной конфигурации, без гита и прочих бинарей, которые ставлю сам, хотя несколько лет тюнил свой конфиг для чистой Conemu. Это действительно лучший эмулятор терминала для windows, позволяющий разделять экран (для любителей tmux/screen), создавать вкладки и включить quake-style режим консоли:
Conemu
Следущим шагом рекомендую поставить модули: oh-my-posh, posh-git и PSReadLine. Первые два сделают промт приятнее, добавив в него информацию о текущей сессии, статусе последней выполненной команды, индикатор привелегий и статус гит-репозитория в текущем расположении. PSReadLine сильно прокачивает промт, добавляя например поиск по истории введенных команд (CRTL + R) и удобные подсказки для командлетов по CRTL + Space:
И да, теперь консоль можно очищать комбинацией CTRL + L, и забыть про cls
.
Visual Studio Code
Редактор. Всё самое плохое, что я могу сказать про PowerShell, относится сугубо к PowerShell ISE, те кто видели первую версию с тремя панелями врядли забудут этот опыт. Отличающаяся кодировка терминала, отсутствие базовых возможностей редактора, вроде автоматического отступа, автозакрывающихся скобок, форматирования кода и целый набор порождаемых им антипаттернов про которые я вам не расскажу (на всякий случай) — это все про ISE.
Не используйте его, используйте Visual Studio Code с расширением PowerShell — тут есть всё, чего бы вы не захотели (в разумных пределах, конечно). И не забывайте, что в PoweShell до шестой версии (PowerShell Core 6.0) кодировка для скриптов — UTF8 BOM, иначе русский язык сломается.
Помимо подсветки синтаксиса, подсказки методов и возможности дебага скриптов, плагин устанавливает линтер, который так же поможет вам следовать закрепившимся в сообществе практикам, например в один клик (по лампочке) развернет сокращения. На деле это обычный модуль, который можно поставить и независимо, например добавить его в ваш пайплайн подписи скриптов: PSScriptAnalyzer
Задать параметры форматирования кода можно в настройках расширения, по всем настройкам (и редактора и расширений) есть поиск: File - Preferences - Settings
:
Для того чтобы получить новую консоль conpty следует установить флаг в настройках, позже, вероятно, этот совет будет неактуальным.
Стоит запомнить, что любое действие в VS Code можно выполнить из центра управления, вызываемого комбинацией CTRL + Shift + P. Отформатировать вставленный из чата кусок кода, отсортировать строки по алфавиту, поменять индент с пробелов на табы и проч, всё это в центре управления.
Например включить полный экран и расположение редактора по центру:
Source Control; Git, SVN
Часто у системных администраторов Windows есть фобия разрешения конфликтов в системах контроля версий, вероятно от того, что если представитель этого множества пользуется git, то зачастую один и не встречается ни с какими проблемами такого рода. С vscode разрешение конфликтов сводится буквально к кликам мыши на тех частях кода что нужно оставить или заместить.
Вот эти надписи между 303 и 304 строкой кликабельны, стоит нажать на все такие что появляются в документе в случае конфликта, сделать коммит фиксирующий изменения и отправить изменения на сервер. У — Удобство.
О том как работать с системами контроля версий доступно и с картинками написано в доке vscode, пробегитесь глазами до конца там кратко и хорошо.
Snippets
Сниппеты — своего рода макросы/шаблоны позволяющие ускорить написание кода. Однозначно must see.
Быстрое создание объекта:
Рыба для comment-based help:
В том случае, если командлету нужно передать большое количество параметров, есть смысл использовать сплаттинг.
Вот сниппет для него:
Просмотр всех доступных сниппетов доступен по Ctrl + Alt + J:
Если после этого у вас появилось желание продолжить улучшать свое окружение, но вы еще ниразу не слышали про осом-листы, то вот же, положил. Так же, если у вас есть свой набор расширений пригождающихся вам при написании PowerShell-скриптов, буду рад увидеть их список в коментариях.
Производительность
Тема производительности не такая простая как может показаться на первый взгляд. С одной стороны, преждевременные оптимизации могут сильно снизить читаемость и поддерживаемость кода, сэкономив 300мс времени выполнения скрипта, обычное время работы которого может быть десяток минут, применение их в таком случае безусловно деструктивно. С другой — есть несколько довольно простых приемов, повышающих как читаемость кода так и скорость его работы, использовать которые вполне уместно на постоянной основе. Ниже я расскажу о некоторых из них, в случае если же перформанс для вас всё, а читаемость уходит на второй план в силу жестких временных ограничений простоя сервиса на время обслуживания, рекомендую обратиться к профильной литературе.
Pipeline и foreach
Самый простой и всегда рабочий способ поднять производительность — уйти от использования пайпов. В силу типобезопасности и удобства работы ради, PowerShell пропуская элементы через пайп оборачивает каждый из них в объект. В dotnet языках такое поведение называется боксинг. Боксинг хорош, он гарантирует безопасность, но у него есть своя цена, которую порой нет смысла платить.
Первым шагом поднять производительность и на мой взгляд повысить читаемость можно убрав все применения командлета Foreach-Object
и заменив его на оператор foreach. Вас может смутить то, что на самом деле это две разных сущности, ведь foreach
является алиасом для Foreach-Object
— на практике главное отличие в том, что foreach
не принимает значения из пайплайна, при этом работает по опыту до трех раз быстрее.
Представим задачу: нам нужно обработать большой лог для формирования какой-то его производной, например, выбрать и привести к другому формату ряд записей в нём:
Get-Content D:tempSomeHeavy.log | Select-String '328117'
В примере выше первый этап поставленной задачи — выбор необходимых записей пройденый самым очевидным путем, хорош своей когнитивной легкостью и выразительностью. При этом он содержит место сильно снижающее производительность — пайплайн, точнее будет сказать что виноват не сам пайплайн, а поведение командлета Get-Content
. Для передачи данных по конвейеру он читает файл построчно и оборачивает каждую строку лога из базового типа string
в объект, сильно увеличивая его размер, уменьшая локальность данных в кеше и делая не особо нужную нам работу. Избежать этого просто — нужно указать ему параметром о том, что эти данные нужно прочитать полностью за один раз:
When reading from and writing to binary files, use the AsByteStream parameter and a value of 0 for the ReadCount parameter. A ReadCount value of 0 reads the entire file in a single read operation. The default ReadCount value, 1, reads one byte in each read operation and converts each byte into a separate object, which causes errors when you use the Set-Content cmdlet to write the bytes to a file unless you use AsByteStream
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-content
Get-Content D:tempSomeHeavy.log -ReadCount 0 | Select-String '328117'
На моём файле лога размером чуть более одного гигабайта преимущество второго подхода почти в три раза:
Я советую вам не верить мне на слово, а самим проверить так ли это на файле похожего размера. В целом же Select-String
даёт хороший результат и если итоговое время вас устраивает — пора остановиться с оптимизацией этой части скрипта. В случае если итоговое время выполнения скрипта до сих пор сильно зависит от этапа получения данных, можно еще немного снизить время выборки данных заменив командлет Select-String
. Это очень мощный и удобный инструмент, но что бы таким быть Select-String
добавляет некоторое количество метаданных в свой вывод, опять же производя не бесплатную по времени работу, мы можем отказаться от лишних метаданных и сопутствующей работы заменив командлет оператором языка:
foreach ( $line in ( Get-Content D:tempSomeHeavy.log -ReadCount 0 )) {
if ( $line -match '328117' ) {
$line
}
}
На моих тестах время выполнения уменьшилось до 30 секунд, то-есть я выйграл 30%, при этом уже на моём примере оно того не особо стоило, так как кода стало больше и хоть они банальный, но нежелание разобраться в нем у стороннего наблюдателя, по-моему опыту, увеличилось вдвое (как население в тех замках ;-). Если вы оперируете десятками гигабайт логов, то это несомненно ваш путь. Что еще хотелось бы отметить в приведенном выше коде, так это оператор -match
; суть его — поиск совпадения по регулярному выражению. В конкретном случае в силу простоты этого выражения поиск сводится к вычислительно простому поиску по подстроке, но так бывает не всегда — от сложности вашего регулярного выражения время выполнения может увеличиваться с любой вообразимой вами прогрессией — регулярые выражения все-таки Тьюринг полный язык, будьте с ними осторожны.
Следующая задача — как-то обработать этот лог, напишем решение «в лоб» с добавлением в каждую отобранную строчку текущей даты и запись в файл через пайп:
foreach ( $line in ( Get-Content D:tempSomeHeavy.log -ReadCount 0 )) {
if ( $line -match '328117' ) {
"$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" | Out-File D:tempResult.log -Append
}
}
Результаты выполнения замеренные командлетом Measure-Command
:
Hours : 2
Minutes : 20
Seconds : 9
Milliseconds : 101
Попробуем улучшить результат. Думаю многим очевидно, что запись каждой отдельной строки в файл не самая оптимальная операция, куда лучше сделать накопительный буфер который периодически сбрасывать на диск, в идеале сбросить его один раз. Так же стоит отметить, что строки в PowerShell неизменяемы и любая операция со строкой порождает новую область в памяти, куда записывается новая строка, а старая остается ждать сборщик мусора — это дорого и по скорости и по памяти. Для решения этой проблемы в дотнете есть специализированный класс, который позволяет изменять строки, при этом инкапсулируя логику более аккуратного выделения памяти и имя ему — StringBuilder. При создании класса выделяется буфер в оперативной памяти в который добавляются новые строки без повторного выделения памяти, в том случае если размера буфера не хватает для добавления новой строки, то создается новый вдвое большего размера и работа продолжается с ним. Помимо того что такая стратегия сильно уменьшает количество выделений памяти, её еще можно подтюнить если знать примерный объем памяти который будут занимать строки и задать его в конструкторе при создании объекта.
$StringBuilder = New-Object System.Text.StringBuilder
foreach ( $line in ( Get-Content D:tempSomeHeavy.log -ReadCount 0 )) {
if ( $line -match '328117' ) {
$null = $StringBuilder.AppendLine( "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" )
}
}
Out-File -InputObject $StringBuilder.ToString() -FilePath D:tempResult.log -Append -Encoding UTF8
Время выполнения этого кода всего 5 минут, вместо прошлых двух с половиной часов:
Hours : 0
Minutes : 5
Seconds : 37
Milliseconds : 150
Стоит отметить конструкцию Out-File -InputObject
, суть её в том, чтобы в очередной раз избавиться от пайплайна. Такой способ быстрее пайпа и работает со всеми командлетами — любое значение которое в сигнатуре командлета является значением принимаемым из пайпа может быть задано параметром. Наиболее простой способ узнать какой именно параметр принимает себе проходящие через пайп значения — выполнить Get-Help
на командлете с параметром -Full
, среди списка параметров один должен содержать в себе Accept pipeline input? true (ByValue)
:
-InputObject <psobject>
Required? false
Position? Named
Accept pipeline input? true (ByValue)
Parameter set name (All)
Aliases None
Dynamic? false
В обоих случаях PowerShell ограничивал себя тремя гигабайтами памяти:
Во время работы StringBuilder
на каждой итерации выводит в консоль информацию о выделенной на текущий момент памяти и размере добавленных элементов из которой можно взять примерные значения для установки в конструкторе:
Всем хорош предыдущий подход, кроме разве что того, что 3Гб это 3Гб. Попробуем уменьшить потребление памяти и заиспользуем другой dotnet-класс написаный для решения таких проблем — StreamReader.
$StringBuilder = New-Object System.Text.StringBuilder
$StreamReader = New-Object System.IO.StreamReader 'D:tempSomeHeavy.log'
while ( $line = $StreamReader.ReadLine()) {
if ( $line -match '328117' ) {
$null = $StringBuilder.AppendLine( "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" )
}
}
$StreamReader.Dispose()
Out-File -InputObject $StringBuilder.ToString() -FilePath C:tempResult.log -Append -Encoding UTF8
Hours : 0
Minutes : 5
Seconds : 33
Milliseconds : 657
Время выполнения осталось практически тем же, но потребление памяти и его характер изменились. Если в предыдущем примере при чтении файла в памяти занималось место сразу под весь файл, у меня это больше гигабайта, а работа скрипта характеризовалась утилизацией трех гигабайт, то при использовании стримридера, занятая процессором память медленно увеличивалась пока не дошла до 2Гб. Конечный объем занятой памяти я заскринить не успел, но есть скрин того что происходило ближе к концу работы:
Поведение программы по расходу памяти вполне очевидно — вход у неё грубо говоря «труба», а выход — наш StringBuilder
— «бассейн» который и разливается до конца работы программы. Зададим размер буфера, что бы убрать лишние аллокации (я выбрал 100МБ) и начнем сбрасывать содержимое в файл при приближении к концу буфера. Последнюю проверку я реализовал в лоб — сравниваю прошел ли буфер отметку в 90% от общего размера (может быть эту операцию имеет смысл вынести из цикла, проверьте сами разницу во времени):
$BufferSize = 104857600
$StringBuilder = New-Object System.Text.StringBuilder $BufferSize
$StreamReader = New-Object System.IO.StreamReader 'C:tempSomeHeavy.log'
while ( $line = $StreamReader.ReadLine()) {
if ( $line -match '1443' ) {
# проверка приближения к концу буфера
if ( $StringBuilder.Length -gt ( $BufferSize - ( $BufferSize * 0.1 ))) {
Out-File -InputObject $StringBuilder.ToString() -FilePath C:tempResult.log -Append -Encoding UTF8
$StringBuilder.Clear()
}
$null = $StringBuilder.AppendLine( "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" )
}
}
Out-File -InputObject $StringBuilder.ToString() -FilePath C:tempResult.log -Append -Encoding UTF8
$StreamReader.Dispose()
Hours : 0
Minutes : 5
Seconds : 53
Milliseconds : 417
Максимальное потребление памяти составило 1Гб при почти той же скорости выполнения:
Безусловно результаты по абсолютным числам утилизированной памяти будут отличаться от одной машины к другой, всё зависит от того сколько памяти вообще доступно на машине и соответственно насколько агрессивным будет её освобождение. Если память для вас критична, а несколько процентов производительности не так, то можно еще уменьшить её потребление заиспользовав StreamWriter, он как стримридер, только стримрайтер Оставлю вам его для самостоятельного изучения, а то мне уже кажется я тут засиделся, ибо конца этому нет.
По-моему идея обозначена довольно явно — любую проблему люди уже решали, всегда следует начать искать решение в стандартной библиотеке. Главное эту проблему локализовать, хотя вероятно еще важнее — не выдумывать. Если Select-String
и Out-File
вас устраивают по времени, машина не встает и не падает с OutOfMemoryException
, то используйте их — простота и выразительность важнее.
Нативные бинарники
Часто, проникшись всем удобством и мощностью PowerShell администраторы начинают стремиться использовать встроенные командлеты вместо системных бинарников, с одной стороны их сложно в этом упрекнуть — удобство, с другой: PowerShell — в первую очередь шелл и запуск бинарников его прямая задача, с которой он отлично справляется.
Пример задачи, в прошлом решенной мной с помощью StringBuilder
и вызова консольной команды dir
— получение относительных путей всех файлов в каталоге и подкаталогах (большого количества файлов). С использованием нативной команды время выполнения меньше в пять раз:
$CurrentPath = ( Get-Location ).Path + ''
$StringBuilder = New-Object System.Text.StringBuilder
foreach ( $Line in ( &cmd /c dir /b /s /a-d )) {
$null = $StringBuilder.AppendLine( $Line.Replace( $CurrentPath, '.' ))
}
$StringBuilder.ToString()
Hours : 0
Minutes : 0
Seconds : 3
Milliseconds : 9
$StringBuilder = New-Object System.Text.StringBuilder
foreach ( $Line in ( Get-ChildItem -File -Recurse | Resolve-Path -Relative )) {
$null = $StringBuilder.AppendLine( $Line )
}
$StringBuilder.ToString()
Hours : 0
Minutes : 0
Seconds : 16
Milliseconds : 337
Присваивание результата работы в $null — наиболее дешевый способ подавления вывода. Наиболее дорогой, вы думаю догадались — отправка пайпом в Out-Null
; мало того, такой способ подавления (присваивание результата в $null
) еще и уменьшает время выполнения, хоть и незначительно.
# быстро:
$null = $StringBuilder.AppendLine( $Line )
# медленно:
$StringBuilder.AppendLine( $Line ) | Out-Null
Однажды у меня стояла задача синхронизировать каталоги с большим количеством файлов, при этом это была лишь часть работы довольно большого скрипта, этап подготовки. Реализация синхронизации каталогов с помощью Compare-Object
, хоть и выглядела достойно и компактно, затрачивала на свою работу времени больше, чем весь планируемый мною временной бюджет скрипта. Выходом из этой ситуации стало использование широкоизвестной в узких кругах утилиты robocopy.exe, компромисом же стало написание враппера (точнее класса для PowerShell 5), кодом которого спешу с вами поделиться:
class Robocopy {
[String]$RobocopyPath
Robocopy () {
$this.RobocopyPath = Join-Path $env:SystemRoot 'System32Robocopy.exe'
if ( -not ( Test-Path $this.RobocopyPath -PathType Leaf )) {
throw 'Исполняемый файл робокопи не найден'
}
}
[void]CopyFile ( [String]$SourceFile, [String]$DestinationFolder ) {
$this.CopyFile( $SourceFile, $DestinationFolder, $false )
}
[void]CopyFile ( [String]$SourceFile, [String]$DestinationFolder, [bool]$Archive ) {
$FileName = [IO.Path]::GetFileName( $SourceFile )
$FolderName = [IO.Path]::GetDirectoryName( $SourceFile )
$Arguments = @( '/R:0', '/NP', '/NC', '/NS', '/NJH', '/NJS', '/NDL' )
if ( $Archive ) {
$Arguments += $( '/A+:a' )
}
$ErrorFlag = $false
&$this.RobocopyPath $FolderName $DestinationFolder $FileName $Arguments | Foreach-Object {
if ( $ErrorFlag ) {
$ErrorFlag = $false
throw "$_ $ErrorString"
} else {
if ( $_ -match '(?<=(0x[da-f]{8}))(?<text>(.+$))' ) {
$ErrorFlag = $true
$ErrorString = $matches.text
} else {
$Logger.AddRecord( $_.Trim())
}
}
}
if ( $LASTEXITCODE -eq 8 ) {
throw 'Some files or directories could not be copied'
}
if ( $LASTEXITCODE -eq 16 ) {
throw 'Robocopy did not copy any files. Check the command line parameters and verify that Robocopy has enough rights to write to the destination folder.'
}
}
[void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder ) {
$this.SyncFolders( $SourceFolder, $DestinationFolder, '*.*', '', $false )
}
[void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [Bool]$Archive ) {
$this.SyncFolders( $SourceFolder, $DestinationFolder, '*.*', '', $Archive )
}
[void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include ) {
$this.SyncFolders( $SourceFolder, $DestinationFolder, $Include, '', $false )
}
[void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include, [Bool]$Archive ) {
$this.SyncFolders( $SourceFolder, $DestinationFolder, $Include, '', $Archive )
}
[void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include, [String]$Exclude ) {
$this.SyncFolders( $SourceFolder, $DestinationFolder, $Include, $Exclude, $false )
}
[void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include, [String]$Exclude, [Bool]$Archive ) {
$Arguments = @( '/MIR', '/R:0', '/NP', '/NC', '/NS', '/NJH', '/NJS', '/NDL' )
if ( $Exclude ) {
$Arguments += $( '/XF' )
$Arguments += $Exclude.Split(' ')
}
if ( $Archive ) {
$Arguments += $( '/A+:a' )
}
$ErrorFlag = $false
&$this.RobocopyPath $SourceFolder $DestinationFolder $Include $Arguments | Foreach-Object {
if ( $ErrorFlag ) {
$ErrorFlag = $false
throw "$_ $ErrorString"
} else {
if ( $_ -match '(?<=(0x[da-f]{8}))(?<text>(.+$))' ) {
$ErrorFlag = $true
$ErrorString = $matches.text
} else {
$Logger.AddRecord( $_.Trim())
}
}
}
if ( $LASTEXITCODE -eq 8 ) {
throw 'Some files or directories could not be copied'
}
if ( $LASTEXITCODE -eq 16 ) {
throw 'Robocopy did not copy any files. Check the command line parameters and verify that Robocopy has enough rights to write to the destination folder.'
}
}
}
Код этот я писал несколько лет назад и некоторые решения в нем могут показаться не лучшими, но проводить глубокую ревизию его мне в данный момент времени крайне лениво (иначе конца написанию этой статьи не будет), если у вас есть желание сделать его лучше — добро пожаловать в коментарии.
Внимательные читатели спросят, мол как так: в классе который борется за перформанс используется Foreach-Object
!? Это правда, и приведенный пример один из случаев уместного использования этого командлета и вот почему: в отличие от foreach
, командлет Foreach-Object
не дожидается полного выполнения команды отправляющей данные в пайп — обработка происходит потоково, в конкретной ситуации, например, генерируя исключения сразуже, а не дожидаясь окончания процесса синхронизации. Парсинг вывода утилиты подходящее этому командлету место.
Использование описанного выше враппера до банального простое, стоит лишь добавить обработку исключений:
$Robocopy = New-Object Robocopy
# копирование одного файла
$Robocopy.CopyFile( $Source, $Dest )
# синхронизация папок
$Robocopy.SyncFolders( $SourceDir, $DestDir )
# синхронизация только файлов .xml и установка архивного бита
$Robocopy.SyncFolders( $SourceDir, $DestDir , '*.xml', $true )
# синхронизация всех файлов кроме *.zip *.tmp *.log и установка архивного бита
$Robocopy.SyncFolders( $SourceDir, $DestDir, '*.*', '*.zip *.tmp *.log', $true )
Послевкусие
Производительность скриптов тема обширная и отчасти холиварная — микрооптимизации могут забрать времени на реализацию и тестирование больше чем принесут, поддерживаемость и читаемость кода может снизиться настолько, что цена его поддержки будет выше чем профит от использования такого решения; вместе с тем, есть ряд простых рекомендаций делающих ваш код проще, понятне и быстрее, стоит лишь начать их использовать:
-
использовать оператор
foreach
вместо командлетаForeach-Object
в скриптах; -
минимизировать количество пайплайнов;
-
читать/писать файлы разом, а не построчно;
-
использовать
StringBuilder
и прочие специализированные классы; -
профилировать код и понимать узкие места, прежде чем что-то оптимизировать;
-
не стыдиться вызывать нативные бинарники (пастить «батники» в скрипты не стоит);
И пожалуй главное еще раз: не спешите оптимизировать что-то без реальной необходимости, преждевременная оптимизация может всё испортить.
Jobs
Бывает так, что вы уже оптимизировали всё, что казалось необходимым и пришли к некоторому компромису, между читаемость и скоростью, но то ли данных стало больше, то ли дальнейшие алгоритмические трюки себя не оправдывают, а сократить время работы нужно. В этом случае незаменимым помощником может стать параллельность исполнения некоторых частей кода. Убедиться стоит разве что в том, что не будет проблем с IO, если вы вдруг решили что скорости диска хватит на любое количество потоков вмещающихся в память.
Минутка юмора на счет ssd
Вот так происходит первая загрузка свежеустановленной Windows Server 2019 в Hyper-V на ssd (решилось миграцией виртуалки на hdd):
Со второй версии PowerShell доступны командлеты для работы с заданиями (Get-Command *-Job
), подробнее можно почитать например тут.
Ничего концептуально сложного в них нет, оформляем скриптблок, запускаем задание, получаем результаты в нужный момент:
$Job = Start-Job -ScriptBlock {
Write-Output 'Good night'
Start-Sleep -S 10
Write-Output 'Good morning'
}
$Job | Wait-Job | Receive-Job
Remove-Job $Job
Пример выше призван уместить необходимые командлеты на минимальной единице площади, а не является хорошим паттерном написания асинхронного кода — не стоит делать асинхронный вызов для того, чтобы подождать его завершения. В качестве нестареющего примера использования джобов я рекомендую вам разобраться и подебажить вот этот скрипт распределенного пинга подсети.
Если вы решили не открывать ссылку, еще одна попытка с моей стороны:
https://xaegr.wordpress.com/2011/07/12/threadping/
Проблема, которая вроде бы и не проблема, но обозначена — каждый джоб хочет немного памяти, что бы быть быстрее и запускается полноценным процессом операционной системы со всеми плюсами и минусами этого подхода. Вот так, например, умирает приведенный выше джоб (50 мегабайт — это 50 мегабайт):
Джобы помогут решить любые ваши задачи параллельно и сделать это удобно. Обязательно изучите этот механизм, джобы — это лучший выбор для простого решения задачи на довольно высоком уровне абстракции, при этом уложиться получится в минимальное количество строк. Помните одну важную вещь — ваши скрипты должны быть читаемыми для людей которые придут после вас, скрипты пишут для людей.
Но бывает так, что этой абстракции перестает хватать в силу архитектурных ограничений такого решения, например, сложно в такой парадигме сделать интерактивный гуи с биндингом значений на форме к каким-то переменным.
Runspaces
Концепции ранспейсов посвещена целая серия статей статей в блоге майкрософта и я очень рекомендую обратиться к первоисточнику — Beginning Use of PowerShell Runspaces: Part 1. Коротко, ранспейс — это отдельный поток PowerShell который работает в том же процессе операционной системы, от того не имея оверхеда на новый процесс. Если концепция легких потоков вам нравится и вы хотите пускать их десятками (нет, концепции каналов в PowerShell нет), то у меня для вас хорошая новость: для удобства вся низкоуровневая логика вот в этом репозитарии модуля на гитхаб (там есть гифки) уже обернута в более знакомую концепцию джобов. А пока покажу как работать с ними руками, но первую ссылку из этого абзаца не забывайте посетить в любом случае.
В качестве примера использования ранспейсов могу привести скелет простой WPF формы, отрисовка которой происходит в том же потоке операционной системы что и основной процесс PowerShell, но в отдельном потоке рантайма. Взамодействие с ним происходит через потокобезопасный хэштейбл — вам не нужно писать никаких мьютексов, всё уже работает. Плюс такого подхода — вы можете в основном скрипте реализовать любой сложности и длительности работы алгоритм, блокировка которым основного потока исполнения не приведет к «зависанию» формы. Пруф в последней строке скрипта.
В конкретном примере запускается только один ранспейс, хотя ничего не мешает вам породить еще парочку в случае необходимости и завести им пул для удобства.
# Хештейбл синхронизированный между потоками
$GUISyncHash = [hashtable]::Synchronized(@{})
<#
WPF форма
#>
$GUISyncHash.FormXAML = [xml](@"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Sample WPF Form" Height="510" Width="410" ResizeMode="NoResize">
<Grid>
<Label Content="Пример формы" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="37" Width="374" FontSize="18"/>
<Label Content="Откуда" HorizontalAlignment="Left" Margin="16,64,0,0" VerticalAlignment="Top" Height="26" Width="48"/>
<TextBox x:Name="BackupPath" HorizontalAlignment="Left" Height="23" Margin="69,68,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="300"/>
<Label Content="Куда" HorizontalAlignment="Left" Margin="16,103,0,0" VerticalAlignment="Top" Height="26" Width="35"/>
<TextBox x:Name="RestorePath" HorizontalAlignment="Left" Height="23" Margin="69,107,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="300"/>
<Button x:Name="FirstButton" Content="√" HorizontalAlignment="Left" Margin="357,68,0,0" VerticalAlignment="Top" Width="23" Height="23"/>
<Button x:Name="SecondButton" Content="√" HorizontalAlignment="Left" Margin="357,107,0,0" VerticalAlignment="Top" Width="23" Height="23"/>
<CheckBox x:Name="Check" Content="Сделать мне хорошо" HorizontalAlignment="Left" Margin="16,146,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.113,-0.267" Width="172"/>
<Button x:Name="Go" Content="Go" HorizontalAlignment="Left" Margin="298,173,0,0" VerticalAlignment="Top" Width="82" Height="26"/>
<ComboBox x:Name="Droplist" HorizontalAlignment="Left" Margin="16,173,0,0" VerticalAlignment="Top" Width="172" Height="26"/>
<ListBox x:Name="ListBox" HorizontalAlignment="Left" Height="250" Margin="16,210,0,0" VerticalAlignment="Top" Width="364"/>
</Grid>
</Window>
"@)
<#
Поток формы
#>
$GUISyncHash.GUIThread = {
$GUISyncHash.Window = [Windows.Markup.XamlReader]::Load(( New-Object System.Xml.XmlNodeReader $GUISyncHash.FormXAML ))
$GUISyncHash.Check = $GUISyncHash.Window.FindName( "Check" )
$GUISyncHash.GO = $GUISyncHash.Window.FindName( "Go" )
$GUISyncHash.ListBox = $GUISyncHash.Window.FindName( "ListBox" )
$GUISyncHash.BackupPath = $GUISyncHash.Window.FindName( "BackupPath" )
$GUISyncHash.RestorePath = $GUISyncHash.Window.FindName( "RestorePath" )
$GUISyncHash.FirstButton = $GUISyncHash.Window.FindName( "FirstButton" )
$GUISyncHash.SecondButton = $GUISyncHash.Window.FindName( "SecondButton" )
$GUISyncHash.Droplist = $GUISyncHash.Window.FindName( "Droplist" )
$GUISyncHash.Window.Add_SourceInitialized({
$GUISyncHash.GO.IsEnabled = $true
})
$GUISyncHash.FirstButton.Add_Click( {
$GUISyncHash.ListBox.Items.Add( 'Click FirstButton' )
})
$GUISyncHash.SecondButton.Add_Click( {
$GUISyncHash.ListBox.Items.Add( 'Click SecondButton' )
})
$GUISyncHash.GO.Add_Click( {
$GUISyncHash.ListBox.Items.Add( 'Click GO' )
})
$GUISyncHash.Window.Add_Closed( {
Stop-Process -Id $PID -Force
})
$null = $GUISyncHash.Window.ShowDialog()
}
$Runspace = @{}
$Runspace.Runspace = [RunspaceFactory]::CreateRunspace()
$Runspace.Runspace.ApartmentState = "STA"
$Runspace.Runspace.ThreadOptions = "ReuseThread"
$Runspace.Runspace.Open()
$Runspace.psCmd = { Add-Type -AssemblyName PresentationCore, PresentationFramework, WindowsBase }.GetPowerShell()
$Runspace.Runspace.SessionStateProxy.SetVariable( 'GUISyncHash', $GUISyncHash )
$Runspace.psCmd.Runspace = $Runspace.Runspace
$Runspace.Handle = $Runspace.psCmd.AddScript( $GUISyncHash.GUIThread ).BeginInvoke()
Start-Sleep -S 1
$GUISyncHash.ListBox.Dispatcher.Invoke( "Normal", [action] {
$GUISyncHash.ListBox.Items.Add( 'Привет' )
})
$GUISyncHash.ListBox.Dispatcher.Invoke( "Normal", [action] {
$GUISyncHash.ListBox.Items.Add( 'Наполняю выпадающее меню' )
})
foreach ( $item in 1..5 ) {
$GUISyncHash.Droplist.Dispatcher.Invoke( "Normal", [action] {
$GUISyncHash.Droplist.Items.Add( $item )
$GUISyncHash.Droplist.SelectedIndex = 0
})
}
$GUISyncHash.ListBox.Dispatcher.Invoke( "Normal", [action] {
$GUISyncHash.ListBox.Items.Add( 'While ( $true ) { Start-Sleep -S 10 }' )
})
while ( $true ) { Start-Sleep -S 10 }
Еще один пример работы с WPF можете посмотреть в моём репозитории на github, там один поток и всё довольно просто, ну и еще он позволяет читать smart диска: https://github.com/snd3r/GetDiskSmart/. А ещё там можно посмотреть пример биндинга объектов к форме, когда работает магия MVVM:
Если на вашем компьютере не стоит старшая Visual Studio, например потому что ваша организация не удовлетворяет требований к бесплатному использованию Community Edition или у вас нет желания добавлять в систему программу установка которой будет необратима без резервной копии раздела, то на гитхабе есть простой инструмент для рисования простых xaml-форм для wpf — https://github.com/punker76/kaxaml
Вместо заключения
PowerShell — мощная и удобная среда для работы с Windows-инфраструктурой. Он хорош концептуально, он удобен своим синтаксисом и самодокументирующими названиями командлетов, он может в перформанс себя как среды и вас как специалиста, стоит лишь разобраться в концепциях которыми он оперирует и начать получать удовольствие.
Когда я начинал писать эту статью, в планах была небольшая заметка по оформлению кода и наглядной демонстрации преимуществ ответственного отношения к стайлгайдам, под названием «PowerShell, хорошие практики», но Остапа понесло. Опыта написания такого размера технических статей у меня до этого небыло, поэтому прошу извинить за некоторую сумбурность повествования — писалось всё последовательно из головы, в лучших традициях снятия дампа сознания. При этом я старался давать ссылки на заслуживаюшие внимания ресурсы и рекомендую вам сделать все их фиолетовыми. Эти ссылки хоть на деле и из первой страницы гугла, но во-первых гугол у всех разный, во-вторых я все же сверял свои ожидания с контентом и только в случае совпадения добавлял в статью.
Если у вас есть что добавить из своего опыта — добро пожаловать в комментарии.
P.S. Boomburum, не поддерживать в 2019 подсветку синтаксиса powershell — стрёмный стрём.