如何在调用其他Cmdlet的Cmdlet中支持PowerShell的-WhatIf和-Confirm参数?

28
我有一个 PowerShell 脚本 cmdlet,支持 -WhatIf-Confirm 参数。它通过在执行更改之前调用 $PSCmdlet.ShouldProcess() 方法来实现此功能。这很好地解决了问题。 但我的 Cmdlet 是通过调用其他 Cmdlet 来实现的,而我无法将 -WhatIf-Confirm 参数传递给我调用的Cmdlets。 如何将 -WhatIf-Confirm 的值传递给我的Cmdlet调用的Cmdlets呢? 例如,如果我的Cmdlet是Stop-CompanyXyzServices,并且它使用Stop-Service来实现其操作, 如果将 -WhatIf 传递给 Stop-CompanyXyzServices,则我希望它也被传递到 Stop-Service。是否可以实现呢?
5个回答

19

显式传递参数

您可以使用$WhatIfPreference$ConfirmPreference变量来传递-WhatIf-Confirm参数。以下示例使用参数分离技术实现:

if($ConfirmPreference -eq 'Low') {$conf = @{Confirm = $true}}

StopService MyService -WhatIf:([bool]$WhatIfPreference.IsPresent) @conf
$WhatIfPreference.IsPresent如果在包含函数上使用-WhatIf开关,将会返回True。在包含函数上使用-Confirm开关会暂时将$ConfirmPreference设置为low

隐式传递参数

既然-Confirm-WhatIf可以自动设置$ConfirmPreference$WhatIfPreference变量,那么传递它们实际上是必要的吗?

考虑以下示例:

function ShouldTestCallee {
    [cmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')] 
    param($test)

    $PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Confirm?")
}


function ShouldTestCaller {
    [cmdletBinding(SupportsShouldProcess=$true)]
    param($test)

    ShouldTestCallee
}

$ConfirmPreference = 'High'
ShouldTestCaller
ShouldTestCaller -Confirm

ShouldTestCallerShouldProcess()方法返回True

即使我没有传递开关,使用ShouldTestCaller -Confirm会导致确认提示。

编辑:

@manojlds的回答让我意识到我的解决方案总是将$ConfirmPreference设置为“Low”或“High”。我已更新我的代码,仅在确认首选项为“Low”时才设置-Confirm开关。


+1 表示 $WhatIfPreference.IsPresent 已经生效。我不确定它是否适用于 OP 所需的解决方案。 - manojlds
1
使用这个方法可以得到完整的解决方案。请查看我的答案。之前不知道$WhatIfPreference,现在知道了,很好。 - manojlds
@Dan说他已经有一个支持“-WhatIf”和“-Confirm”参数的脚本了。我认为我不需要再写其他代码了。 - Rynant
1
"-Confirm"和"-WhatIf"将会被你调用的命令继承。 - JasonMArcher
1
回答不错,但这些编辑使结论变得混乱且难以理解。那么,正确的方式是:$ConfirmPreference.IsPresent 还是依赖于首选项变量的传递机制? - alecov
@Alek,就个人而言,我更喜欢使用传递机制(隐式传递-WhatIf-Confirm)来代替函数和cmdlets。你需要注意这些开关是否被每个命令所支持,并且将调用它们包装在$PSCmdlet.ShouldProcess()中。 - Charlie Joynt

7

在进行一些搜索后,我找到了一个不错的解决方案,可以将常用参数传递给被调用的命令。您可以使用 @ 展开运算符来传递传递给您的命令的所有参数。例如,如果

Start-Service -Name ServiceAbc @PSBoundParameters

在您的PowerShell脚本中,PowerShell将把传递给您的脚本的所有参数传递给Start-Service命令。唯一的问题是,如果您的脚本包含一个 -Name 参数,它也会被传递,PowerShell会投诉您包含了两个 -Name 参数。我编写了以下函数来将所有公共参数复制到一个新字典中,然后展开该字典。

function Select-BoundCommonParameters
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        $BoundParameters
    )
    begin
    {
        $boundCommonParameters = New-Object -TypeName 'System.Collections.Generic.Dictionary[string, [Object]]'
    }
    process
    {
        $BoundParameters.GetEnumerator() |
            Where-Object { $_.Key -match 'Debug|ErrorAction|ErrorVariable|WarningAction|WarningVariable|Verbose' } |
            ForEach-Object { $boundCommonParameters.Add($_.Key, $_.Value) }

        $boundCommonParameters
    }
}

