Where is $PSCmdlet.ShouldProcess state stored between calls (and how can it be reset)?

Viewed 920

Consider the following contrived example:

function Test-ProcessContinue {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')]
    Param()

    for ($i = 1; $i -le 3; $i++) {
        if ($PSCmdlet.ShouldProcess("$i", "Process")) {
            Write-Output "Processing $i"
        }
        else {
            Write-Verbose "No chosen"
        }
    }

    for ($i = 1; $i -le 3; $i++) {
        if ($PSCmdlet.ShouldProcess("$i", "Process")) {
            Write-Output "Processing $i"
        }
        else {
            Write-Verbose "No chosen"
        }
    }

    $yta = $false; $nta = $false
    for ($i = 1; $i -le 3; $i++) {
        if ($PSCmdlet.ShouldContinue("$i", "Continue", [ref]$yta, [ref]$nta) -or $yta) {
            Write-Output "Continuing with $i"
        }
        elseif ($nta) {
            Write-Verbose "No to all chosen"
            break
        }
        else {
            Write-Verbose "No chosen"
        }
    }
}

...and one of its potential outputs:

PS C:\> Test-ProcessContinue -Verbose

Confirm
Are you sure you want to perform this action?
Performing the operation "Process" on target "1".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"): a
Processing 1
Processing 2
Processing 3
Processing 1
Processing 2
Processing 3

Continue
1
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"): a
Continuing with 1
Continuing with 2
Continuing with 3

In the case of the ShouldContinue loop (third for loop), I can see that the overload with the two by-reference boolean parameters is responsible for storing whether the end-user chose Yes to All or No to All into those two booleans.

However, in the case of the two ShouldProcess blocks (first two for loops), how is this state preserved?

In particular, in between the first two ShouldProcess blocks, how could I check if Yes to All or No to All were specified and/or what would I need to reset or clear in order to make the second ShouldProcess block ask for confirmation again?

