Powershell scripting: recommended way to implement

2020-03-10 05:47发布

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...

2条回答
唯我独甜
2楼-- · 2020-03-10 06:14

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 *
查看更多
别忘想泡老子
3楼-- · 2020-03-10 06:17

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.

查看更多
登录 后发表回答