最终的结果是您将像“-Verbose”这样的参数传递给脚本中调用的命令,并且它们会遵循调用者的意图。

当我将它插入到一些现有的代码中时,我遇到了一些问题——PSBoundParameters实际上是System.Management.Automation.PSBoundParametersDictionary类型。我通过在一个数组中建立不匹配键的方式来解决这个问题,然后调用$BoundParameters.Remove()来剥离非命令参数,最后在end{}块中返回修改后的PSBoundParameters。 - Stephen Connolly

3

这里是基于@Rynant和@Shay Levy的答案的完整解决方案:

function Stop-CompanyXyzServices
{
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')]

    Param(
        [Parameter(
            Position=0,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true
        )]      
        [string]$Name
    )

    process
    {
        if($PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Stop XYZ services '$Name'")){  
            ActualCmdletProcess
        }
        if([bool]$WhatIfPreference.IsPresent){
            ActualCmdletProcess
        }
    }
}

function ActualCmdletProcess{
# add here the actual logic of your cmdlet, and any call to other cmdlets
Stop-Service $name -WhatIf:([bool]$WhatIfPreference.IsPresent) -Confirm:("Low","Medium" -contains $ConfirmPreference)
}

我们需要查看是否将-WhatIf单独传递,以便可以将whatif传递给各个cmdlet。 ActualCmdletProcess基本上是重构,以便您不必为了WhatIf再次调用相同的命令集。希望这能帮助某些人。

我认为你不应该检查"Low","Medium" -contains $ConfirmPreference。设置-Confirm:$true会将$ConfirmPreference设置为'low',但是如果它是'Medium',你的代码将把$ConfirmPreference设置为'Low'。但我想知道是否需要传递-Confirm-WhatIF;请参见我的编辑。 - Rynant
@Rynant - 它们必须这样做,以便为每个单独的 cmdlet 和我们正在编写的自定义 cmdlet 进行确认。因此,将会有多个确认。 - manojlds
你试过我的例子了吗?函数 ShouldTestCallee 会根据 ShouldTestCaller 上是否使用 -Confirm 来确认,即使我没有传递确认参数。 - Rynant

1

根据@manojlds的评论更新

将$WhatIf和$Confirm转换为布尔值,并将这些值传递给基础cmdlet:

function Stop-CompanyXyzServices
{
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')]

    Param(
        [Parameter(
            Position=0,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true
        )]      
        [string]$Name
    )


    process
    {
        if($PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Stop service '$Name'"))
        {                   
            Stop-Service $name -WhatIf:([bool]$WhatIf) -Confirm:([bool]$confirm)
        }                       
    }
}

你确定这个能行吗?在 cmdlet 中,我觉得不行。而且即使是 whatif 输出也只显示自定义 cmdlet,而 OP 已经从 ShouldProcess 中获取了该输出。 - manojlds
抱歉,不起作用。这也是我的初始解决方案,所以我很希望它能成功。但有两个问题 - 即使你使用 "-WhatIf","$WhatIf"仍未被设置。@Rynant的建议“$ WhatIfPreference.IsPresent”确实可以解决这个问题。此外,由于您正在if检查的内部执行此操作,“-WhatIf”将不会被设置。 - manojlds
不确定为什么它对你不起作用。如果我在没有WhatIf或confirm的情况下调用函数,我会得到一个确认,我按回车键,服务就停止了。如果我使用WhatIf调用函数,我会得到一个whatif文本。如果我使用Confirm调用它,我会得到一个确认,按回车键,服务就停止了。 - Shay Levy
嘿,它可以这样工作,但OP正在寻求对每个cmdlet的确认和whatif。如果我的答案能够清楚地表达我的意思和我认为OP想要的内容,请查看我的答案。基本上,当我提供whatif时,它应该从我们的cmdlet开始,然后从使用的每个cmdlet开始提供whatif。 - manojlds

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接