PowerShell参数:释放脚本的力量

所有 PowerShell 命令都可以有一个或多个参数,有时被称为参数。如果您在 PowerShell 函数中不使用 PowerShell 参数,那么您就不是在编写良好的 PowerShell 代码!

在本文中,您将学习有关创建和使用 PowerShell 参数或参数的几乎所有方面!

这是我书中《PowerShell for SysAdmins》的示例。如果您想学习 PowerShell 或学习一些行业技巧,来看看吧!

为什么需要参数?

当您开始创建函数时,您可以选择包括参数或不包括参数以及这些参数的工作方式。

假设您有一个安装 Microsoft Office 的函数。也许它在函数内部静默调用 Office 安装程序。函数的作用对我们来说并不重要。基本函数如下,其中包括函数的名称和脚本块。

function Install-Office {
    ## 在此运行静默安装程序
}

在这个例子中,您只是运行了 Install-Office,而没有使用参数,它会执行其操作。

无论 Install-Office 函数是否有参数都没关系。显然它没有任何强制性参数;否则,PowerShell 将不允许我们在不使用参数的情况下运行它。

何时使用 PowerShell 参数

办公软件有很多不同的版本。也许你需要安装 Office 2013 和 2016 版本。目前,你没有办法指定这一点。每次想要改变行为时,你都可以更改函数的代码。

例如,你可以创建两个单独的函数来安装不同的版本。

function Install-Office2013 {
    Write-Host 'I installed Office 2013. Yippee!'
}

function Install-Office2016 {
    Write-Host 'I installed Office 2016. Yippee!'
}

这样做是可行的,但不具有可扩展性。它强迫你为每个发布的 Office 版本创建一个单独的函数。当你无需这样做时,你将不得不复制大量的代码。

相反,你需要一种方法在运行时传入不同的值来改变函数的行为。你怎么做呢?

是的!参数或有些人称之为参数。

由于我们想要安装不同版本的 Office 而无需每次更改代码,你必须向该函数添加至少一个参数。

在你迅速想出要使用的 PowerShell 参数之前,首先要问自己一个问题;“在这个函数中你预计需要做出的最小更改是什么?”。

记住,你需要重新运行这个函数而不改变函数内部的任何代码。在这个例子中,参数对你来说可能很明显;你需要添加一个Version参数。但是,当你有一个有几十行代码的函数时,答案可能不太明显。只要尽可能准确地回答这个问题,它总是会有所帮助。

所以你知道你需要一个Version参数。现在呢?现在你可以添加一个,但像任何一个优秀的编程语言一样,有多种方法来实现这个目标。

在这个教程中,我将向您展示基于我近十年的PowerShell经验创建参数的“最佳”方法。然而,要知道这并不是创建参数的唯一方式。

还有一种叫做位置参数的东西。这些参数允许您向参数传递值,而无需指定参数名称。位置参数能够工作,但并不被认为是“最佳实践”。为什么?因为当您在函数上定义了许多参数时,它们很难阅读。

创建一个简单的PowerShell参数

在函数上创建参数需要两个主要组件;一个参数块和参数本身。一个param块由param关键字定义,后面跟着一对括号。

An example of a param block
function Install-Office { [CmdletBinding()] param() Write-Host 'I installed Office 2016. Yippee!' }

在这一点上,函数的实际功能还没有改变。我们只是组合了一些管道,为第一个参数做准备。

一旦我们有了param块,现在您将创建参数。我建议您创建参数的方法包括Parameter块,后跟参数类型,然后是参数变量名称。

An example of a param block
function Install-Office { [CmdletBinding()] param( [Parameter()] [string]$Version ) Write-Host 'I installed Office 2016. Yippee!' }

我们现在已经在PowerShell中创建了一个函数参数,但这里究竟发生了什么?

Parameter块是每个参数的可选但建议的一部分。像param块一样,它是“函数管道”,为参数添加额外功能做准备。第二行是您定义参数类型的地方。

在这种情况下,我们选择将Version参数转换为字符串。定义显式类型意味着传递给该参数的任何值,如果尚未是字符串,将始终尝试“转换”为字符串。

类型不是强制性的,但强烈建议使用。明确定义参数的类型将显著减少以后许多不必要的情况。相信我。

现在,参数已经定义好了,你可以运行Install-Office命令,并将Version参数传递一个版本字符串,比如2013。传递给Version参数的值有时称为参数的参数或值。

Passing a Parameter to the Function
PS> Install-Office -Version 2013 I installed Office 2016. Yippee!

