第 9 章 - 函数

需要经常修改的 PowerShell 单行命令和脚本通常是转换为可重用函数的良好选择。

尽可能编写函数,因为函数更面向工具。 可以将函数添加到脚本模块中,将该模块放在 $env:PSModulePath 中定义的位置,然后调用这些函数,而无需查找保存函数的位置。 使用 PowerShellGet 模块,可以轻松地在 NuGet 存储库中共享 PowerShell 模块。 PowerShellGet 随 PowerShell 5.0 及更高版本一起提供。 也可为 PowerShell 3.0 及更高版本单独下载。

不要把事情搞得过于复杂。 保持简单,并使用最直接的方法完成任务。 避免在重复使用的代码中使用别名和位置参数。 为代码设置格式以提高可读性。 不要硬编码数值;使用参数和变量。 即使不会造成任何伤害,也不要编写不必要的代码。 它增大了不必要的复杂性。 在编写任何 PowerShell 代码时,注意细节会带来很大的好处。

命名

在 PowerShell 中为函数命名时,请使用 帕斯卡命名法名称,并结合使用已批准的动词和单数名词。 要在 PowerShell 中获取已批准的动词列表,请运行 Get-Verb。 以下示例按照 Get-Verb 属性对 的结果进行了排序。

Get-Verb | Sort-Object -Property Verb

通过 Group 属性,可以了解动词的使用方式。

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 会话时会生成一条警告消息。 警告消息使你的函数显得不够专业。 未经批准的动词还会限制函数的可发现性。

简单函数

PowerShell 中的函数用函数关键字后跟函数名称以及一个左大括号和右大括号 ({ }) 来声明。 函数执行的代码包含在这些大括号中。

function Get-Version {
    $PSVersionTable.PSVersion
}

以下示例中的函数是一个返回 PowerShell 版本的简单示例。

Get-Version
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  693

在为函数使用通用名称(如 Get-Version)时,可能会导致命名冲突。 未来添加的默认命令或其他人可能编写的命令可能会与之冲突。 在函数名称的名词部分加上前缀,以防止命名冲突。 例如:<ApprovedVerb>-<Prefix><SingularNoun>

以下示例使用了前缀 PS

function Get-PSVersion {
    $PSVersionTable.PSVersion
}

除名称外,该函数与前一个函数完全相同。

Get-PSVersion
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  693

即使向名词添加前缀,你仍然可能会有命名冲突。 我喜欢用我的首字母作为函数名称的前缀。 制定并坚持使用一个标准。

function Get-MrPSVersion {
    $PSVersionTable.PSVersion
}

除了使用更独特的名称以避免与其他 PowerShell 命令发生命名冲突外,该函数与前两个函数没有什么不同。

Get-MrPSVersion
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  693

载入内存后,即可在 Function PSDrive 上看到函数。

Get-ChildItem -Path Function:\Get-*Version
CommandType     Name                                               Version
-----------     ----                                               -------
Function        Get-Version
Function        Get-PSVersion
Function        Get-MrPSVersion

如果要从当前会话中删除这些函数,请从 Function PSDrive 中删除它们,或关闭并重新打开 PowerShell。

Get-ChildItem -Path Function:\Get-*Version | Remove-Item

验证函数是否确实已删除。

Get-ChildItem -Path Function:\Get-*Version

如果函数是作为模块的一部分加载的,则可以卸载模块来删除它们。

Remove-Module -Name <ModuleName>

Remove-Module cmdlet 会从当前 PowerShell 会话的内存中删除 PowerShell 模块。 它不会将它们从系统或磁盘中删除。

参数

不要静态赋值。 请改用参数和变量。 命名参数时,尽可能使用与默认 cmdlets 相同的名称作为参数名称。

在下面的函数中,请注意我使用了 ComputerName 而不是 ComputerServerNameHost 作为参数名。 使用 ComputerName 来标准化参数名称,使其与默认 cmdlet 等的参数名称和大小写相匹配。

