Я не понимаю 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”).
Что-то я больше убеждаюсь в том, что PythonShell или там JavaScriptShell был бы более прост и понятен :)
ОтветитьУдалитьВ свое время писал C#Script с кучей приблуд для удобной работы с файловой системой и немного с сетью имхо та джуниорская поделка была даже лучше чем повершелл, ну как минимум понятнее.
УдалитьБуйство атрибутов.
ОтветитьУдалитьPoSH (по моим ощущениям слабо знакомого с PoSH человека) создан для написания небольших скриптов "на коленке", либо вообще команд в консоли. Поэтому динамическая типизация и адаптация типов: для быстрого создания рабочих скриптов (или их прототипов). Если весь скрипт содержит даже 1000 строк, строгость архитектуры\дизайна не особо и нужна. Многое отлавливается во время теста, практически всегда передаются правильные параметры.
ОтветитьУдалитьПри работе в консоли неверные параметры вообще не страшны. Более того, удобно просто набрать в консоли (интуитивно подходящее) имя командлета, и получить подсказку, что вводить. В результате частенько проще (и быстрее) подобрать нужную комбинацию команд\параметров, чем изучать мануалы.
За валидацию спасибо. Даже не задумывался про такую возможность, и юзал if(!$arg){LogAndExcept}
Спасибо за фидбек.
УдалитьЯ вот тут несколько не согласен с
> Если весь скрипт содержит даже 1000 строк, строгость архитектуры\дизайна не особо и нужна.
У меня сейчас в моих скриптах отсилы 1500 строк, но я сразу же начал использовать модули для декомпозиции. И мне все равно сложно прыгать с места на место, чтобы удержать в голове, чего же нужно передать/вернуть. Без строгой валидации мне просто приходится "уговаривать" PS работать. Ну а когда что-то в скрипте на 1000 строк идет не так, то понять причино-следственные связи становится крайне сложно.
А можно более подробную информацию по скриптам? Насколько точная была начальная постановка (не было\были наброски\были описаны модули и интерфейсы\было детализировано почти все)? Насколько линейная (сколько функций вызываются более, чем из одного места)? Весь код лежит в одном скрипте, или скрипты вызываются друг из друга? Насколько активно использовались командлеты PS и средства .NET? Сколько изменений ожидалось, и сколько пришлось сделать?
УдалитьС моей стороны скрипта на PoSH, для работы с TFS. Изначально была крайне подробная постановка, плюс часть функционала была вынесена в шарповые утилиты. Скрипт почти линеен, каждая функция (кроме логирования) вызывается один раз, суммарная длина одного скрипта - 600 строк. Все ошибки обрабатываются одинаково - запись в лог, создание сигнального файла, выход. Сам PoSH используется по минимуму, для проверки переменных, наличия\отсутствия файлов-записей, для создания\удаления файлов\каталогов. Ключевые моменты (операции с файлами и версиями в ТФС) скинуты на библиотеки ТФС API. Само собой, максимум предварительного и покомпонентного тестирования. Пришлось сделать одно изменение, это одно изменение в одной функции (тело, контракт, вызов).
Львиная часть скрипта пришлась на формирование\чтение 10 путей (проверка на null, на существование элементов ФС, сопутствующее логирование). За счет подробного логирования и линейности скрипта местоположение ошибок находилось за минуты. Размер скрипта можно было значительно уменьшить, но читаемость решила. Минимум алиасов, минимум однострочников, максимум проверок и логирования.
Пока что предположительное среднее кол-во строк кода, когда обязательно нужно думать об архитектуре - 1000 строк. Если есть функции, используемые более, чем в одном месте - число начинает падать.
1. Начальная постановка задачи.
УдалитьБыла постановка проблемы - автоматизировать перформанс тестирование. Ничего более подробного не ставилось. Это привело к череде рефакторингов и итеративной разработке (что довольно сложно делать в скриптах).
2. Насколько линейная логика
Относительно линейная: запустить тулу несколько раз, собрать логи, залить их в базу. Сделать удобным весь этот процесс для пользователя. Каждый запуск кастомизируем: с какими аргументами запускаем, как прогреваем первый запуск, куда кладем результаты, какие дополнительные параметры пишем в базу.
3. Весь код лежит в одном скрипте?
Нет, есть модуль для аплоада результатов, есть модуль для сырового запуска тулы, есть модуль для запуска разных версий тулы с разными параметрами.
Используется а-ля фунциональной композиции, но на основе PS-ных функций.
4. Насколько много PS-а/.NET-а.
Тут получилось по феншую. Если есть в PS-е, используем встроенное, если нет встроенного, то делаем фасадную функцию, которая оборачивает .NET-й код в PS-ную функцию.
5. Сколько изменений ожидалось/сколько пришлось сделать
Тут была самая обычная итеративная разработка. Я точно не знал, что мне нужно, поэтому периодически вносил изменения то там, то сям. И тут я заметил, что динамическая природа скорее мне мешала (хотя, как известно, динамические языки хороши для прототипирования). Но мне было сложно удержать в голове задачу и еще сложнее было вносить минимальные правки: ошибка в одном месте приводила к вылету ошибки совсем в далеких местах. Например, переименовал, аргумент метода, теперь туда прилетал дефолт, который пробрасывался через три уровня функций и падал в совершенно левом месте. Собственно, вот эта статья - это результат моих попыток понять и сделать интефрейс функций максимально строгим.