Автор: Sergey Teplyakov

Я не понимаю PowerShell. А раз так, то нужно попробовать устаканить свои знания и поделиться ими с миром.

Главная проблема PowerShell, ИМХО, динамическая типизация и адаптивная система типов. PowerShell всеми силами старается избежать ошибки времени исполнения путем конвертации типов туда и обратно (Хоббит, блин!).

Чтобы избежать этого, я стараюсь сделать контракт методы максимально четким. Если что-то должно быть строкой, то я хочу упасть как можно раньше, если кто-то подсунет что-то другое. Если аргумент не может быть Null, то падать нужно как можно раньше, а не передавать его дальше по стеку вызовов. Если аргумент является обязательным и пользователь забыл его указать, то нужно четко сказать об этом, а не просить пользователя ввести его руками.

Параметр метода в PowerShell может быть $Null в одном из двух случаев: пользователь явно передал $Null в качестве значения аргумента. Или же пользователь вообще не указал данный аргумент при вызове метода. Мне, как автору метода, обычно все равно, почему параметр отсутствует. Я просто хочу гарантировать, что он не будет Null. Но, чтобы добиться этого, придется использовать разные подходы.

Обязательные и необязательные параметры в PowerShell

Первая задача, с которой я столкнулся заставить PowerShell ругаться, если пользователь забыл указать обязательный параметр. По умолчанию, все параметры в PowerShell являются необязательными:Function Foo
{
[CmdletBinding()]
param
(
[string]$arg0)
Write-Host 'Foo: `$arg0: '$arg0''
}
# Вызываем метод Foo без параметров
Foo

Казалось бы, PowerShell поддерживает возможность сделать параметр обязательным с помощью Parameter(Mandatory = $True). Но не факт, что он вам поможет. Function Foo
{
[CmdletBinding()]
param
(
[Parameter(Mandatory=$True)]
[string]$arg0
)
Write-Host 'Foo: `$arg0: '$arg0''
}

При вызове метода Foo без параметров мы не получим ошибку времени исполнения. Вместо этого, PowerShell попросит пользователя ввести обязательный параметр (!!):

Supply values for the following parameters:
arg0:

Именно таким образом работает большинство стандартных командлетов. Но это далеко не лучшее поведение, если скрипты запускаются ночью и отсутствие параметра является багом в скрипте. К тому же, добавление НОВОГО обязательного параметра в функцию нарушит принцип Открыт-Закрыт, поскольку приведет к поломке всех существующих клиентов этой функции!

ПРИМЕЧАНИЕ
На всякий случай напомню, что принцип Открыт-Закрыт заключается не в расширении иерархии фигур квадратами и кругами без изменения метода Draw, а в отсутствии эффектов бабочки: изменение в одной части системы не должны ломать другие части системы. Подробности, по ссылке выше.

Эмуляция обязательных параметров в PowerShell

Единственный известный мне способ заставить PowerShell падать в случае отсутствия аргумента, использовать следующий трюк:Function Foo
{
[CmdletBinding()]
param
(
[string]$arg0 = $(throw ''arg0' is required argument'))
Write-Host 'Foo: `$arg0: '$arg0''
}

Поскольку PowerShell это почти-expression language, то предыдущий код работает и при вызове Foo без аргументов, мы получим ошибку:

'arg0' is required argument
At line:5 char:27
+ [string]$arg0 = $(throw ''arg0' is required argument'))
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

С этим подходом есть две проблемы.

Во-первых, по умолчанию не отображается стек вызовов, что делает отладку подобных ошибок не совсем простым делом. Выше показан текст ошибки по умолчанию и в нем есть лишь место генерации исключения, но не понятно, кто и откуда вызвал метод Foo. Лечится это путем явного вывода стека вызовов:try
{
Boo
}
catch
{
$_ | Format-List * -Force
}

ПРИМЕЧАНИЕ
Подробнее о трейсинге исключений можно почитать в заметке Resolve-Error. Хотя, меня немного удивляет необходимость пляски с бубмном для базовой операции отображения причины ошибки.

Во-вторых, этот трюк не работает с аргументами, которые могут передаваться из пайплайна:Function Foo
{
[CmdletBinding()]
param
(
[Parameter(ValueFromPipeline=$True)]
[string]$arg0 = $(throw ''arg0' is required argument'
))
Write-Host 'Foo: `$arg0: '$arg0''
}
'foo' | Foo

Вызов этого кода все равно приведет к исключению: arg0 is required argument!

ПРИМАЧАНИЕ
Параметры по умолчанию ведут себя достаточно странно в PowerShell. Так, если использовать параметры по умолчанию и задать Mandatory=$True, то PowerShell все равно спросит у пользователя значение аргумента. Если же задать ValueFromPipeline=$True, и передать аргументы через пайплайн, то инициализатор по умолчанию будет вызван, но его значение будет проигнорировано!

Сейчас мы более или менее научились работать с обязательными параметрами в PowerShell, но остается и другая проблема: как не дать пользователю передать в метод $Null.

Валидация аргументов в PowerShell

Поскольку в PowerShell есть конструкции if и throw, то всегда можно воспользоваться проверкой аргументов с их помощью. Но есть и другой способ с помощью атрибутов Validate*.Function Foo
{
[CmdletBinding()]
param
(
[ValidateNotNullOrEmpty()]
[string]$arg0
)
Write-Host 'Foo: `$arg0: '$arg0''
}

Теперь, при вызове Foo $Null, мы получим сообщение с ошибкой, которое четко покажет, кто не прав и что делать:

Foo : Cannot validate argument on parameter 'arg0'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.
At line:17 char:5
+ Foo $Null

В целом, вариант вполне рабочий, только нужно помнить о нескольких моментах.

Во-первых, не забывайте использовать круглые скобки после имени атрибута. Если вместо [ValidateNotNullOrEmpty()] оставить лишь [ValidateNotNullOrEmpty], то вызов метода Foo $Null вообще может завершиться успешно! В этом случае, PowerShell будет рассматривать [ValidateNotNullOrEmpty] не как атрибут, а как аннотацию типов (!). Его не будет смущать, что их (аннотации) две и по умолчанию его даже не будет смущать, что такого типа не существует. Чтобы получить ошибку в этом случае придется включить строгий режим с помощью Set-StrictMode -Version Latest. (Я вообще рекомендую всегда работать со строгим режимом. Чем раньше упадем, тем быстрее разберемся с проблемой).

Во-вторых, нужно знать, какой атрибут использовать для тех или иных типов. Так, в случае строк, нужно использовать именно ValidateNotNullOrEmpty, а не просто ValidateNotNull, поскольку PowerShell автоматом конвертирует $Null в пустую строку.

Заключение

Вот как выглядит определение метода с одним обязательным аргументом в PowerShell:Function Foo
{
[CmdletBinding()]
param
(
[ValidateNotNullOrEmpty()]
[string]$arg0 = $(throw ''arg0' is required argument'
))
Write-Host 'Foo: `$arg0: '$arg0''
}

В этом случае, пользователь не сможет передать в метод $null, а также не сможет забыть об аргументе $arg0.

Все, что вам нужно помнить, так это разницу между валидацией аргумента и защитой от необязательных параметров. В первом случае нужно использовать атрибуты валидации, а во втором - $(throw msg).

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).