PowerShell ValidateSet: Завершение по Tab & Значения параметров

При написании сценария или функции PowerShell часто требуется принимать ввод от пользователя через параметры. Если не ограничивать значения, которые могут принимать эти параметры, то нельзя гарантировать, что не будут переданы неподходящие значения. В этой статье вы узнаете, как использовать атрибут проверки параметра ValidateSet, чтобы ограничить значения только теми, которые вы определили.

При написании сценария или функции PowerShell можно использовать множество различных атрибутов проверки, чтобы проверить, что переданные значения параметров допустимы, и уведомить пользователя, если это не так.

Эта статья сосредоточена на атрибуте проверки ValidateSet. Вы узнаете, что делает ValidateSet, почему вам может понадобиться использовать ValidateSet в вашем коде и как это сделать. Вы также узнаете о функции автозаполнения, которая активируется с помощью ValidateSet и поможет пользователям вашего кода предоставлять допустимые значения параметров.

PowerShell ValidateSet: Краткий обзор

ValidateSet – это атрибут параметра, который позволяет вам определить набор элементов, которые могут быть приняты только в качестве значения для этого параметра.

Например, предположим, у вас есть сценарий, который предназначен для работы с контроллерами домена Active Directory. У этого сценария есть параметр, который указывает имя контроллера домена. Не имеет ли смысла ограничить список допустимых значений только фактическими именами контроллеров домена? Нет причины разрешать пользователю использовать “foobar” в качестве значения, когда заранее известно, какие значения нужны сценарию. ValidateSet предоставляет вам такую ​​возможность.

Требования

Эта статья будет посвящена обучающему примеру. Если вы планируете следовать за мной, вам понадобится следующее:

  • Visual Studio Code или любой другой редактор кода. Я буду использовать Visual Studio Code.
  • По крайней мере PowerShell 5.1 для большей части кода в этой статье. Есть один раздел, который требует PowerShell 6.1 или более поздней версии, и я укажу это, когда мы дойдем до этого момента.

Весь код в этой статье был протестирован в следующих средах:

Operating System PowerShell Versions
Windows 7 SP1 5.1, Core 6.2
Windows 10 1903 5.1, Core 6.2
Linux Mint 19.2 Core 6.2

Чтобы объяснить концепции вокруг ValidateSet, вы собираетесь создать небольшой скрипт под названием Get-PlanetSize.ps1. Этот скрипт возвращает информацию о размерах планет в нашей солнечной системе.

Вы начнете с простого скрипта и постепенно улучшите его способность обрабатывать ввод от конечного пользователя и сделаете обнаружение возможных значений его параметров легким для них.

Начало работы

Для начала скопируйте и вставьте приведенный ниже код PowerShell в свой любимый текстовый редактор и сохраните его как Get-PlanetSize.ps1.

$planets = [ordered]@{
     'Mercury' = 4879
     'Venus'   = 12104
     'Earth'   = 12756
     'Mars'    = 6805
     'Jupiter' = 142984
     'Saturn'  = 120536
     'Uranus'  = 51118
     'Neptune' = 49528
     'Pluto'   = 2306
 }
 $planets.keys | Foreach-Object {
     $output = "The diameter of planet {0} is {1} km" -f $_, $planets[$_]
     Write-Output $output
 }

Запустите скрипт из приглашения PowerShell, и вы должны получить следующее:

PS51> .\Get-PlanetSize.ps1
 The diameter of planet Mercury is 4879 km
 The diameter of planet Venus is 12104 km
 The diameter of planet Earth is 12756 km
 The diameter of planet Mars is 6805 km
 The diameter of planet Jupiter is 142984 km
 The diameter of planet Saturn is 120536 km
 The diameter of planet Uranus is 51118 km
 The diameter of planet Neptune is 49528 km
 The diameter of planet Pluto is 2306 km

Информативно, но не очень гибко; возвращается информация о каждой планете, даже если вам нужна информация только о Марсе.

Возможно, вы хотите иметь возможность указать одну планету вместо возвращения всех. Для этого вы можете ввести параметр. Давайте рассмотрим, как это сделать.

Получение ввода с использованием параметра

Чтобы скрипт мог принимать параметр, добавьте блок Param() в начало скрипта. Назовите параметр Planet. Подходящий блок Param() выглядит следующим образом.

В приведенном ниже примере строка [Parameter(Mandatory)] гарантирует, что имя планеты всегда передается в скрипт. Если оно отсутствует, скрипт запросит его.

Param(
     [Parameter(Mandatory)]
     $Planet
 )