这里到底发生了什么?你告诉它你想安装2013版,但它仍然告诉你已经安装了2016版。当你添加一个参数时,你必须记住将函数的代码更改为变量。当参数传递给函数时,该变量将扩展为传递的任何值。

更改2016的静态文本,并将其替换为Version参数变量,并将单引号转换为双引号,以便变量会被展开。

Modifying function code account for a parameter
function Install-Office { [CmdletBinding()] param( [Parameter()] [string]$Version ) Write-Host "I installed Office $Version. Yippee!" }

现在你可以看到,无论你传递给Version参数的值是什么,它都会作为$Version变量传递到函数中。

强制性参数属性

回想一下,我提到过[Parameter()]行只是“函数管道”,需要为函数做好进一步的准备工作吗?向参数添加参数属性就是我之前提到的额外工作。

A parameter doesn’t have to be a placeholder for a variable. PowerShell has a concept called parameter attributes and parameter validation. Parameter attributes change the behavior of the parameter in a lot of different ways.

例如,你将设置的最常见的参数属性之一是Mandatory关键字。默认情况下,你可以调用Install-Office函数而不使用Version参数,它会正常执行。版本参数是可选的。当然,它不会在函数内部展开$Version变量,因为没有值,但它仍然会运行。

在创建参数时,很多时候你会希望用户始终使用该参数。你将依赖于函数内部某处的参数值,如果参数没有传递,函数将失败。在这些情况下,你希望强制用户传递这个参数值给你的函数。你希望该参数变成强制性的

一旦你建立了基本框架,让用户强制使用参数就很简单,就像你在这里所做的一样。你必须在参数的括号内包含关键字Mandatory。一旦你完成了这一步,执行没有该参数的函数将会停止执行,直到输入了一个值。

Using a mandatory parameter
function Install-Office { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Version ) Write-Host "I installed Office $Version. Yippee!" } PS> Install-Office cmdlet Install-Office at command pipeline position 1 Supply values for the following parameters: Version:

函数将等待你为Version参数指定一个值。一旦你这样做并按下Enter,PowerShell将执行该函数并继续。如果你为参数提供了一个值,PowerShell将不会每次提示你输入参数。

PowerShell参数验证属性

使参数变为必填项是你可以添加的最常见的参数属性之一,但你还可以使用参数验证属性。在编程中,限制用户输入是非常重要的,尽量让输入更为严格。限制用户(甚至是你自己!)可以传递给函数或脚本的信息,将消除函数内部不必要的代码,这些代码必须考虑各种情况。

通过示例学习

例如,在Install-Office函数中,我演示了向其传递值2013的过程,因为我知道它会起作用。我写了这段代码!我在代码中假设(在代码中永远不要这样做!)任何了解的人都会指定版本为2013或2016。嗯,对你来说很明显的事情对其他人来说可能并不那么明显。

如果你想要更详细地了解版本,可能更准确的是将2013版本指定为15.0,将2016版本指定为16.0,如果Microsoft仍然使用过去的版本架构。但是,如果由于你假设他们将指定2013或2016的版本,你的函数内部有一些查找具有这些版本的文件夹或其他内容的代码,该怎么办呢?

下面是一个示例,你可能正在使用$Version字符串构建文件路径。如果有人传递一个不完整的文件夹名称,如Office2013Office2016的值,它将失败,或者执行更糟糕的操作,比如删除意外的文件夹或更改你没有考虑到的东西。

Assuming Parameter Values
function Install-Office { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Version ) Get-ChildItem -Path "\\SRV1\Installers\Office$Version" } PS> Install-Office -Version '15.0' Get-ChildItem : Cannot find path '\SRV1\Installers\Office15.0' because it does not exist. At line:7 char:5 Get-ChildItem -Path "\\SRV1\Installers\Office$Version" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CategoryInfo : ObjectNotFound: (\SRV1\Installers\Office15.0:String) [Get-ChildItem], ItemNotFoundExcep tion FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

为了限制用户输入的内容,您可以添加一些 PowerShell 参数验证。

使用 ValidateSet 参数验证属性

有各种各样的参数验证方法可供选择。要查看完整列表,请运行 Get-Help about_Functions_Advanced_Parameters。在这个示例中,ValidateSet 属性可能是最合适的选择。

ValidateSet 验证属性允许您指定允许作为参数值的值列表。由于我们只考虑字符串 20132016,我希望确保用户只能指定这些值。否则,函数将立即失败,并通知他们原因。