function Test-MrParameter {

    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

以下函数可查询系统中的所有命令,并返回带有特定参数名称的命令编号。

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 参数。 没有任何命令带有 ComputerServerNameHostMachine 等参数。

Get-MrParameterCount -ParameterName ComputerName, Computer, ServerName,
    Host, Machine
ParameterName NumberOfCmdlets
------------- ---------------
ComputerName               39
Computer                    0
ServerName                  0
Host                        0
Machine                     0

参数名称的大小写应与默认 cmdlets 相同。 例如,使用 ComputerName 而不是 computername。 这种命名方案可以帮助熟悉 PowerShell 的人发现你的函数,并且在外观和感觉上与默认 cmdlets 相似。

param 语句让你能够定义一个或多个参数。 参数定义之间以逗号 (,) 分隔。 有关详细信息,请参阅 about_Functions_Advanced_Parameters

高级函数

在 PowerShell 中将函数转化为高级函数非常简单。 函数与高级函数的区别之一是,高级函数具有自动添加的通用参数。 常见参数包括 VerboseDebug 等参数。

从上一节使用的 Test-MrParameter 函数开始。

function Test-MrParameter {

    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

有几种不同的方法可以查看通用参数。 一种方法是使用 Get-Command 查看语法。

Get-Command -Name Test-MrParameter -Syntax

注意 Test-MrParameter 函数没有任何通用参数。

Test-MrParameter [[-ComputerName] <Object>]

另一种方法是向下钻取 Get-Command 的参数属性。

(Get-Command -Name Test-MrParameter).Parameters.Keys
ComputerName

添加 CmdletBinding 属性,将函数转变为高级函数。

function Test-MrCmdletBinding {

    [CmdletBinding()] # 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 属性添加 WhatIfConfirm 风险缓解参数。 只有进行更改的命令才需要这些参数。

function Test-MrSupportsShouldProcess {

    [CmdletBinding(SupportsShouldProcess)]
    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

请注意,现在有 WhatIfConfirm 参数。

Get-Command -Name Test-MrSupportsShouldProcess -Syntax
Test-MrSupportsShouldProcess [[-ComputerName] <Object>] [-WhatIf] [-Confirm]
[<CommonParameters>]

同样,也可以使用 Get-Command 返回实际参数名称的列表,包括常用参数名称以及 WhatIfConfirm

(Get-Command -Name Test-MrSupportsShouldProcess).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable
WhatIf
Confirm

参数验证

尽早验证输入。 如果没有有效输入则无法完成,不允许代码继续执行。

参数变量必须始终指定数据类型。 在下面的示例中,String 被指定为 ComputerName 参数的数据类型。 这种验证限制了它只允许为 ComputerName 参数指定一个计算机名称。

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [string]$ComputerName
    )

    Write-Output $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]
   , ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-MrP
   arameterValidation

当前定义的问题在于,省略 ComputerName 参数的值是有效的,但要成功完成函数,则必须要有一个值。 在这种情况下,Mandatory 参数属性是有用的。

下面示例中使用的语法与 PowerShell 3.0 及更高版本兼容。 可以指定 [Parameter(Mandatory=$true)],使函数与 PowerShell 2.0 或更高版本兼容。

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ComputerName
    )

    Write-Output $ComputerName

}

既然 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 参数验证属性。

即使在设置默认值时,也尽量不要使用静态值。 在下面的示例中,$env:COMPUTERNAME 被用作了默认值,如果没有提供默认值,它将自动转换为本地计算机名称。

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    Write-Output $ComputerName

}

详细输出

如果要编写复杂的代码,则内联注释非常有用,但除非用户查看代码,否则不会看到它们。

以下示例中的函数在 foreach 循环内有一个内联注释。 虽然找到这条注释并不困难,但试想一下,要是这个函数包含数百行代码呢?

function Test-MrVerboseOutput {

    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    foreach ($Computer in $ComputerName) {
        #Attempting to perform an 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 an 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
    }

}

beginend 块是可选的。 beginprocess 块之前指定,用于在从管道接收项之前执行任何初始工作。 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 参数。 可以在调用 .NET 方法之前更改 $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: https://mikefrobbins.com
    Twitter: @mikefrobbins
#>

    [CmdletBinding()]
    param (

    )

    #Function Body

}

在为函数添加基于注释的帮助时,可以像默认内置命令一样为它们获取帮助。

对于初学者来说,PowerShell 中编写函数的所有语法似乎都让人不知所措。 如果记不住语法,请在另一个显示器上打开 PowerShell 集成脚本环境 (ISE) 的第二个实例,并在键入函数代码的同时查看 “Cmdlet(高级函数)- 完成 ”代码片段。 可以使用 Ctrl + J 组合键在 PowerShell ISE 中访问代码片段。

总结

在本章中,你学习了在 PowerShell 中编写函数的基础知识,包括如何:

  • 创建高级函数
  • 使用参数验证
  • 使用详细输出
  • 支持管道输入
  • 处理错误
  • 创建基于注释的帮助

审阅

  1. 如何在 PowerShell 中获取已批准的动词列表?
  2. 如何将 PowerShell 函数转变为高级函数?
  3. 何时应将 WhatIfConfirm 参数添加到你的 PowerShell 函数中?
  4. 如何将非终止性错误转换为终止错误?
  5. 在您的函数中为什么要添加基于注释的帮助?

参考