При создании функции Powershell мы можем передать в нее разные параметры использую атрибут Parameters() и блок param(). Эти параметры могут быть использованы при работе в конвейере, валидации типов данных, определении обязательности их использования, значений по умолчанию и многого другого. В этой статье мы разберем как использовать Powershell Parameters() и param() на примерах.
Где и как использовать param и Parameters
Создавая функции (команды) мы преследуем одну цель - сделать возможность использования какого-то кода многократным. С таким подходом следующая функция имеет право на жизнь:
function Test-Site{
ping 8.8.8.8
}
Ситуация меняется, например, когда вам нужно пинговать разные хосты. Самым плохим вариантом будет дублирование предыдущего кода:
function Test-Site1{
ping 8.8.8.8
}
function Test-Site2{
ping 6.6.6.6
}
Такой подход нарушает один из основных принципов программирования DRY (dont repeat yourself). Его смысл простой - избегайте дублирование написанного кода. Дублирование кода понижает риск ошибок и дальнейшей поддержки. Это основной смысл функций.
Для соблюдения принципа DRY есть несколько вариантов.
Переменная args
В Powershell, так же как и во многих других языках, существует переменная 'args'. Смысл в этой переменной хранить все переданные значения в коллекции (так же называются списки, листы или просто массив).
Эта переменная создается автоматически при вызове команды:
function Test-Site1{
Write-Host "Тип данных: $($args.GetType())"
Write-Host "Переданные значения: $args"
}
Что бы получать доступ к каждому значение мы должны обращаться к нему по индексу:
function Test-Array{
Write-Host "Первый элемент: $($args[0])" -ForegroundColor Cyan
Write-Host "Второй элемент: $($args[1])" -ForegroundColor DarkMagenta
}
Одна из проблем, которая появляется при использовании '$args', неочевидность использования. Спустя время вы, или другой человек, можете не вспомнить про порядок передаваемых значений, их типы данных и т.д. Нужно будет открывать код и читать его что бы разобраться в этом.
Именованные параметры
Более очевидным способом передачи значений - использование именованных параметров. Мы присваиваем значение определенным ключам (параметрам). Такая реализация чаще всего применяется в создании функций.
Использование ключей выглядит так:
Test-NetConnection -ComputerName '8.8.8.8' -Hops 1
Вряд ли у вас возникнут сомнения в том, что должно находится в параметре "ComputerName" или "Hops". Это нельзя гарантировать при использовании "$args".
Что бы реализовать параметры в вашей функции существует 2 подхода.
Первый способ похож на реализацию параметров в большинстве языков, мы просто помещаем их в круглые скобки:
function Test-Site($dest1,$dest2){
ping $dest1
ping $dest2
}
Test-Site -dest1 '8.8.8.8' -dest2 '6.6.6.6'
Второй способ реализуется через блок param() в котором мы объявляем параметры:
function Test-Site{
param(
$dest1,
$dest2
)
ping $dest1
ping $dest2
}
Test-Site -dest1 '8.8.8.8' -dest2 '6.6.6.6'
Оба примера отменяют возможность работы переменной "$args" - массив просто будет пустым.
Блок 'param' более предпочтительный способ. Он принят сообществом как стандарт передачи параметров. Кроме этого он позволяет использовать расширенный функционал.
Расширенные функции с CmdletBinding и Parameter
В Powershell есть понятие расширенных функций. Эти функции создаются при наличии одного или двух атрибутов: 'CmdletBinding ' и 'Parameter'. Благодаря им у вашей команды автоматически появляется следующий список ключей (указан ключ и алиас):
- -Debug (-db)
- -ErrorAction (-ea)
- -ErrorVariable (-ev)
- -InformationAction (-infa)
- -InformationVariable (-iv)
- -OutBuffer (-ob)
- -OutVariable (-ov)
- -PipelineVariable (-pv)
- -Verbose (-vb)
- -WarningAction (-wa)
- -WarningVariable (-wv)
Атрибут 'CmdletBinding()' - может хранить в себе значения, которые касаются всей функции без привязки к конкретному параметру. В статье будут указаны некоторые его применения. Определяется этот атрибут до блока 'param':
function Get-Data{
[CmdletBinding()]
param(
$Name
)
Write-Host $Name
}
Get-Data -Name 'Alex' -ErrorAction SilentlyContinue
Атрибут 'Parameter' используется внутри 'param'. Он используется для проверки (валидации) определенных параметров. Эти проверки могут быть разными: может ли ключ принимать данные с конвейера, какова его максимальна длина, является ли он обязательным и т.д. Блок 'Parameter' можно дублировать.
Пример использования (только для демонстрации синтаксиса):
function Test-Site{
[CmdletBinding()]
param(
[Parameter(Проверка1,Проверка2)]
[Parameter(Проверка3=$true)]
$dest1,
[Parameter(Проверка1,Проверка4)]
$dest2
)
ping $dest2
}
Так как я сам легко путаюсь в понятии параметр, аргумент и атрибут, ниже скриншот с небольшим объяснением:
- Все что помещается в квадратные скобки - это атрибуты. На скриншоте, под номерами 1 и 2, выделена их часть;
- Параметры это то, что мы передаем в функцию/команду. Я, например, их часто называю ключами. На скриншоте они находятся под номерами 3 и 4;
- Внутри атрибутов находятся аргументы. Это просто характеристики параметра. Часть из них выделена под номером 5. Аргументами так же называют значения, которые передаются в параметр.
Переменная $PSBoundParameters
После того, как у функции появляются параметры, переменная "$args" становится пустой. Вместо нее создается переменная $PSBoundParameters. Переменная хранит хэш-таблицу с ключами-значениями:
function Test-Variables($aa){
echo "Переменная args: " $args
echo "Хэш-таблица: " $PSBoundParameters
}
Test-Variables -aa "Test"
Использование param в файлах
Эта статья рассматривает использования 'param()' в функциях т.к. это наиболее популярный способ использования. Тем не менее вы можете поместить этот блок в файл:
param(
$dest1,
$dest2
)
ping $dest2
Далее мы можем вызвать этот файл и передать в него значения:
.\SomeScript.ps1 -dest1 'google.com' -dest2 'anothersite'
Если до блока 'param()' будет какой-то код (кроме комментариев), произойдет ошибка:
- param : Имя "param" не распознано как имя командлета, функции, файла сценария или выполняемой программы;
- The term 'Param' is not recognized as the name of a cmdlet
Обязательные Mandatory аргументы
Работая с другими командами Powershell вы замечали, что какие-то параметры являются обязательными к заполнению, а какие-то нет. По умолчанию все параметры опциональны. Что бы сделать их обязательными к заполнению используется аргумент 'Mandatory'. Обязательность использования параметра указывается в 'Parameter()':
function Test-Site{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
$dest1,
# у параметра dest2 нет проверок
$dest2
)
ping $dest2
}
Если вы не используете параметр явно, то он у вас будет запрошен. Это правило, в случае выше, касается только одного параметра так как он является обязательным.
Обратите внимание, что объявление 'Mandatory' не снимает с вас обязанности проверки на корректность переданных данных. Это дает лишь возможность остановить работу функции еще на первоначальном этапе.
Проверка типов и значение по умолчанию
Хороший прием по предотвращению ошибок - добавить проверку типов данных в начале функции. В Powershell много разных типов данных - далее перечислены основные:
- [string] - строка;
- [int] - число;
- [bool] - True/False;
- [float] - число с плавающей точкой;
- [DateTime] - дата и время;
- [array] - массив;
- [hashtable] - массив ключ-значение;
- [PSCustomObject] - массив ключ-значение.
Эта проверка выполняется до объявления переменной.
function Check-Type{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Data
)
$Data
$Data.GetType()
}
Check-Type -Data 1
Check-Type -Data $True
Обратите внимание, что в функцию передается булево значение и число. Так как они могут быть преобразованы в строки - эта конвертация происходит. В случае массивов произошла бы ошибка т.к. он не может быть преобразован в строку.
У некоторых типов вы можете добавить скобки '[]'. Такой прием говорит, что вы ожидаете получить массив определенных типов. В случае строк '[string]' массив строк будет выглядеть '[string[]]' (ниже пример на числах):
function Check-Type{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[int[]]$Data
)
$Data
$Data.GetType()
}
Check-Type -Data '12'
Объявить значение по умолчанию мы можем только у аргумента, который не является 'Mandatory'. Это делается следующим образом:
function Test-Site{
[CmdletBinding()]
param(
[string]$dest1=1
)
$dest1
$dest1.GetType()
}
Test-Site
switch
Аналогичным образом мы можем объявить переменную типа switch. Если параметр будет указан, то такая переменна будет иметь значение 'True':
function Do-Stuff{
[CmdletBinding()]
param(
[switch]$var
)
Write-Host "Параметр указан: $($var.IsPresent)"
}
Такой подход уменьшает риск ошибок и сокращает время на написание команды. Использование switch похоже на bool, но у них есть разница. В случае '[bool]' нужно будет указывать значение параметра.
Разделение параметров на группы с ParameterSetName
В ситуациях, когда в функцию нужно передать разный набор обязательных параметров, мы можем использовать атрибут ParameterSetName. Для примера можно посмотреть на описание синтаксиса команды 'Get-EventLog':
Get-Command Get-EventLog -Syntax
У нас выводится 2 возможных варианта использования команды. Для каждого из варианта использования команды могут быть определены свои индивидуальные ключи. Именно для этого используется ParameterSetName.
Создадим команду с 2-умя группами. Первая группа 'Group1' работает с именами пользователей, а 'Group2' только с идентификаторами и требует дополнительно параметра:
function Get-MessageInfo{
[CmdletBinding()]
param(
[Parameter(Mandatory, ParameterSetName='Group1')]
$User,
[Parameter(Mandatory, ParameterSetName='Group2')]
$ID,
[Parameter(Mandatory, ParameterSetName='Group2')]
$Message
)
if ($ID){
return "Выбрана 2-оя группа с ID $ID и Message $Message"
}
return "Выбрана 1-ая группа с User $User"
}
Get-MessageInfo -User "Alex"
Get-MessageInfo -ID 123
На скриншоте видно, что если мы введем 'ID', то появится диалог с вводом 'Message'. Вы так же можете попробовать использовать кнопку 'tab' после выбора одной из групп - параметры из другой группы перестанут предлагаться.
Если вы попробуете запустить команду без указания каких либо групп - получите ошибку:
- Не удается разрешить набор параметров с использованием указанных именованных параметров.
- parameter set cannot be resolved using the specified named parameters
Группу, которую вы планируете использовать по умолчанию, можно определить в 'CmdletBinding' используя атрибут 'DefaultParameterSetName':
[CmdletBinding(DefaultParameterSetName='Group1')]
Один ключ может находится в нескольких группах. Для этого, группу, нужно указать в разных 'Parameter()'. Каждый аргумент, указанный в Parameter, будет действовать для определенной группы. Так, в случае ниже, в одной группе "$Message" является обязательным, а в другой опциональным параметром:
function Get-MessageInfo{
[CmdletBinding()]
param(
[Parameter(Mandatory, ParameterSetName='Group1')]
$User,
[Parameter(Mandatory, ParameterSetName='Group2')]
$ID,
[Parameter(Mandatory, ParameterSetName='Group1')]
[Parameter(ParameterSetName='Group2')]
$Message
)
if ($ID){
return "Message не используется"
}
return "Указан $Message"
}
Get-MessageInfo -ID 123
Get-MessageInfo -User "Alex"
Определение выбранной группы с $PSCmdlet
После вызова команды создается переменная $PSCmdlet. В этой переменной хранятся разные свойства команды. Одно из таких свойств 'ParameterSetName', который показывает выбранную группу. Это может понадобиться что бы более удобно расписать логику:
function Get-Sum{
[CmdletBinding()]
param(
[Parameter(Mandatory, ParameterSetName='Group1')]
$a,
[Parameter(Mandatory, ParameterSetName='Group1')]
[Parameter(Mandatory, ParameterSetName='Group2')]
$b,
[Parameter(Mandatory, ParameterSetName='Group2')]
$c
)
$group = $PSCmdlet.ParameterSetName
Write-Host "Выбрана группа $group"
# в зависимости от группы
# разные действия
if ($group -eq 'Group1'){
return $a + $b + 5
}
else{
return $с + $b + 10
}
}
Если какой-то параметр не будет принадлежать к конкретной группе - его можно будет использовать в любых комбинациях.
Позиционные параметры с Position
При создании команды каждый параметр имеет свою позицию. Эти позиции соответствуют порядку их объявления внутри функции:
function Get-Sum{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
$a,
[Parameter(Mandatory)]
$b,
[Parameter(Mandatory)]
$c
)
return $a + $b + $c
}
Так как позиционное использование параметров может привести к проблемам - вы можете запретить их не использование в 'CmdLetBindint':
[CmdletBinding(PositionalBinding=$false)]
'PositionalBinding' запрещает неявное использование параметров - это когда вы не объявляли их сами. С помощью аргумента 'Postion' вы можете объявить их явно:
function Get-Sum{
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory, Position=0)]
$a,
[Parameter(Mandatory)]
$b,
[Parameter(Mandatory, Position=1)]
$c
)
return $a + $b + $c
}
Get-Sum 1 2 -b 3
Такой подход часто используется в обычных командах Powershell. Параметры типа 'Name' или 'ComputerName' могут работать позиционно, а остальные нет.
Параметры конвейера
Для использования конвейера можно определить несколько аргументов:
- ValueFromPipeline - работает с массивами. Может быть использован в функции единожды;
- ValueFromPipelineByPropertyName - принимает массивы ключ значение. Можно использовать множество раз.
Создадим функцию, которая будет принимать оба параметра:
function Get-Data{
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
[string]$Message
)
process {
$Message
}
}
@(1,2,3) | Get-Data
[PSCustomObject]@{Message='Hello there';} | Get-Data
В отличие от всех предыдущих примеров, при создании конвейера, мы должны использовать блок Process. Это единственный блок, функционал которого будет повторяться для каждого элемента переданного через конвейер. Если вы его не будете использовать - функция будет выполнена только для последнего элемента. Более подробно конвейер обсуждался в предыдущей статье: "Разбираем работу конвейера Powershell на примере команд и создании функции".
Единственный момент, который не оговорен в той статье, 'ValueFromPipeline' можно использовать несколько раз, но в определенном случае. Если у вы используете группировку (разделяете параметры на группы) и указываете один из параметров явно:
function Get-Salary{
[CmdletBinding(DefaultParameterSetName='Group1')]
param(
[Parameter(Mandatory, ParameterSetName='Group1', ValueFromPipeline)]
$User,
[Parameter(Mandatory, ParameterSetName='Group2', ValueFromPipelineByPropertyName)]
$ID,
[Parameter(Mandatory, ParameterSetName='Group2', ValueFromPipeline)]
$Message
)
process {
if ($ID){
return "Выбрана 2-оя группа с ID $ID и Message $Message"
}
return "Выбрана 1-ая группа с User $User"
}
}
# Явно указали параметр ID
# значит используем 2-ую группу
@(1,2,3) | Get-Salary -ID 1
# Используем группу по умолчанию
@(1,2,3) | Get-Salary
Включаем значения вне параметров с ValueFromRemainingArguments
У вас может быть ситуация, когда вы не можете предусмотреть параметр для конкретного значения. Это так же может быть проблема с ограничением Powershell на 32 параметра. В этом случае вы можете использовать 'ValueFromRemainingArguments', который включает все значения не привязанных к каким-то параметрам.
Атрибут 'ValueFromRemainingArguments' объявляется следующим образом:
function Get-Data{
[CmdletBinding()]
param(
$SomeMessage,
[Parameter(Position=1, ValueFromRemainingArguments)]
$Others
)
Write-Host "Эти значения не привязаны к параметрам $Others"
}
Get-Data -SomeMessage 1 2 3 4 5
Обратите внимание, что порядок в этом случае не важен.
Атрибуты валидации параметров
Значения у параметров могут проходить разные проверки. Для этих проверок существуют разные атрибуты:
- [AllowNull()] - позволяет использовать $null у параметров с другим типом данных. Если вы объявляете, например, тип параметра '[array]', а передаете $null, произойдет ошибка. Этот параметр помогает ее избежать у 'Mandatory';
- [AllowEmptyString()] - то же самое, что и 'AllowNull', но если у вас указан параметр типа '[string]';
- [AllowEmptyCollection()] - аналогичен предыдущем двум, но для коллекций, например [string[]];
- [ValidateNotNull()] - проверяет что не передан $null. Нужен для не 'Mandatory' параметров;
- [ValidateNotNullOrEmpty()] - проверяет, что в параметр не равен $null и пустой строке. Так же используется не для '[Mandatory]' параметров;
- [ValidateCount(1,5)] - минимальное и максимальное количество значений переданных через параметр;
- [ValidateLength(1,10)] - минимальная и максимальная длина в параметре. В том числе можно использовать аргументы: 'Positivem', 'Negative', 'NonPositive' (0 или меньше), 'NonNegative' (0 или больше);
- [ValidateRange(0,10)] - проверка на минимальное и максимальное значения числа;
- [ValidatePattern("[0-9][0-9][0-9][0-9]")] - проверка по шаблону регулярных выражений;
- [SupportsWildcards()] - проверка на простые регулярные выражения в виде "*","?","[]" и т.д.;
- [ValidateScript({$_ -ge (get-date)})] - проверка через дополнительный скрипт;
- [ValidateSet("Low", "Average", "High")] - ограничение выбора значений;
- [ValidateDrive('C','D')] - проверка, что значение является путем и относится к диску 'C' или 'D';
- [ValidateUserDrive] - проверяет, что путь начинается с 'User:\'. Используется в JEA;
- DynamicParam {<statement-list>} - динамические параметры, которые работают в зависимости от условий. Например функция может возвращать публичную и приватную информацию. Если запрошена приватная информация - дополнительно нужно ввести пароль (появиться дополнительный параметр).
Рассмотрим некоторые из этих атрибутов.
Проверка на $null
При передаче результата работы одной функции другой не всегда можно быть уверенным в результате. Вы легко можете получить $null (ничего), который не относится к типам данных. Передача в функцию такой переменной, если параметр помечен как 'Mandatory', приведет к ошибке:
Function Get-SomeInfo {
Param(
[Parameter(Mandatory)]
[array]$Data
)
$Data
}
Get-SomeInfo -Data $null
Ошибки при этом могут быть разными:
- Не удается привязать аргумент к параметру "Data", так как он имеет значение NULL
- Не удается привязать аргумент к параметру "A1", так как он представляет собой пустую строку. Cannot bind argument to parameter 'name' because it is an empty.
- Не удается преобразовать значение "" в тип "System.Boolean". Логические параметры поддерживают только логические значения и числа
- Не удается привязать аргумент к параметру "A1", так как он имеет значение NULL.
Если в вашей функции это ожидаемый результат - вы можете допустить передачу "$null" с "[AllowNull()]":
Function Get-SomeInfo {
Param(
[Parameter(Mandatory)]
[AllowNull()]
[array]$Data
)
$Data
}
Get-SomeInfo -Data $null
'AllowNull()' работает для типов '[array]' и других типов данных, кроме строк и коллекций. Для них нужно использовать '[AllowEmptyString()]' и '[AllowEmptyCollection()]'.
Перед указанием таких атрибутов стоит проверить во что будет преобразован ваш тип данных, например:
# преобразуется в 0
[int]$null
# в False
[bool]$null
# в False
[switch]$null
# в $null
[PSCustomObject]$null
# в пустую строку
[string]$null
У параметров, которые не помечены 'Mandatory', возникнет противоположная проблема. Они пропускают $null без ошибок:
Function Foo {
Param(
[array]$A1
)
Write-Host $A1
}
Foo -A1 $Null
Что бы остановить выполнение скрипта при $null мы можем пометить параметр как '[ValidateNotNullOrEmpty]' или '[ValidateNotNullOrEmpty()] ':
Function Foo {
Param(
[ValidateNotNull()]
[array]$A1
)
Write-Host $A1
}
Foo -A1 $Null
Подсчет значений
Вы можете считать количество элементов в массиве (ValidateCount), длину строк (ValidateLength) и числа (ValidateRange). Эти атрибуты работают одинаково - вам нужно указать минимальное и максимальное значение:
Function Bar {
Param(
[ValidateCount(0,5)]
$A1
)
"pass"
}
Bar -A1 @{'Foo1'=$null; 'Foo2'=$null; 'Foo3'=$null; 'Foo4'=$null; 'Foo5'=$null;}
Bar -A1 @(1,2,3,4,5)
Bar -A1 @(1,2,3,4,5,6)
Проверка по скрипту
Вы можете создать свою проверку параметров с помощью "ValidateScript". Так вы создадите параметр, который запретит указывать прошедшие даты:
Function Bar {
Param(
[ValidateScript({$PSItem -ge $(Get-Date)})]
[datetime]$A1
)
$A1
}
Bar -A1 $(Get-Date)
Bar -A1 $(Get-Date).AddSeconds(1)
Ошибка происходит из-за того, что команда отправляет данные, допустим в 11.1 секунд, а валидация в 11.2. Во втором примере мы добавляем 1 секунду что бы ошибки не было.
Вместо "$PSItem" можно использовать "$_".
Проверка по шаблонам регулярных выражений
Мы можем привязать к параметру проверку на простые регулярные выражения (SupportsWildcards) и расширенные (ValidatePattern). Так мы убедимся, что в параметр передают валидный IP адрес:
Function Check-IP {
Param(
[ValidatePattern("\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}\b")]
[string]$IP
)
'pass'
}
Check-IP -IP '127.0.0.1'
Check-IP -IP '256.0.0.1'
Ограничение выбора значений с ValidateSet
В некоторых командах есть возможность выбора значений у параметров. Такой выбор осуществляется через кнопку 'tab'. Вы можете проверить это нажав 'tab' в следующей команде:
Test-NetConnection -ErrorAction
У вас будет выполняться переключение между значениями: Continue, Ignore, Inquire и т.д.
Подобный выбор задается в атрибуте ValidateSet:
function Get-Salary{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateSet('Саши', 'Маши')]
$User
)
return "Пароль $user 123"
}
Если вы укажете значение не прописанное в 'ValidateSet', то получите ошибку:
- Get-Salary : Не удается проверить аргумент для параметра "User". Аргумент "Маши1"
Передача ключей и значений в одной переменной (Splatting)
Обычный вызов команды выглядит следующим образом:
Get-Date -Year 2021 -Day 10 -Month 8
Мы можем взять все ключи и значения из этой команды, поместить в hashtable и передать в команду как набор параметров:
$date = @{
'Year'='2021';
'Day'='10';
'Month'='8';
}
Что бы Powershell понял, что мы передаем не одну переменную, а набор ключей и значений, мы должны ее пометить знаком '@':
Get-Date @date
Такой подход называется 'Splatting' (брызги) и немного повышает читаемость кода.
Если в hashtable будет находиться дополнительный параметр, который не реализован в команде, то выйдет ошибка:
- Не удается найти параметр, соответствующий имени параметра
- A parameter cannot be found that matches parameter name
Обычные массивы так тоже можно передавать, но вы должны знать позиции под которыми должны располагаться значения:
function Test-Funct{
param(
[Parameter(Position=0)]
$pos1,
[Parameter(Position=1)]
$pos2,
[Parameter(Position=2)]
$pos3
)
Write-Host "Позиция $pos1"
Write-Host "Позиция $pos2"
Write-Host "Позиция $pos3"
}
$positions = @(1;2;3)
Test-Funct @positions
Используя splatting, в Powershell 7+, вы можете перезаписывать значения из метода splatting:
$message = @{
'Object'='Hello world';
'BackgroundColor'='Blue';
}
$new_message = 'New message'
Write-Host @message -Object $new_message
Алиасы
К параметрам можно привязать алиасы (короткие имена/псевдонимы). Это делается через атрибут 'Alias()'. Ограничений в количестве атрибутов нет:
function Get-PCName{
Param(
[Parameter(Mandatory=$true)]
[Alias('Computer','PC','IP','Address')]
$Name
)
$Name
}
Get-PCName -Computer 'SRV1'
Get-PCName -PC 'SRV1'
Get-PCName -IP 'SRV1'
Get-PCName -Address 'SRV1'
...
Подписывайтесь на наш Telegram канал
Теги: #powershell