Powershell脚本编写:当函数调用嵌套时实现ShouldProcess的推荐方法?

发布于 2024-08-22 05:17:08 字数 2787 浏览 9 评论 0原文

测试脚本:

function outer
{
    [cmdletbinding(supportsshouldprocess=$true)]
    param($s)

    process
    {        
        $pscmdlet.shouldprocess("outer $s", "ShouldProcess") | out-null
        "" | out-file "outer $s"

        inner ImplicitPassthru
        inner VerbosePassthru -Verbose:$Verbose 
        inner WhatifPassthru -WhatIf:$WhatIf
    }
}

function inner
{
    [cmdletbinding(supportsshouldprocess=$true)]
    param($s)

    process
    {   
        $pscmdlet.shouldprocess("inner $s", "ShouldProcess") | out-null
        "" | out-file "inner $s"
    }
}

"`n** NORMAL **"
outer normal
"`n** VERBOSE **"
outer verbose -Verbose
"`n** WHATIF **"
outer whatif -WhatIf

输出:

** NORMAL **
VERBOSE: Performing operation "ShouldProcess" on Target "inner VerbosePassthru".
What if: Performing operation "ShouldProcess" on Target "inner WhatifPassthru".
What if: Performing operation "Output to File" on Target "inner WhatifPassthru".

** VERBOSE **
VERBOSE: Performing operation "ShouldProcess" on Target "outer verbose".
VERBOSE: Performing operation "ShouldProcess" on Target "inner VerbosePassthru".
What if: Performing operation "ShouldProcess" on Target "inner WhatifPassthru".
What if: Performing operation "Output to File" on Target "inner WhatifPassthru".

** WHATIF **
What if: Performing operation "ShouldProcess" on Target "outer whatif".
What if: Performing operation "Output to File" on Target "outer whatif".
What if: Performing operation "ShouldProcess" on Target "inner ImplicitPassthru".
What if: Performing operation "Output to File" on Target "inner ImplicitPassthru".
What if: Performing operation "ShouldProcess" on Target "inner VerbosePassthru".
What if: Performing operation "Output to File" on Target "inner VerbosePassthru".
What if: Performing operation "ShouldProcess" on Target "inner WhatifPassthru".
What if: Performing operation "Output to File" on Target "inner WhatifPassthru".

在我看来,这里有几个奇怪的地方:

  • 指定 -WhatIf:$foo 将始终在被调用者(及其被调用者)中打开 $WhatIf,无论 $foo 是什么。
  • 当您确实指定 -WhatIf “for real”(不将其限制为现有变量)时,它会隐式传播到被调用者。无需直通或泼溅。
  • 与 -WhatIf 不同,显式 -Verbose 不会隐式级联到被调用者。
  • 当您尝试手动传递 -Verbose:$foo 时,您确实会看到与 -WhatIf:$foo 类似的行为。但它只影响手动测试 $psCmdlet.ShouldProcess() 的脚本——内置 cmdlet 不受影响。

注意:Confirm 的行为与 WhatIf 相同。为了简洁起见,我省略了它。

在网络和 Connect 中搜索,我几乎看不到任何有关高级功能的 ShouldProcess 行为(赞成或反对)的深入讨论。最接近的是 James 的帖子O'Neill 建议在整个调用堆栈中传递 $psCmdlet 的单个实例。然而,他这样做是为了解决一个完全不同的问题(避免多个 -Confirm 提示)。同时,当您坚持使用为每个函数提供的标准 $psCmdlet 时,我没有看到任何有关预期内容的文档......更不用说设计模式、最佳实践等......

Test script:

function outer
{
    [cmdletbinding(supportsshouldprocess=$true)]
    param($s)

    process
    {        
        $pscmdlet.shouldprocess("outer $s", "ShouldProcess") | out-null
        "" | out-file "outer $s"

        inner ImplicitPassthru
        inner VerbosePassthru -Verbose:$Verbose 
        inner WhatifPassthru -WhatIf:$WhatIf
    }
}

function inner
{
    [cmdletbinding(supportsshouldprocess=$true)]
    param($s)

    process
    {   
        $pscmdlet.shouldprocess("inner $s", "ShouldProcess") | out-null
        "" | out-file "inner $s"
    }
}

"`n** NORMAL **"
outer normal
"`n** VERBOSE **"
outer verbose -Verbose
"`n** WHATIF **"
outer whatif -WhatIf

Output:

** NORMAL **
VERBOSE: Performing operation "ShouldProcess" on Target "inner VerbosePassthru".
What if: Performing operation "ShouldProcess" on Target "inner WhatifPassthru".
What if: Performing operation "Output to File" on Target "inner WhatifPassthru".

** VERBOSE **
VERBOSE: Performing operation "ShouldProcess" on Target "outer verbose".
VERBOSE: Performing operation "ShouldProcess" on Target "inner VerbosePassthru".
What if: Performing operation "ShouldProcess" on Target "inner WhatifPassthru".
What if: Performing operation "Output to File" on Target "inner WhatifPassthru".