您可以在原始 Parameter 关键字的正下方添加参数验证属性。在这个示例中,在参数属性的括号内,您有一个项目数组;20132016。参数验证属性告诉 PowerShell,Version 的有效值只能是 20132016。如果尝试传递不在集合中的值,您将收到一个错误通知,告诉您只有特定的选项可用。

Using the ValidateSet parameter validation attribute
function Install-Office { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('2013','2016')] [string]$Version ) Get-ChildItem -Path "\\SRV1\Installers\Office$Version" } PS> Install-Office -Version 15.0 Install-Office : Cannot validate argument on parameter 'Version'. The argument "15.0" does not belong to the set "2013,2016" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again. At line:1 char:25 Install-Office -Version 15.0 ~~~~ CategoryInfo : InvalidData: (:) [Install-Office], ParameterBindingValidationException FullyQualifiedErrorId : ParameterArgumentValidationError,Install-Office

ValidateSet 属性是常用的验证属性。要了解参数值受限的所有方式的详细信息,请查看运行 Get-Help about_Functions_Advanced_Parameters 命令的 Functions_Advanced_Parameters 帮助主题。

参数集

假设您只想让某些 PowerShell 参数与其他参数一起使用。也许您已向 Install-Office 函数添加了一个 Path 参数。此路径将安装安装程序的任何版本。在这种情况下,您不希望用户使用 Version 参数。

您需要参数集。

参数可以分组成只能与同一组中的其他参数一起使用的集合。使用下面的函数,您现在可以同时使用 Version 参数和 Path 参数来生成安装程序的路径。

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('2013','2016')]
        [string]$Version,
        
        [Parameter(Mandatory)]
        [string]$Path
    )
    
    if ($Version) {
        Get-ChildItem -Path "\\SRV1\Installers\Office$Version"
    } elseif ($Path) {
        Get-ChildItem -Path $Path
    }
}

然而,这会带来一个问题,因为用户可能会同时使用两个参数。此外,由于两个参数都是强制性的,当不希望如此时,他们将被强制使用两个参数。为了解决这个问题,我们可以像下面这样将每个参数放在一个参数集中。

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ParameterSetName = 'ByVersion')]
        [ValidateSet('2013','2016')]
        [string]$Version,
        
        [Parameter(Mandatory, ParameterSetName = 'ByPath')]
        [string]$Path
    )
    
    if ($Version) {
        Get-ChildItem -Path "\\SRV1\Installers\Office$Version"
    } elseif ($Path) {
        Get-ChildItem -Path $Path
    }
}

通过在每个参数上定义参数集名称,这允许您一起控制参数组。

默认参数集

如果用户尝试不带参数运行 Install-Office 会发生什么?这是没有考虑到的,您将会看到友好的错误消息。

No parameter set

要修复这个问题,您需要在CmdletBinding()区域内定义一个默认参数集。这会告诉函数,如果没有显式使用参数,则选择要使用的参数集,将[CmdletBinding()]更改为[CmdletBinding(DefaultParameterSetName = 'ByVersion')]

现在,每当您运行Install-Office时,它都会提示您输入Version参数,因为它将使用该参数集。

管道输入

到目前为止的示例中,您一直在创建具有 PowerShell 参数的函数,这些参数只能使用典型的-ParameterName Value语法传递。但是,正如您已经学到的那样,PowerShell具有直观的管道,允许您无缝地将对象从一个命令传递到另一个命令,而不使用“典型”的语法。

当您使用管道时,您可以使用管道符号|将命令“链接”在一起,使用户能够将Get-Service的输出发送到Start-Service,从而将Name参数传递给Start-Service作为一种快捷方式。

使用循环的“旧”方法

在您正在处理的自定义函数中,您正在安装 Office 并具有Version参数。假设您在一个 CSV 文件中的一行中有一个计算机名列表,并在第二行中有需要在这些计算机上安装的 Office 版本。CSV 文件看起来像这样:

ComputerName,Version
PC1,2016
PC2,2013
PC3,2016

您希望在每台计算机旁边安装该计算机的 Office 版本。

首先,您需要在函数上添加一个ComputerName参数,以便在每次函数迭代时传递不同的计算机名称。下面我创建了一些伪代码,代表可能存在于虚构函数中的一些代码,并添加了一个Write-Host实例,以查看函数内部变量的展开情况。