Простейший способ включить параметр Planet в скрипт – изменить строку $planets.keys | Foreach-Object { на $Planet | Foreach-Object {. Теперь вы не полагаетесь на статически определенную хэш-таблицу, а читаете значение параметра Planet.

Теперь, если вы запустите скрипт и укажете планету с помощью параметра Planet, вы увидите информацию только о этой конкретной планете.

PS51> .\Get-PlanetSize.ps1 -Planet Mars
 The diameter of planet Mars is 6805 km

Отлично. Скрипт завершен? Возможно, нет.

Варианты слишком открыты

Что произойдет, если вы попытаетесь найти диаметр планеты Барсум с помощью Get-PlanetSize.ps1?

PS51> .\Get-PlanetSize.ps1 -Planet Barsoom
The diameter of planet Barsoom is  km

Хм, это не верно. Барсум не находится в списке планет, но скрипт все равно выполняется. Как это исправить?

Проблема здесь заключается в том, что сценарий принимает любой ввод и использует его, независимо от того, является ли он допустимым значением или нет. Сценарию нужен способ ограничить, какие значения принимаются для параметра Planet. Введите ValidateSet!

Гарантирование использования только определенных значений

A ValidateSet list is a comma-separated list of string values, wrapped in single or double-quotes. Adding a ValidateSet attribute to a script or function parameter consists of adding a line of text to the Param() block, as shown below. Replace the Param() block in your copy of Get-PlanetSize.ps1 with the one below and save the file.

Param(
     [Parameter(Mandatory)]
     [ValidateSet("Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto")]
     $Planet
 )

Попробуйте снова запустить сценарий, используя Barsoom в качестве вашего параметра Planet. Теперь будет возвращено полезное сообщение об ошибке. Сообщение конкретизирует, что пошло не так, и даже предоставляет список возможных значений для параметра.

PS51> .\Get-PlanetSize.ps1 -Planet Barsoom
 Get-PlanetSize.ps1 : Cannot validate argument on parameter 'Planet'. The argument "Barsoom" does not belong to the set
 "Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
 At line:1 char:32
 .\Get-PlanetSize.ps1 -Planet Barsoom
 ~~~
 CategoryInfo          : InvalidData: (:) [Get-PlanetSize.ps1], ParameterBindingValidationException
 FullyQualifiedErrorId : ParameterArgumentValidationError,Get-PlanetSize.ps1 

Сделаем PowerShell чувствительным к регистру при использовании ValidateSet

По умолчанию атрибут ValidateSet нечувствителен к регистру. Это означает, что он разрешит любую строку, если она есть в списке разрешенных со любым регистром. Например, в приведенном выше примере он примет Mars так же легко, как и mars. При необходимости вы можете заставить ValidateSet быть чувствительным к регистру, используя опцию IgnoreCase.

Опция IgnoreCase в ValidateSet определяет, должны ли предоставленные значения для параметра точно соответствовать списку допустимых значений. По умолчанию IgnoreCase установлено в $True (игнорировать регистр). Если вы установите это в $False, то предоставление mars в качестве значения для параметра Planet для Get-PlanetSize.ps1 вызовет сообщение об ошибке.

Вы можете использовать опцию IgnoreCase, присвоив ей значение $true в конце списка допустимых значений, как показано ниже.

[ValidateSet("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", IgnoreCase = $false)]  

Теперь, когда вы пытаетесь использовать значение для Planet, которое не точно как значение в списке, проверка завершится неудачно.

Использование автозаполнения

Еще одно преимущество использования ValidateSet заключается в том, что он предоставляет вам автозаполнение. Это означает, что вы можете переключаться между возможными значениями параметра, используя клавишу TAB. Это значительно улучшает удобство использования сценария или функции, особенно из консоли.

В приведенных ниже примерах есть несколько вещей, на которые следует обратить внимание:

  • Автозаполнение переходит к первому значению после отображения последнего.
  • Значения представлены в алфавитном порядке, даже если они не упорядочены в ValidateSet алфавитном порядке.
  • Ввод начальной буквы и нажатие клавиши TAB ограничивает значения, предлагаемые автозаполнением, только теми, которые начинаются с этой буквы.
Cycling through parameter values using Tab Completion
Restricting values returned by Tab Completion

Вы также можете воспользоваться автозаполнением ValidateSet в Интегрированной среде сценариев PowerShell (ISE), как показано в приведенном ниже примере. Функция ISE Интеллектуальное заполнение показывает вам список возможных значений в удобном окне выбора.

Интеллектуальное заполнение возвращает все значения, содержащие введенную вами букву, а не только те, которые начинаются с нее.

Tab Completion and Intellisense in ISE

Теперь, когда мы рассмотрели атрибуты проверки ValidateSet, как они есть в Windows 5.1, давайте посмотрим, что было добавлено в PowerShell Core 6.1, и посмотрим, не может ли это дать нашему сценарию больше возможностей проверки.

Понимание изменений в ValidateSet в PowerShell 6.1

С появлением PowerShell Core 6.1 были добавлены две новые возможности к атрибутам проверки ValidateSet:

  • Свойство ErrorMessage
  • Использование классов в ValidateSet через доступ к System.Management.Automation.IValidateSetValuesGenerator

Свойство ErrorMessage

Сообщение об ошибке по умолчанию, генерируемое при предоставлении неверного названия планеты для Get-PlanetSize.ps1, полезно, но немного длинное:

PS61> .\Get-PlanetSize.ps1 -Planet Barsoom
 Get-PlanetSize.ps1 : Cannot validate argument on parameter 'Planet'. The argument "Barsoom" does not belong to the set
 "Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
 At line:1 char:32
 .\Get-PlanetSize.ps1 -Planet Barsoom
 ~~~
 CategoryInfo          : InvalidData: (:) [Get-PlanetSize.ps1], ParameterBindingValidationException
 FullyQualifiedErrorId : ParameterArgumentValidationError,Get-PlanetSize.ps1 

Используйте свойство ErrorMessage атрибута проверки ValidateSet, чтобы установить другое сообщение об ошибке, как показано в примере ниже. {0} автоматически заменяется предоставленным значением, а {1} автоматически заменяется списком допустимых значений.

Param(
     [Parameter(Mandatory)]
     [ValidateSet("Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto",ErrorMessage="Value '{0}' is invalid. Try one of: '{1}'")]
     $Planet
 )

Замените блок Param() в файле сценария и сохраните его. Затем повторно выполните Get-PlanetSize.ps1 -Planet Barsoom. Обратите внимание, что ошибка стала более краткой и описательной.

PS61> .\Get-PlanetSize.ps1 -Planet Barsoom
 Get-PlanetSize.ps1 : Cannot validate argument on parameter 'Planet'. Value 'Barsoom' is invalid. Try one of: 'Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto'
 At line:1 char:32
 .\Get-PlanetSize.ps1 -Planet Barsoom
 ~~~
 CategoryInfo          : InvalidData: (:) [Get-PlanetSize.ps1], ParameterBindingValidationException
 FullyQualifiedErrorId : ParameterArgumentValidationError,Get-PlanetSize.ps1 

Затем рассмотрим новый способ определения допустимых значений в ValidateSet с помощью класса PowerShell класс.

Классы PowerShell

Пользовательские типы, известные в PowerShell как классы, доступны с версии 5. С появлением PowerShell Core 6.1 появилась новая функция, позволяющая использовать класс для предоставления значений для ValidateSet.

Использование класса позволяет обойти основное ограничение ValidateSet – он статичен. То есть он встроен как часть функции или сценария и может быть изменен только путем редактирования самого сценария.

Новая функция, которая работает с ValidateSet, – это возможность использования класса System.Management.Automation.IValidateSetValuesGenerator. Мы можем использовать его в качестве основы для наших собственных классов с использованием наследования. Для работы с ValidateSet класс должен быть основан на System.Management.Automation.IValidateSetValuesGenerator и должен реализовать метод с именем GetValidValues().

Метод GetValues() возвращает список значений, которые вы хотите принять. Блок Param() с статическим списком планет, замененным на класс [Planet], будет выглядеть следующим образом. Этот пример еще не будет работать. Продолжайте читать, чтобы узнать, как это реализовать.

Param(
     [Parameter(Mandator)]
     [ValidateSet([Planet],ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]
     $Planet
 )

Использование класса для списка значений ValidateSet: Реальный пример

Чтобы продемонстрировать использование класса для списка значений ValidateSet, вы собираетесь заменить статический список планет, использованный ранее, списком, загруженным из текстового файла CSV. Вам больше не нужно будет поддерживать статический список значений внутри самого сценария!

Создание источника данных для класса

Сначала вам нужно создать файл CSV, содержащий каждое из допустимых значений. Для этого скопируйте и вставьте этот набор данных в новый текстовый файл и сохраните его как planets.csv в той же папке, что и сценарий Get-PlanetSize.ps1.

Planet,Diameter
 "Mercury","4879"
 "Venus","12104"
 "Earth","12756"
 "Mars","6805"
 "Jupiter","142984"
 "Saturn","120536"
 "Uranus","51118"
 "Neptune","49528"
 "Pluto","2306"

Добавление класса к скрипту

Любой класс, использованный ValidateSet, должен быть уже определен до того, как ValidateSet попытается его использовать. Это означает, что структура Get-PlanetSize.ps1 как есть не будет работать.

Класс [Planet] должен быть определен перед его использованием, поэтому он должен быть размещен в начале скрипта. Подходящее определение каркасного класса выглядит примерно так:

class Planet : System.Management.Automation.IValidateSetValuesGenerator {
     [String[]] GetValidValues() {
 }
 }

Внутри метода GetValidValues() класса используйте командлет Import-CSV для импорта текстового файла planets.csv, созданного ранее. Файл импортируется в переменную с глобальной областью видимости, названную $planets, чтобы позже к ней можно было обращаться в скрипте.

Используйте оператор return для возврата списка названий планет через GetValidValues(). После этих изменений класс должен выглядеть примерно так.

class Planet : System.Management.Automation.IValidateSetValuesGenerator {
     [String[]] GetValidValues() {
             $Global:planets = Import-CSV -Path planets.csv
             return ($Global:planets).Planet
     }
 }

Затем удалите объявление хэш-таблицы $planets из скрипта, как показано ниже. Глобальная переменная $planets, которая заполняется классом [Planet], содержит данные о планетах.

$planets = [ordered]@{
     'Mercury' = 4879
     'Venus'   = 12104
     'Earth'   = 12756
     'Mars'    = 6805
     'Jupiter' = 142984
     'Saturn'  = 120536
     'Uranus'  = 51118
     'Neptune' = 49528
     'Pluto'   = 2306
 }

Теперь оберните оставшийся исходный код в функцию и назовите ее Get-PlanetDiameter, чтобы отличить ее от названия скрипта. Поместите блок Param(), который находился в начале скрипта, внутрь функции. Замените статический список планет ссылкой на класс [Planet], как показано ниже.

[ValidateSet([Planet],ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]`

Замените строку $output = "Диаметр планеты {0} составляет {1} км" -f $_, $planets[$_] на следующие две строки. Они позволяют скрипту искать планету в массиве объектов, созданных с помощью Import-CSV, а не в хэш-таблице, которую вы ранее удалили из скрипта:

$targetplanet = $planets | Where -Property Planet -match $_
 $output = "The diameter of planet {0} is {1} km" -f $targetplanet.Planet, $targetplanet.Diameter

После этих изменений ваш окончательный скрипт должен выглядеть так:

class Planet : System.Management.Automation.IValidateSetValuesGenerator {
     [String[]] GetValidValues() {
         $Global:planets = Import-CSV -Path planets.csv
         return ($Global:planets).Planet
     }
 }
 Function Get-PlanetDiameter {
     Param(
         [Parameter(Mandatory)]
         [ValidateSet([Planet],ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]
         $Planet
     )
     $Planet | Foreach-Object {
         $targetplanet = $planets | Where -Property Planet -match $_
         $output = "The diameter of planet {0} is {1} km" -f $targetplanet.Planet, $targetplanet.Diameter
         Write-Output $output
     }
 }

Помните, что начиная с этой точки скрипт работает только в PowerShell 6.1 или более поздней версии

Теперь, как использовать этот скрипт?

Запуск скрипта

Вы не можете использовать новую версию скрипта, просто выполнев его. Весь полезный код теперь заключен в функцию. Вам нужно дот сорсить файл, чтобы получить доступ к функции из сессии PowerShell.

PS61> . .\Get-PlanetSize.ps1

После дот сорсинга у вас теперь есть доступ к новой функции Get-PlanetDiameter в сессии PowerShell с возможностью автозаполнения.

Какая польза от всей этой работы?”, – можете спросить. “Похоже, что скрипт работает так же, но теперь код сложнее использовать!”

Попробуйте это:

  • Откройте файл planets.csv, который вы создали ранее.
  • Добавьте новую строку с новым именем и диаметром.
  • Сохраните файл CSV.

В той же сессии, в которой вы исходно подключили свой скрипт, попробуйте найти диаметр новой планеты, используя Get-PlanetDiameter. Это работает!

Использование класса таким образом приносит нам несколько преимуществ:

  • Список допустимых значений теперь отделен от самого кода, но любые изменения значений в файле учитываются скриптом.
  • Файл может поддерживаться тем, кто никогда не обращается к скрипту.
  • A more complex script could look up information from a spreadsheet, database, Active Directory or a web API.

Как вы можете видеть, возможности почти бесконечны при использовании класса для предоставления значений ValidateSet.

Подведем итог

Мы охватили много материала, поскольку создавали Get-PlanetSize.ps1, так что давайте подытожим.

В этой статье вы узнали:

  • Что такое атрибут валидации ValidateSet и почему вам может понадобиться его использовать
  • Как добавить ValidateSet в функцию PowerShell или скрипт
  • Как работает автодополнение с ValidateSet
  • Как использовать свойство IgnoreCase для управления регистрозависимостью вашего ValidateSet
  • Как использовать свойство ErrorMessage с вашим ValidateSet и PowerShell 6.1
  • Как использовать класс для создания динамического ValidateSet с PowerShell 6.1

На что вы ждете? Начните использовать ValidateSet уже сегодня!

Дальнейшее чтение

Source:
https://adamtheautomator.com/powershell-validateset/