** WHATIF **
What if: Performing operation "ShouldProcess" on Target "outer whatif".
What if: Performing operation "Output to File" on Target "outer whatif".
What if: Performing operation "ShouldProcess" on Target "inner ImplicitPassthru".
What if: Performing operation "Output to File" on Target "inner ImplicitPassthru".
What if: Performing operation "ShouldProcess" on Target "inner VerbosePassthru".
What if: Performing operation "Output to File" on Target "inner VerbosePassthru".
What if: Performing operation "ShouldProcess" on Target "inner WhatifPassthru".
What if: Performing operation "Output to File" on Target "inner WhatifPassthru".

To my eye there are several oddities here:

  • Specifying -WhatIf:$foo will always turn on $WhatIf in the callee (and its callees), no matter what $foo is.
  • When you do specify -WhatIf "for real" (without constraining it to an existing variable), it propagates to callees implicitly. No need for passthru or splatting.
  • Unlike -WhatIf, explicit -Verbose does not cascade to callees implicitly.
  • When you try to manually passthru -Verbose:$foo, you do see behavior is similar to -WhatIf:$foo. But it only affects scripts that manually test $psCmdlet.ShouldProcess() -- built in cmdlets aren't affected.

N.B.: Confirm behaves identical to WhatIf. I omitted it for brevity.

Searching the web and Connect, I see hardly any in-depth discussion of ShouldProcess behavior (pro or con) as pertains to advanced functions. Closest thing is a post from James O'Neill that recommends passing a single instance of $psCmdlet throughout the call stack. However, he does so to workaround an entirely different problem (avoiding multiple -Confirm prompts). Meanwhile, when you stick with the standard $psCmdlet provided to each function, I see no docs on what to expect...much less design patterns, best practices, etc...

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

李白 2024-08-29 05:17:08

您不能真正引用 $WhatIf 或 $Verbose,因为它们是为您合成的,即这些变量在您的函数中不存在。如果用户指定了它们,那么您可以通过 $PSBoundParameters 获取它们,但如果用户没有指定,那么显然它们不会出现在这个哈希表中。

当您将值传递给开关时,PowerShell 将执行典型的强制过程以尝试将指定值转换为布尔值。由于 $whatif 未定义,因此它的值为 $null,这会导致开关值设置为 $true。这可能是因为它看到开关被显式指定,实际上没有值,这相当于只指定 -Whatif 没有值。当您跟踪参数绑定时,您可以看到这一点:

function Foo
{
    [CmdletBinding(SupportsShouldProcess=1)]
    param()

    Process
    {
        $PSBoundParameters
    }
}

Trace-Command -name ParameterBinding -expr {Foo -whatif:$xyzzy} -PSHost
DEBUG: BIND NAMED cmd line args [Foo]
DEBUG:   BIND arg [] to parameter [WhatIf]
DEBUG:     COERCE arg to [System.Management.Automation.SwitchParameter]
DEBUG:       Arg is null or not present, type is SWITCHPARAMTER, value is true.
DEBUG:         BIND arg [True] to param [WhatIf] SUCCESSFUL
DEBUG: BIND POSITIONAL cmd line args [Foo]
DEBUG: MANDATORY PARAMETER CHECK on cmdlet [Foo]
DEBUG: CALLING BeginProcessing
DEBUG: CALLING EndProcessing

根据外部是使用 -verbose 还是 -whatif 调用,$WhatIfPreference 和 $VerbosePreference 在外部中进行适当设置。我可以看到这些值很好地传播到内部。 $pscmdlet.ShouldProcess 似乎存在 PowerShell 错误。在这种情况下,它似乎没有尊重 $VerbosePreference 的价值。您可以尝试像这样通过 -Verbose 传递到内部:

inner VerbosePassthru -Verbose:($VerbosePreference -eq 'Continue')

另一种选择是使用 Get-Variable -Scope ,如下所示:

function Outer
{
    [CmdletBinding(SupportsShouldProcess=1)]
    param()

    Process
    {
        $pscmdlet.ShouldProcess("Outer process", '') > $null
        inner
        #inner -Verbose:($VerbosePreference -eq 'Continue')
    }
}

function Inner
{
    [CmdletBinding(SupportsShouldProcess=1)]
    param()

    Process
    {
        $pscmdlet = (Get-Variable -Scope 1 -Name PSCmdlet).Value
        $pscmdlet.ShouldProcess("Inner process", '') > $null
        "Inner $VerbosePreference"
    }
}

Outer -Verbose

我不确定我喜欢这个,因为它意味着您知道外部比内部高 1 级。您可以“遍历”作用域堆栈,在堆栈中查找下一个 PSCmdlet 变量。这有效地消除了必须传递 PSCmdlet (这很恶心)的情况,但它仍然是一个 hack。您应该考虑在 MS Connect 上提交有关此问题的错误。

