具有 'dynamic' ConfirmImpact 属性设置的 Powershell Cmdlet

Powershell Cmdlet with 'dynamic' ConfirmImpact attribute setting

我正在编写支持 ShouldProcess 的 Powershell cmdlet。我想要一个 'dynamic' 值,而不是固定的 ConfirmImpact 值,它取决于传递给 cmdlet 的参数值。让我举个例子。

假设我是网络托管服务提供商。我有很多网站,每个网站都属于以下类别之一,按重要性排序:ProductionTestDevelopment。作为托管管理的一部分,我有一个 Remove-WebSite cmdlet 用于销毁网站。以下代码说明了这一点:

Class WebSite {
    [string] $Name
    [string] $Category # Can be one of: Production, Test, Development
}

Function Remove-WebSite {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite
    )
    Write-Host "$($WebSite.Name) was destroyed"
}

目前网站未经确认即被销毁。虽然这很方便,但有太多实习生错误地破坏了生产现场,所以我希望通过利用 Powershell 的 ShouldProcess 功能,在 Remove-WebSite cmdlet 上多一点安全网。

所以我将 SupportsShouldProcessConfirmImpact 值添加到 CmdletBinding 属性。我的 cmdlet 定义变为:

Function Remove-WebSite {
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite
    )

    if ($PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)")) {
        Write-Host "$($WebSite.Name) was destroyed"
    }
}

根据此定义,现在要求任何调用 Remote-Website cmdlet 的人确认他们确实要销毁该站点。现在几乎没有任何生产站点被错误破坏,除了网络开发人员抱怨他们的自动化脚本已经停止工作。

我真正想要的是 cmdlet 的 ConfirmImpact 值在运行时根据网站类别的重要性而变化 - High 对于生产站点,Medium 用于测试站点,Low 用于开发站点。以下函数定义说明了这一点:

Function CategoryToImpact([string]$Category) {
    Switch ($Category) {
        'Production' {
            [System.Management.Automation.ConfirmImpact]::High
            break
        }
        'Test' {
            [System.Management.Automation.ConfirmImpact]::Medium
            break
        }
        'Development' {
            [System.Management.Automation.ConfirmImpact]::Low
            break
        }
        default {
            [System.Management.Automation.ConfirmImpact]::None
            break
        }
    }
}

Function Remove-WebSite {
    [CmdletBinding(SupportsShouldProcess=$true<#,ConfirmImpact="Depends!"#>)]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite
    )

    # This doesn't work but I hope it illustrates what I'd *like* to do
    #$PSCmdLet.ConfirmImpact = CategoryToImpact($WebSite.Category)

    if ($PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)")) {
        Write-Host "$($WebSite.Name) was destroyed"
    }
}

假设这是可能的,如何做到这一点?

这是完整的脚本和一些测试代码的粘贴:http://pastebin.com/kuk6HNm6

这不是您要的(我认为严格来说是不可能的),但这可能是更好的方法。

不用理会 ConfirmImpact,而是 prompt the user with $PSCmdlet.ShouldContinue()

根据Requesting Confirmation from Cmdlets中给出的指导(强调我的):

For most cmdlets, you do not have to explicitly specify ConfirmImpact. Instead, use the default setting of the parameter, which is Medium. If you set ConfirmImpact to High, the operation will be confirmed by default. Reserve this setting for highly disruptive actions, such as reformatting a hard-disk volume.

进一步:

Most cmdlets request confirmation using only the ShouldProcess method. However, some cases might require additional confirmation. For these cases, supplement the ShouldProcess call with a call to the ShouldContinue method.

...

If a cmdlet calls the ShouldContinue method, the cmdlet must also provide a Force switch parameter. If the user specifies Force when the user invokes the cmdlet, the cmdlet should still call ShouldProcess, but it should bypass the call to ShouldContinue.

鉴于此指导,我提出以下更改:

Function Remove-WebSite {
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite ,
        [Switch] $Force
    )

    if ($PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)")) {
        $destroy =
            $Force -or
            $WebSite.Category -ne 'Production' -or
            $PSCmdlet.ShouldContinue("Are you sure you want to destroy $($WebSite.Name)?", "Really destroy this?")
        if ($destroy) {
            Write-Host "$($WebSite.Name) was destroyed"
        }
    }
}

最简单的解决方案是删除$PSCmdlet.ShouldProcess方法调用,并根据我们自己的条件有条件地调用$PSCmdlet.ShouldContinue方法。这样做的问题是我们失去了 -WhatIf 功能。正如 briantist 指出的那样,$PSCmdlet.ShouldContinue 应该与 $PSCmdlet.ShouldProcess 一起使用,除非这会导致多余的确认提示,即用户提示两次,而一次就足够了。

通过实验,我发现通过在CmdletBinding属性声明中设置ConfirmImpact='None'ShouldProcess不再显示提示,但仍然returns $false 如果指定了 -WhatIf。因此 ShouldProcessShouldContinue 都可以被调用,并且仍然只向用户显示一个提示。然后我可以使用我自己的逻辑来确定是否调用 ShouldContinue

这是一个完整的解决方案:

# Represents a website
Class WebSite {
    # The name of the web site
    [string] $Name

    # The category of the website, which can be one of: Production, Test, Development
    [string] $Category # Can be one of

    <#
        Gets the ConfirmImpact level based on Category, as follows:

            Category     ConfirmImpact
            -----------  -------------
            Production   High
            Test         Medium
            Development  Low
            Default      None
    #>
    [System.Management.Automation.ConfirmImpact] GetImpact() {
        Switch ($this.Category) {
            'Production' {
                return [System.Management.Automation.ConfirmImpact]::High
            }
            'Test' {
                return [System.Management.Automation.ConfirmImpact]::Medium
            }
            'Development' {
                return [System.Management.Automation.ConfirmImpact]::Low
            }
        }
        return [System.Management.Automation.ConfirmImpact]::None
    }

    # String representation of WebSite
    [string] ToString() {
        return "$($this.Category) site $($this.Name)"
    }
}

<#
.SYNOPSIS
Destroys a WebSite

.DESCRIPTION
The Remove-WebSite cmdlet permanently destroys a website so use with care.
To avoid accidental deletion, the caller will be prompted to confirm the
invocation of the command if the value of $ConfirmPreference is less than
or equal to the Impact level of the WebSite. The Impact level is based
upon the category, as follows:

    Category     ConfirmImpact
    -----------  -------------
    Production   High
    Test         Medium
    Development  Low
    Default      None

.PARAMETER Website
The WebSite to destroy.

.PARAMETER Force
Destroys website without prompt

.PARAMETER Confirm
Require confirmation prompt always regardless of $ConfirmPreference

.PARAMETER WhatIf
Show what would happen if the cmdlet was run. The cmdlet is not run.

#>
Function Remove-WebSite {
    # Set ConfirmImpact to 'None' so that ShouldProcess automatically returns
    # true without asking for confirmation, regardless of the value of
    # $ConfirmPreference. 
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='None')]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite,
        [Switch] $Force
    )

    # Returns true without prompt unless -WhatIf is specified when, in which case
    # false is returned without prompt
    if ($PSCmdlet.ShouldProcess($WebSite)) {

        # Determine whether to continue with the command. Only destroy website if...
        $continue = 
            # ...forced to by Force parameter...
            $Force -or

            #...or the Impact level of the Website is less than $ConfirmPreference...
            $WebSite.GetImpact() -lt $ConfirmPreference -or

            #...or the user clicked 'Yes' in ShouldContinue prompt
            $PSCmdlet.ShouldContinue("Are you sure you want to destroy $($WebSite)?", $null)

        if ($continue) {
            Write-Host "$($WebSite.Name) was destroyed"
        }
    }
}