第 9 章 - 函数
如果你正在编写 PowerShell 单行程序或脚本,并发现你经常需要针对不同的场景对其进行修改,则可以考虑将其转换为可重用的函数。
只要有可能,我更喜欢编写函数,因为它们更具有工具导向性。 我可以将函数放在脚本模块中,将该模块放在 $env:PSModulePath
中,然后调用这些函数,而无需查找它们的实际保存位置。 使用 PowerShellGet 模块,可以轻松地在 NuGet 存储库中共享这些模块。 PowerShellGet 随 PowerShell 版本 5.0 及更高版本提供。 对于 PowerShell 版本 3.0 及更高版本,它可以作为单独的下载提供。
不要将事情复杂化。 保持简单,并使用最直接的方式完成任务。 避免在重用的任何代码中使用别名和位置参数。 设置代码的格式以提高可读性。 不要对值进行硬编码;使用参数和变量。 不要编写不必要的代码,即使它不会造成任何损害。 它增加了不必要的复杂性。 编写任何 PowerShell 代码时,请注意细节。
命名
在 PowerShell 中命名函数时,结合使用帕斯卡拼写法名称和已批准的谓词和单数名词。 我还建议在名词前面加上前缀。 例如:<ApprovedVerb>-<Prefix><SingularNoun>
。
PowerShell 中有一个批准的谓词的特定列表,可通过运行 Get-Verb
获取这些谓词。
Get-Verb | Sort-Object -Property Verb
Verb Group
---- -----
Add Common
Approve Lifecycle
Assert Lifecycle
Backup Data
Block Security
Checkpoint Data
Clear Common
Close Common
Compare Data
Complete Lifecycle
Compress Data
Confirm Lifecycle
Connect Communications
Convert Data
ConvertFrom Data
ConvertTo Data
Copy Common
Debug Diagnostic
Deny Lifecycle
Disable Lifecycle
Disconnect Communications
Dismount Data
Edit Data
Enable Lifecycle
Enter Common
Exit Common
Expand Data
Export Data
Find Common
Format Common
Get Common
Grant Security
Group Data
Hide Common
Import Data
Initialize Data
Install Lifecycle
Invoke Lifecycle
Join Common
Limit Data
Lock Common
Measure Diagnostic
Merge Data
Mount Data
Move Common
New Common
Open Common
Optimize Common
Out Data
Ping Diagnostic
Pop Common
Protect Security
Publish Data
Push Common
Read Communications
Receive Communications
Redo Common
Register Lifecycle
Remove Common
Rename Common
Repair Diagnostic
Request Lifecycle
Reset Common
Resize Common
Resolve Diagnostic
Restart Lifecycle
Restore Data
Resume Lifecycle
Revoke Security
Save Data
Search Common
Select Common
Send Communications
Set Common
Show Common
Skip Common
Split Common
Start Lifecycle
Step Common
Stop Lifecycle
Submit Lifecycle
Suspend Lifecycle
Switch Common
Sync Data
Test Diagnostic
Trace Diagnostic
Unblock Security
Undo Common
Uninstall Lifecycle
Unlock Common
Unprotect Security
Unpublish Data
Unregister Lifecycle
Update Data
Use Other
Wait Lifecycle
Watch Common
Write Communications
在前面的示例中,我已按照“谓词”列对结果进行排序。 通过“组”列,你可以了解这些谓词的用法。 将函数添加到模块时,请务必在 PowerShell 中选择一个批准的谓词。 如果选择未批准的谓词,模块将在加载时生成警告消息。 该警告消息使函数看起来不专业。 未批准的谓词还会限制函数的可发现性。
简单函数
PowerShell 中的函数使用函数关键字声明,后面依次跟函数名称、左大括号和右大括号。 函数将执行的代码包含在这些大括号中。
function Get-Version {
$PSVersionTable.PSVersion
}
显示的函数是返回 PowerShell 版本的简单示例。
Get-Version
Major Minor Build Revision
----- ----- ----- --------
5 1 14393 693
名称很可能与名称类似于 Get-Version
的函数以及 PowerShell 中的默认命令或其他人可能编写的命令发生冲突。 因此,我建议为函数的名词部分加上前缀以防止命名冲突。 在下面的示例中,我将使用前缀“PS”。
function Get-PSVersion {
$PSVersionTable.PSVersion
}
除了名称外,此函数与上一个函数相同。
Get-PSVersion
Major Minor Build Revision
----- ----- ----- --------
5 1 14393 693
即使在名词前面加上前缀(如 PS),仍可能出现名称冲突。 我通常使用我的姓名缩写作为函数名词的前缀。 制定标准并坚持遵循。
function Get-MrPSVersion {
$PSVersionTable.PSVersion
}
此函数与前两个函数没有什么不同,唯一区别是它使用更合理的名称,以防止与其他 PowerShell 命令发生命名冲突。
Get-MrPSVersion
Major Minor Build Revision
----- ----- ----- --------
5 1 14393 693
加载到内存后,可在函数 PSDrive 上查看函数。
Get-ChildItem -Path Function:\Get-*Version
CommandType Name Version Source
----------- ---- ------- ------
Function Get-Version
Function Get-PSVersion
Function Get-MrPSVersion
如果要从当前会话中删除这些函数,则必须从函数 PSDrive 中将其删除,或关闭之后再重新打开 PowerShell。
Get-ChildItem -Path Function:\Get-*Version | Remove-Item
验证是否确实删除了函数。
Get-ChildItem -Path Function:\Get-*Version
如果函数是作为模块的一部分加载的,则可以卸载模块以删除它们。
Remove-Module -Name <ModuleName>
Remove-Module
cmdlet 从当前 PowerShell 会话中的内存中删除模块,不会从系统或磁盘中删除模块。
参数
请勿静态分配值! 使用参数和变量。 命名参数时,请尽可能使用与默认 cmdlet 相同的名称作为参数名称。
function Test-MrParameter {
param (
$ComputerName
)
Write-Output $ComputerName
}
我为什么使用 ComputerName 而不是 Computer、ServerName 或 Host 作为参数名称? 这是因为我希望函数像默认 cmdlet 一样标准化。
我将创建一个函数,用于查询系统上的所有命令并返回具有特定参数名称的命令数量。
function Get-MrParameterCount {
param (
[string[]]$ParameterName
)
foreach ($Parameter in $ParameterName) {
$Results = Get-Command -ParameterName $Parameter -ErrorAction SilentlyContinue
[pscustomobject]@{
ParameterName = $Parameter
NumberOfCmdlets = $Results.Count
}
}
}
正如下面的结果所示,有 39 个命令具有 ComputerName 参数。 没有任何 cmdlet 具有 Computer、ServerName、Host 或 Machine 这样的参数。
Get-MrParameterCount -ParameterName ComputerName, Computer, ServerName, Host, Machine
ParameterName NumberOfCmdlets
------------- ---------------
ComputerName 39
Computer 0
ServerName 0
Host 0
Machine 0
我还建议对参数名称使用与默认 cmdlet 相同的大小写。 使用 ComputerName
,而不是 computername
。 这会使你的函数看起来像默认 cmdlet。 已经熟悉 PowerShell 的用户会感到得心应手。
该 param
语句允许你定义一个或多个参数。 参数定义用逗号 (,
) 分隔。 有关详细信息,请参阅 about_Functions_Advanced_Parameters。
高级函数
将 PowerShell 中的函数转换为高级函数非常简单。 函数与高级函数之间的一个区别是,高级函数具有多个自动添加到函数的通用参数。 这些通用参数包括 Verbose 和 Debug。
我将从上一部分中使用的 Test-MrParameter
函数开始介绍。
function Test-MrParameter {
param (
$ComputerName
)
Write-Output $ComputerName
}
请注意,Test-MrParameter
函数没有任何通用参数。 可以通过多种不同的方法来查看通用参数。 其中一种方法是使用 Get-Command
查看语法。
Get-Command -Name Test-MrParameter -Syntax
Test-MrParameter [[-ComputerName] <Object>]
另一种方法是通过 Get-Command
向下钻取参数。
(Get-Command -Name Test-MrParameter).Parameters.Keys
ComputerName
添加 CmdletBinding
以将函数转换为高级函数。
function Test-MrCmdletBinding {
[CmdletBinding()] #<<-- This turns a regular function into an advanced function
param (
$ComputerName
)
Write-Output $ComputerName
}
添加 CmdletBinding
会自动添加通用参数。 CmdletBinding
需要一个 param
块,但 param
块可以为空。
Get-Command -Name Test-MrCmdletBinding -Syntax
Test-MrCmdletBinding [[-ComputerName] <Object>] [<CommonParameters>]
通过 Get-Command
向下钻取参数会显示实际参数名称,包括常见参数名称。
(Get-Command -Name Test-MrCmdletBinding).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable
SupportsShouldProcess
SupportsShouldProcess
会添加 WhatIf 和 Confirm 参数。 只有做出更改的命令需要这些参数。
function Test-MrSupportsShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param (
$ComputerName
)
Write-Output $ComputerName
}
请注意,现在有 WhatIf 和 Confirm 参数。
Get-Command -Name Test-MrSupportsShouldProcess -Syntax
Test-MrSupportsShouldProcess [[-ComputerName] <Object>] [-WhatIf] [-Confirm] [<CommonParameters>]
同样,还可以使用 Get-Command
返回实际参数名称列表,其中包括常见参数名称以及 WhatIf 和 Confirm。
(Get-Command -Name Test-MrSupportsShouldProcess).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable
WhatIf
Confirm
参数验证
尽早验证输入。 如果不可能在没有有效输入的情况下运行代码,那为什么允许代码在某路径上继续运行?
始终键入要用于参数的变量(指定数据类型)。
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[string]$ComputerName
)
Write-Output $ComputerName
}
在前面的示例中,我指定了 String 作为 ComputerName 参数的数据类型。 这将导致它只允许指定一个计算机名。 如果通过以逗号分隔的列表指定了多个计算机名,则会生成错误。
Test-MrParameterValidation -ComputerName Server01, Server02
Test-MrParameterValidation : Cannot process argument transformation on parameter
'ComputerName'. Cannot convert value to type System.String.
At line:1 char:42
+ Test-MrParameterValidation -ComputerName Server01, Server02
+
+ CategoryInfo : InvalidData: (:) [Test-MrParameterValidation], ParameterBindingArg
umentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-MrParameterValidation
当前定义的问题在于省略 ComputerName 参数的值是有效的,但需要一个值才能成功完成该函数。 这时 Mandatory
参数属性就发挥作用了。
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$ComputerName
)
Write-Output $ComputerName
}
前面的示例中使用的语法为 PowerShell 版本 3.0 及更高兼容版本。
可以改为指定 [Parameter(Mandatory=$true)]
,以使函数与 PowerShell 版本 2.0 及更高版本兼容。 现在 ComputerName 是必需的,如果未指定,该函数将提示输入名称。
Test-MrParameterValidation
cmdlet Test-MrParameterValidation at command pipeline position 1
Supply values for the following parameters:
ComputerName:
如果要允许 ComputerName 参数具有多个值,请使用 String 数据类型,但应向该数据类型添加左方括号和右方括号以允许字符串数组。
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string[]]$ComputerName
)
Write-Output $ComputerName
}
也许你想为 ComputerName 参数指定一个默认值(如果未指定)。
问题在于,默认值不能与必需参数一起使用。 你需要将 ValidateNotNullOrEmpty
参数验证属性与默认值一起使用。
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
Write-Output $ComputerName
}
即使在设置默认值时,也不要使用静态值。 在上面的示例中,$env:COMPUTERNAME
用作默认值,如果未提供值,该默认值将自动转换为本地计算机名。
详细输出
尽管内联注释非常有用(尤其是在编写一些复杂的代码时),但用户不会看到这些注释,除非他们查看代码本身。
下面的示例中显示的函数在 foreach
循环中具有内联注释。 虽然这种特定注释可能并不难找到,但如果函数包含数百个代码行,就很麻烦。
function Test-MrVerboseOutput {
[CmdletBinding()]
param (
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
foreach ($Computer in $ComputerName) {
#Attempting to perform some action on $Computer <<-- Don't use
#inline comments like this, use write verbose instead.
Write-Output $Computer
}
}
一种更好的选择是使用 Write-Verbose
而不是内联注释。
function Test-MrVerboseOutput {
[CmdletBinding()]
param (
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
foreach ($Computer in $ComputerName) {
Write-Verbose -Message "Attempting to perform some action on $Computer"
Write-Output $Computer
}
}
如果在没有 Verbose 参数的情况下调用函数,则不会显示详细输出。
Test-MrVerboseOutput -ComputerName Server01, Server02
通过 Verbose 参数调用函数时,会显示详细输出。
Test-MrVerboseOutput -ComputerName Server01, Server02 -Verbose
管道输入
如果希望函数接受管道输入,则需要进行一些额外的编码。 如本书前面所述,命令可以接受按值(按类型)或按属性名称显示的管道输入。 可以像编写本机命令那样编写函数,以便它们接受其中一种或两种类型的输入。
若要接受按值显示管道输入,请为该特定参数指定 ValueFromPipeline
参数属性。 请记住,只能按每个数据类型之一的值接受管道输入。 例如,如果有两个接受字符串输入的参数,则只有其中一个参数可以接受按值显示的管道输入,因为如果为两个字符串参数都指定了管道输入,则该管道输入不知道该绑定到哪个参数。 这就是我调用此类型的按类型显示的管道输入(而不是按值显示的管道输入)的另一个原因。
管道输入一次输入一个项,这类似于在 foreach
循环中处理项的方式。
如果要接受数组作为输入,则至少需要一个 process
块才能处理所有项。 如果只接受单个值作为输入,则不需要 process
块,但仍建议指定它以保持一致性。
function Test-MrPipelineInput {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline)]
[string[]]$ComputerName
)
PROCESS {
Write-Output $ComputerName
}
}
接受按属性名称显示的管道输入是类似的,但它是使用 ValueFromPipelineByPropertyName
参数属性指定的,并且可为任意数量的参数指定该输入,无需考虑数据类型。 关键在于,通过管道传送的命令输出必须具有与参数名称或函数的参数别名匹配的属性名称。
function Test-MrPipelineInput {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
PROCESS {
Write-Output $ComputerName
}
}
BEGIN
和 END
块是可选的。 BEGIN
在 PROCESS
块之前指定,用于在从管道接收项之前执行任何初始工作。 了解这一点很重要。 在 BEGIN
块中无法访问通过管道传入的值。 END
块将在 PROCESS
块之后指定,用于在处理完通过管道传入的所有值之后进行清理。
错误处理
当无法联系计算机时,以下示例中显示的函数会生成未经处理的异常。
function Test-MrErrorHandling {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
PROCESS {
foreach ($Computer in $ComputerName) {
Test-WSMan -ComputerName $Computer
}
}
}
可采用多种不同的方法在 PowerShell 中处理错误。 Try/Catch
是处理错误的较新式的方法。
function Test-MrErrorHandling {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
PROCESS {
foreach ($Computer in $ComputerName) {
try {
Test-WSMan -ComputerName $Computer
}
catch {
Write-Warning -Message "Unable to connect to Computer: $Computer"
}
}
}
}
尽管上面的示例中显示的函数使用错误处理,但它也会生成未经处理的异常,因为该命令不会生成终止错误。 了解这一点也很重要。 仅捕获终止错误。 将 ErrorAction 参数的值指定为 Stop,将非终止错误转换为终止错误。
function Test-MrErrorHandling {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
PROCESS {
foreach ($Computer in $ComputerName) {
try {
Test-WSMan -ComputerName $Computer -ErrorAction Stop
}
catch {
Write-Warning -Message "Unable to connect to Computer: $Computer"
}
}
}
}
除非确实有必要,否则请勿修改全局 $ErrorActionPreference
变量。 如果直接从 PowerShell 函数内使用 .NET 之类的内容,则不能针对命令本身指定 ErrorAction。 在这种情况下,你可能需要更改全局 $ErrorActionPreference
变量,但如果确实要更改它,请在尝试执行命令后立即将其更改回来。
基于注释的帮助
最佳做法是将基于注释的帮助添加到函数,以便你与之共享函数的人知道如何使用它们。
function Get-MrAutoStoppedService {
<#
.SYNOPSIS
Returns a list of services that are set to start automatically, are not
currently running, excluding the services that are set to delayed start.
.DESCRIPTION
Get-MrAutoStoppedService is a function that returns a list of services from
the specified remote computer(s) that are set to start automatically, are not
currently running, and it excludes the services that are set to start automatically
with a delayed startup.
.PARAMETER ComputerName
The remote computer(s) to check the status of the services on.
.PARAMETER Credential
Specifies a user account that has permission to perform this action. The default
is the current user.
.EXAMPLE
Get-MrAutoStoppedService -ComputerName 'Server1', 'Server2'
.EXAMPLE
'Server1', 'Server2' | Get-MrAutoStoppedService
.EXAMPLE
Get-MrAutoStoppedService -ComputerName 'Server1' -Credential (Get-Credential)
.INPUTS
String
.OUTPUTS
PSCustomObject
.NOTES
Author: Mike F Robbins
Website: http://mikefrobbins.com
Twitter: @mikefrobbins
#>
[CmdletBinding()]
param (
)
#Function Body
}
如果将基于注释的帮助添加到函数,可以像默认的内置命令一样为他们检索帮助。
用于在 PowerShell 中编写函数的所有语法似乎过于复杂,尤其是对于刚入门的用户。 通常,如果我不记得语法的某些内容,我会在单独的监视器上打开 ISE 的第二个副本,并在键入函数代码时查看“Cmdlet (高级函数) - 完成”代码片段。 可以使用 Ctrl+J 组合键在 PowerShell ISE 中访问代码片段。
总结
在本章中,你已了解有关在 PowerShell 中编写函数的基本知识,其中包括如何将函数转变为高级函数,以及在编写 PowerShell 函数(如参数验证、详细输出、管道输入、错误处理和基于注释的帮助)时应考虑的一些较重要的要素。
审阅
- 如何在 PowerShell 中获取批准的谓词的列表?
- 如何将 PowerShell 函数转换为高级函数?
- 应在何时将 WhatIf 和 Confirm 参数添加到 PowerShell 函数?
- 如何将非终止错误转换为终止错误?
- 为什么应该将基于注释的帮助添加到函数?
推荐阅读的主题
反馈
https://aka.ms/ContentUserFeedback。
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:提交和查看相关反馈