You can't really refer to $WhatIf or $Verbose since these are synthesized for you i.e. these variables don't exist in your function. If the user specifies them then you can get at them via $PSBoundParameters but if the user didn't specify then obviously they won't be in this hashtable.

When you pass a value to a switch PowerShell will do the typical coercion process to attempt to convert the specified value to a bool. Since $whatif isn't defined this evals to a $null which results in the switch value being set to $true. This is presumably because it sees the switch is explicitly specified with effectively no value which is the equivalent of just specifying -Whatif with no value. You can see this when you trace the parameter binding:

function Foo
{
    [CmdletBinding(SupportsShouldProcess=1)]
    param()

    Process
    {
        $PSBoundParameters
    }
}

Trace-Command -name ParameterBinding -expr {Foo -whatif:$xyzzy} -PSHost
DEBUG: BIND NAMED cmd line args [Foo]
DEBUG:   BIND arg [] to parameter [WhatIf]
DEBUG:     COERCE arg to [System.Management.Automation.SwitchParameter]
DEBUG:       Arg is null or not present, type is SWITCHPARAMTER, value is true.
DEBUG:         BIND arg [True] to param [WhatIf] SUCCESSFUL
DEBUG: BIND POSITIONAL cmd line args [Foo]
DEBUG: MANDATORY PARAMETER CHECK on cmdlet [Foo]
DEBUG: CALLING BeginProcessing
DEBUG: CALLING EndProcessing

The $WhatIfPreference and $VerbosePreference gets set appropriately in outer based on whether outer was called with -verbose or -whatif. I can see that those values propagate to inner just fine. It would seem that there is a PowerShell bug with $pscmdlet.ShouldProcess. It doesn't seem to be honoring the value of $VerbosePreference in this case. You could try passing through -Verbose to inner like so:

inner VerbosePassthru -Verbose:($VerbosePreference -eq 'Continue')

Another option is to use Get-Variable -Scope like so:

function Outer
{
    [CmdletBinding(SupportsShouldProcess=1)]
    param()

    Process
    {
        $pscmdlet.ShouldProcess("Outer process", '') > $null
        inner
        #inner -Verbose:($VerbosePreference -eq 'Continue')
    }
}

function Inner
{
    [CmdletBinding(SupportsShouldProcess=1)]
    param()

    Process
    {
        $pscmdlet = (Get-Variable -Scope 1 -Name PSCmdlet).Value
        $pscmdlet.ShouldProcess("Inner process", '') > $null
        "Inner $VerbosePreference"
    }
}

Outer -Verbose

I'm not sure I like this because it implies that you know outer is 1 level above inner. You could "walk" the scope stack looking for the next PSCmdlet variable up the stack. This effectively gets rid of having to pass in PSCmdlet (which is gross) but it's still a hack. You should consider filing a bug on MS Connect about this.

情感失落者 2024-08-29 05:17:08

我一直想写出完全相同的问题,而我在差不多 7 年后才写下这篇文章。令我惊讶的是,微软的 PowerShell 团队还没有修复这个问题。我已使用 PowerShell Version 6 Preview(最新版本)重现了该问题。

我想出了一个简单的解决方法,即在 Inner 函数内,我们创建并运行一个 scriptblock,设置 -Verbose 标志通过检查 $VerbosePreference 是否正确设置为 Continue,即使 ShouldProcess 不尊重它:


Function Outer {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    param([string]$Name)

    Process {
        Write-Host "Outer called";
        Inner $Name
    }
}

Function Inner {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    param([string]$Name)

    Process {
        if (-not ($PSBoundParameters.ContainsKey('Verbose'))) {
            $PSBoundParameters.Add('Verbose', [bool]$VerbosePreference -eq 'Continue');
        }

        & {
            [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]

            param([string]$Name)

            if ($PSCmdlet.ShouldProcess($Name, "Inner")) {
                Write-Host "Inner called";
            }
        } @PSBoundParameters;
    }
}

Export-ModuleMember *

I was looking to write exactly the same question, and I am writing this almost 7 years later. I am surprised that Microsoft's PowerShell team have not fixed this yet. I have reproduced the issue with PowerShell Version 6 Preview (latest version).

I have come up with a simple workaround, that is, inside the Inner function, we create and run a scriptblock, setting the -Verbose flag by checking $VerbosePreference which is correctly set to Continue, even though it is not respected by ShouldProcess:


Function Outer {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    param([string]$Name)

    Process {
        Write-Host "Outer called";
        Inner $Name
    }
}

Function Inner {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    param([string]$Name)

    Process {
        if (-not ($PSBoundParameters.ContainsKey('Verbose'))) {
            $PSBoundParameters.Add('Verbose', [bool]$VerbosePreference -eq 'Continue');
        }

        & {
            [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]

            param([string]$Name)

            if ($PSCmdlet.ShouldProcess($Name, "Inner")) {
                Write-Host "Inner called";
            }
        } @PSBoundParameters;
    }
}

Export-ModuleMember *
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文