PowerShell ValidateSet: Tab-Completion & Parameter Values

当你编写 PowerShell 脚本或函数时,通常希望通过参数接受用户输入。如果不限制这些参数接受的值,就无法保证不会出现提供不适当值的情况。在本文中,您将学习如何使用 PowerShell 的 \texttt{ValidateSet} 参数验证属性,将这些值限制为您定义的值。

在编写 PowerShell 脚本或函数时,您可以使用许多不同的验证属性来检查传递给参数的值是否可接受,并在它们不可接受时通知用户。

本文重点介绍 \texttt{ValidateSet} 验证属性。您将了解 \texttt{ValidateSet} 的作用,以及为什么您可能希望在代码中使用 \texttt{ValidateSet} 以及如何使用。您还将了解由 \texttt{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来查找行星Barsoom的直径会发生什么?

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

嗯,这不对。Barsoom不在行星列表中,但脚本仍然运行了。我们该如何修复这个问题?

问题在于脚本接受任何输入并使用它,不管它是否是有效的值。脚本需要一种方法来限制哪些值可以被接受作为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属性不区分大小写。这意味着它将接受任何字符串,只要它在允许的列表中以任何大小写方案出现。例如,上面的示例将接受Marsmars。如果有必要,您可以通过使用IgnoreCase选项来强制ValidateSet区分大小写。

ValidateSet中的IgnoreCase选项是一个验证属性,确定提供给参数的值是否与有效值列表完全匹配。默认情况下,IgnoreCase设置为$True(忽略大小写)。如果将其设置为$False,那么将mars作为Get-PlanetSize.ps1Planet参数的值将生成错误消息。

您可以通过在有效值列表的末尾为其分配$true值来使用IgnoreCase选项,如下所示。

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

现在,当您尝试使用不完全像列表中的值的Planet的值时,验证将失败。

使用Tab Completion

使用ValidateSet的另一个好处是它提供了Tab Completion。这意味着您可以使用TAB键循环浏览参数的可能值。这极大地提高了脚本或函数的可用性,特别是从控制台使用时。

在下面的示例中,有几点需要注意:

  • 在显示最后一个值后,制表符完成会回到第一个值。
  • 尽管这些值未按ValidateSet字母顺序列出,但它们以字母顺序呈现。
  • 键入初始字母并按TAB键会将制表符完成提供的值限制为以该字母开头的值。
Cycling through parameter values using Tab Completion
Restricting values returned by Tab Completion

您还可以在PowerShell集成脚本环境(ISE)中利用ValidateSet制表完成,如下例所示。ISE Intellisense功能会在一个漂亮的选择框中显示可能值的列表。

Intellisense返回包含您键入的字母的所有值,而不仅仅是以它开头的值。

Tab Completion and Intellisense in ISE

现在我们已经介绍了ValidateSet验证属性在Windows 5.1中的情况,让我们看看在PowerShell Core 6.1中添加了什么,看看那是否可以为我们的脚本提供更多的验证功能。

理解PowerShell 6.1中对ValidateSet的更改

随着 PowerShell Core 6.1 的到来,已经添加了两个新的功能到 ValidateSet 验证属性:

  • 属性ErrorMessage
  • 通过访问System.Management.Automation.IValidateSetValuesGenerator来使用 ValidateSet 中的类

属性

当您向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 

使用ValidateSet验证属性的ErrorMessage属性来设置不同的错误消息,如下例所示。{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 

接下来,看一看通过 PowerShell 定义 ValidateSet 中可接受值的新方法。

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 文件。为此,将此数据集复制并粘贴到一个新的文本文件中,并将其保存为与 Get-PlanetSize.ps1 脚本相同文件夹中的 planets.csv

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 cmdlet导入先前创建的文本文件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哈希表声明。由[Planet]类填充的全局变量$planets包含行星数据。

$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

一旦点源化,您现在可以在PowerShell会话中使用新的Get-PlanetDiameter函数,并使用制表符补全。

所有这一切工作的好处是什么?”我听到你问。“脚本似乎以相同的方式工作,但使用代码更加困难!”

试试这个:

  • 打开您之前创建的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是否区分大小写
  • 如何在ValidateSet和PowerShell 6.1中使用ErrorMessage属性
  • 如何使用类在PowerShell 6.1中创建动态的ValidateSet

您还在等什么?开始今天使用ValidateSet吧!

更多阅读

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