Adding the ComputerName parameter
function Install-Office { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('2013','2016')] [string]$Version, [Parameter()] [string]$ComputerName ) <# ## 使用此处的一些代码连接到远程主机 Invoke-Command -ComputerName $ComputerName -ScriptBlock { ## 执行安装此计算机上 Office 版本的操作 Start-Process -FilePath 'msiexec.exe' -ArgumentList 'C:\Setup\Office{0}.msi' -f $using:Version } #> Write-Host "I am installing Office version [$Version] on computer [$ComputerName]" }

一旦您在函数中添加了ComputerName参数,您可以通过读取CSV文件并将计算机名称和版本的值传递给Install-Office函数来实现这一点。

$computers = Import-Csv -Path 'C:\ComputerOfficeVersions.csv'
foreach ($pc in $computers) {
    Install-Office -Version $_.Version -ComputerName $_.ComputerName
}

为参数构建管道输入

通过读取CSV行并使用循环将每行的属性传递给函数的方法是“旧”方法。在此部分,您要完全放弃foreach循环,而是改用管道。

目前,该函数根本不支持管道。直观上,您可能会认为可以使用管道将每台计算机的名称和版本传递给函数。在下面的示例中,我们正在读取CSV并直接将其传递给Install-Office,但这种方法行不通。

No function pipeline input defined
PS> Import-Csv -Path 'C:\ComputerOfficeVersions.csv' | Install-Office

你可以假设任何你想要的,但那并不能让它正常工作。当你知道Import-Csv正在将Version参数作为对象属性发送时,我们会被提示输入Version参数。为什么它不起作用呢?因为你还没有添加任何管道支持。

在PowerShell函数中有两种类型的管道输入;按值(整个对象)和按属性名称(单个对象属性)。你认为哪种方法是将Import-Csv的输出与Install-Office的输入连接在一起的最佳方式?

即使完全没有重构,你也可以使用按属性名称的方法,因为毕竟Import-Csv已经返回了CSV中的列VersionComputerName这些属性。

为自定义函数添加管道支持要比你想象的简单得多。它仅仅是一个参数属性,表示为两个关键字之一;ValueFromPipelineValueFromPipelineByPropertyName

在这个示例中,你想要将Import-Csv返回的ComputerNameVersion属性绑定到Install-OfficeVersionComputerName参数,所以你将使用ValueFromPipelineByPropertyName

由于我们想要绑定这两个参数,你将在两个参数中都添加这个关键字,如下所示,并使用管道重新运行函数。

Adding Pipeline Support
function Install-Office { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipelineByPropertyName)] [ValidateSet('2013','2016')] [string]$Version, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$ComputerName ) <# ## 使用一些代码连接到远程主机 Invoke-Command -ComputerName $ComputerName -ScriptBlock { ## 执行一些操作,在此计算机上安装 Office 版本 Start-Process -FilePath 'msiexec.exe' -ArgumentList 'C:\Setup\Office{0}.msi' -f $using:Version } #> Write-Host "I am installing Office version [$Version] on computer [$ComputerName]" }

这很奇怪。它只对 CSV 中的最后一行运行了。发生了什么?它只执行了最后一行,因为你忽略了在构建不支持管道的函数时不需要的一个概念。

不要忘记进程块!

当你需要创建一个涉及管道支持的函数时,你必须至少在你的函数内包含一个叫做process的“嵌入式”块。

这个处理块告诉 PowerShell,在接收到管道输入时,对每次迭代运行该函数。默认情况下,它只会执行最后一个。

你可以在函数中技术上添加其他块,比如beginend,但是脚本作者不常用它们。

Adding the process block
function Install-Office { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipelineByPropertyName)] [ValidateSet('2013','2016')] [string]$Version, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$ComputerName ) process { 为了告诉 PowerShell 对每个输入对象都执行该函数,我会添加一个包含其中代码的process块。<# ## 使用一些代码连接到远程主机 Invoke-Command -ComputerName $ComputerName -ScriptBlock { ## 执行一些操作,在此计算机上安装 Office 版本 Start-Process -FilePath 'msiexec.exe' -ArgumentList 'C:\Setup\Office{0}.msi' -f $using:Version } #> Write-Host "I am installing Office version [$Version] on computer [$ComputerName]" } }

现在你可以看到,每个对象的`Version`和`ComputerName`属性从`Import-Csv`返回后,被传递给`Install-Office`并绑定到`Version`和`ComputerName`参数。

资源

要深入了解函数参数是如何工作的,查看我的关于PowerShell函数的博客文章

I also encourage you to check my Pluralsight course entitled Building Advanced PowerShell Functions and Modules for an in-depth breakdown of everything there is to know about PowerShell functions, function parameters, and PowerShell modules.

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