Автор: 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 argumentAt 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).