(Favouring ShouldContinue over ShouldProcess is an option for fine-grained control, but it appears to lose the native/built-in support for [CmdletBinding(SupportsShouldProcess=$true)]

2 Answers

First, I'll address $PSCmdlet.ShouldContinue. This is basically a way to prompt on your own regardless of Confirm preferences.

$PSCmdlet.ShouldProcess on the other hand, doesn't always prompt. It takes into account the ConfirmImpact (which you set to High), and the $ConfirmPreference automatic variable, which defaults to High. The valid values are None, Low, Medium, and High and are meant to indicate how much of an impact a change has, so when $ConfirmPreference's value is equal to or less than a command's ConfirmImpact value, then ShouldProcess will prompt.

I know this isn't your direct question, but the background is important for answering what you should do.

The direct question: "where is the answer stored?" has a boring answer: it's stored in an internal variable in the class that defines the ShouldProcess method.

So, no, you can't get at it yourself, unfortunately.

But that brings us back to .ShouldContinue, which can take those references and store those values for you, so when you want the values, and want to be able to make decisions with them, you should use .ShouldContinue.

But, you should really use both. Because they do different things.

.ShouldProcess isn't just responsible for confirmation prompts, it's also responsible for handling -WhatIf/$WhatIfPreference; when you say your command SupportsShouldProcess you are also saying it supports -WhatIf. If you don't use .ShouldProcess, you'll get into the situation of having commands that appear to be safe but actually take action anyway.

So a pattern of something like this would cover your bases:

if ($PSCmdet.ShouldProcess('thing', 'do')) {
    if ($PSCmdlet.ShouldContinue('prompt')) {
        # do it
    }
}

Problem with this goes back to your confirm impact and preferences. If those line up, or if the user invoked your command with -Confirm, you'll be prompting twice: once in the .ShouldProcess and then again in the .ShouldContinue.

That kind of sucks unfortunately.

I wrote a thing that seems to work around this. It's predicated first on a function that allows you to run an arbitrary scriptblock with confirmation, so that you can still run .ShouldProcess while suppressing its prompt.

Then it also tries to calculate whether a prompt is needed or not, and then selectively call .ShouldContinue. I didn't demonstrate storing or resetting the yesToAll and noToAll vars because you already know how to do that.

This is mainly to demo a pattern that could be used to adhere to standard confirmation prompt semantics, with discoverability, support for the -Confirm parameter, $ConfirmPreference, and ConfirmImpact, while maintaining support for -Verbose and -WhatIf.

function Test-Should {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param()

    Begin {
        $local:ShouldConfirm = $ConfirmPreference -ne [System.Management.Automation.ConfirmImpact]::None -and 
                               $ConfirmPreference -le [System.Management.Automation.ConfirmImpact]::High # hard coded :(

        function Invoke-CommandWithConfirmation {
            [CmdletBinding(SupportsShouldProcess)]
            param(
                [Parameter(Mandatory)]
                [ScriptBlock]
                $ScriptBlock
            )

            Invoke-Command -NoNewScope -ScriptBlock $ScriptBlock
        }
    }

    Process {
        if (Invoke-CommandWithConfirmation -ScriptBlock { $PSCmdlet.ShouldProcess('target', 'action') } -Confirm:$false ) {
            if (-not $local:ShouldConfirm -or $PSCmdlet.ShouldContinue('query', 'caption')) {
                'Hi' | Write-Host
                'Hello' | Write-Verbose
            }
        }
    }
}

Invocations:

Test-Should
Test-Should -Confirm
Test-Should -Confirm:$false
Test-Should -Verbose
Test-Should -Verbose -WhatIf
Test-Should -WhatIf -Confirm
Test-Should -WhatIf -Confirm:$false

And so on, with different values of $ConfirmPreference and different values for the command's ConfirmImpact.

The one thing annoying is the value I marked as hard coded: it has to match what you set as the confirm impact for that command.

It turns out it's kind of a pain in the ass to get at that value programmatically, but maybe you could work that in in some way.

This is mostly an extension of @briantist answer, but I was struggling with how to fully implement this for a while but I have gotten it working the way I want and wanted to share in case anyone else has a similar goal as me. I am hoping to turn this into a complete class that does all of this for me, but baby steps.

Function New-Function {
    [CmdletBinding(
        ConfirmImpact='None',
        DefaultParameterSetName="Default",
        HelpURI="",
        SupportsPaging=$False,
        SupportsShouldProcess=$True,
        PositionalBinding=$True
    )] Param(
        [Parameter(ValueFromPipeline)]
        $Items,
        $Impact = 'Medium',
        [Switch]$Force
    )

    Begin {
        $PSCmdlet.GetDynamicParameters()
        If (-Not $PSBoundParameters.ContainsKey('Verbose')) {
            $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference')
        }

        If (-not $PSBoundParameters.ContainsKey('Confirm')) {
            $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference')
        }

        If (-not $PSBoundParameters.ContainsKey('WhatIf')) {
            $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference')
        }

        New-Variable -Name YesToAll -Value $False -Verbose:$False -Confirm:$False -WhatIf:$False
        New-Variable -Name NoToAll  -Value $False -Verbose:$False -Confirm:$False -WhatIf:$False
        
        [Bool]$Local:ShouldConfirm = $Force -OR $ConfirmPreference -ne [System.Management.Automation.ConfirmImpact]::None -and $ConfirmPreference -le [System.Management.Automation.ConfirmImpact]::$Impact
        $Local:ShouldProcess = ([Scriptblock]::Create(("Function ShouldProcess{{[CmdletBinding(SupportsShouldProcess, ConfirmImpact='{0}')]Param()`$PSCmdlet.ShouldProcess('{1}','{2}')}};ShouldProcess -Confirm:`$False" -f $Impact,'Target','Action')))
        $Local:ShouldContinue = ([ScriptBlock]::Create(("Function ShouldContinue {{[CmdletBinding(SupportsShouldProcess, ConfirmImpact='{0}')]Param()New-Variable -Name YesToAll -Value `$PSCmdlet.GetVariableValue('YesToAll') -Verbose:`$False -Confirm:`$False -WhatIf:`$False;New-Variable -Name NoToAll -Value `$PSCmdlet.GetVariableValue('NoToAll') -Verbose:`$False -Confirm:`$False -WhatIf:`$False;`$PSCmdlet.ShouldContinue('{1}','{2}',[Ref]`$YesToAll,[Ref]`$NoToAll);Set-Variable -Name YesToAll -Value `$YesToAll -Scope 1 -Confirm:`$False -Verbose:`$False -WhatIf:`$False;Set-Variable -Name NoToAll  -Value `$NoToAll -Scope 1 -Confirm:`$False -Verbose:`$False -WhatIf:`$False;}};ShouldContinue" -f $Impact,'Target','Action')))
    }
 
    Process {
        ForEach ($Item in $Items) {
            IF ((Invoke-Command -NoNewScope -ScriptBlock $Local:ShouldProcess) -eq $True) {
                If ($Force -or $Local:ShouldConfirm -eq $False -or (Invoke-Command -NoNewScope -ScriptBlock $Local:ShouldContinue)) {
                    IF ($Local:Force) {
                        Write-Verbose -Message 'Force'
                    } ElseIf ($Local:YesToAll -eq $True) {
                        Write-Verbose -Message 'YesToAll'
                    } Else {
                        Write-Verbose -Message 'Yes'
                    }
                    Write-Host $Item
                } Else {
                    If ($Local:NoToAll -eq $True) {
                        Write-Verbose -Message 'NoToAll'
                    } Else {
                        Write-Verbose -Message 'No'
                    }
                }
            }
        }
    }

    End {

    }
}
$ConfirmPreference = 'High'
'1','2','3','4','5','6','7','8','9' | New-Function -Impact Medium
Related