Supports should process…? Oh really?

I think I blogged about it a while ago, but I still see people who assume that stating that their command SupportsShouldProcess is enough to implement –WhatIf and –Confirm. This is partially true. Namely: –WhatIf part will probably work just fine. Confirm will work fine too most of the time. When it will start to misbehave? When we start to use pipeline, or multiple cmdlets that SupportShouldProcess.

Bad example

Let’s start with example of function that in my mind is not good enough:

function Test-ShouldProcess {            
[CmdletBinding(            
    SupportsShouldProcess = $true            
)]            
param (            
    [Parameter(            
        ValueFromPipeline = $true            
    )]            
    [string]$Path,            
    [string]$Destination            
)            
            
process {            
    Move-Item -Path $Path -Destination $Destination            
}            
}

It will work just fine when we pass single path inline. But what happens if we use pipeline? Let’s try:

PowerShell-Scripting-Games-Supports-Should-Process-Issue

Why this is happening? Let see what –Confirm actually does:

function Get-ConfirmPreferenceValue {            
[CmdletBinding(            
    SupportsShouldProcess = $true            
)]param()            
    $ConfirmPreference            
}

If you would run it with –Confirm parameter you will see it will modify value of ConfirmPreference from default (usually: High) to Low (read: any command that has ConfirmImpact will prompt you). This value won’t change no matter how many times you will say “Yes to All”. So if you want take over control and force cmdlets to ignore ConfirmImpact – you won’t be able to skip over prompting. Yes to All = Yes, No to All = No.

Good example

I hope I’ve convinced you that enabling SupportsShouldProcess is not enough. Especially if you want to perform several actions in a bulk, and each cmdlet Supports Should Process on its own (you will be prompted for each action). So how can you implement it to work as expected? First part: you need to stop cmdlet’s urge to prompt you. This is important, otherwise you won’t get far. I usually just explicitly pass value $false to –Confirm parameter. What else? We need to implement ShouldProcess method using $PSCmdlet automatic variable. And finally: let’s say you want to have command that will always prompt users (with ConfirmImpact equal ‘High’), unless he explicitly states this is not necessary. Rather than use Confirm with $false value, I prefer to have –Force switch that will bypass prompting. Here is full function “fixed”:

function Test-ShouldProcessEx {            
[CmdletBinding(            
    SupportsShouldProcess = $true            
)]            
param (            
    [Parameter(            
        ValueFromPipeline = $true            
    )]            
    [string]$Path,            
    [string]$Destination,            
    [switch]$Force            
)            
            
begin {            
    $PSBoundParameters.Remove('Force') | Out-Null            
    $PSBoundParameters.Confirm = $false            
}            
            
process {            
    if ($Force -or $PSCmdlet.ShouldProcess(            
        $Path,             
        "Move to $Destination"            
    )) {            
        Move-Item @PSBoundParameters            
    }            
}            
}            

I used $PSBoundParamters here to pass all required parameters to Move-Item, including $false value for –Confirm parameter. Using if statement together with $Force and $PSCmdlet.ShouldProcess method gives us option to prompt user. But because we control it and cmdlet prompting is disabled – $ConfirmPreference value has no effect on cmdlets used. If I say “Yes to All” or “No to All” now – no more prompts will show up, and all remaining files will be moved (or not):

PowerShell-Scripting-Games-Supports-Should-Process-Fixed

Keep in mind: this is super-simple example, with single cmdlet. Imagine you have few others that support should process (e.g. you create target folder using New-Item) – without taking over control, your prompts will be all over the place. So implementing ShouldProcess method of $PSCmdlet is not only way to control the prompt shown, it’s a way to feel confident that your script/ function does supports should process, not only claims it does. Puszczam oczko

Advertisements

24 thoughts on “Supports should process…? Oh really?

  1. Pingback: Do you really Support Should Process…? | PowerShell.org

  2. I appreciate you taking the time to flesh this out. I was one of the ones (I suspect) that was thinking it was good enough.

    A quick example to show how -confirm works with the pipeline in built-in cmdlets is this (I had to test it to see if it worked how your code does):

    ‘s*’,’t*’ | stop-service -confirm

    I had actually thought that saying “no to all” on one of the “s” services would not keep it from asking about the first and subsequent “t” services. That would have matched adding SupportsShouldProcess alone, I think. But it doesn’t work that way.

    For functions which don’t use the pipeline, though, which most beginning scripters will produce, I think adding SupportsShouldProcess is a great addition to the function.

    I said this elsewhere, but I’m kind of mad at myself for not getting involved in the scripting games before this. The possibilities for learning (even after only 1 event) seem unending.

    Thanks again for explaining this!

    • Thank you for taking time to read it. 🙂 And regarding enabling as implementing: I won’t argue that it may be good enough at times, but I really feel that pattern are good thing. If you start with [CmdletBinding(SupportsShouldProcess = $true)] why not spent few minutes and support it on your own rather then rely on cmdlet picking up ConfirmPreference and prompting because it is set to low…? 🙂

  3. THANK YOU for an excellent article, with really solid examples. This is something that bugs me a lot, and it’s tough to explain – great job.

  4. Pingback: Scripting Games: moje notatki – 1 | PowerShell po polsku

  5. Pingback: Event 1: my notes. | IT Pro PowerShell experience

  6. This is definitely a hot issue for event 1, and I think almost everyone, myself included, don’t fully understand how SupportsShouldProcess works natively and this definitily needs more community discussion or maybe documentation *hint* *hint*.

    Anyway, one thing I want to point out:

    if ($Force -or $PSCmdlet.ShouldProcess…

    I don’t think native PS cmdlets implement it like that, this would basically override -WhatIf if -Force was also provided. Which, just my two cents, is bad. Try it:

    ” | Out-File .\file.txt
    $ConfirmPreference = ‘Low’
    # Get prompted for the move
    Move-Item .\file.txt .\file2.txt
    # Still will get prompted
    Move-Item .\file2.txt .\file.txt -Force
    # Won’t get prompted, but won’t do anything either
    Move-Item .\file.txt .\file2.txt -Force -WhatIf

    • I think I picked up this technique from other PowerShell scripter long ago and never gave it second thought. 😉 I guess – mainly because it matches my expectations. But you are right – all cmdlets I tried won’t ignore -WhatIf and -Confirm if mixed with -Force. So I guess I should stop doing so too or document it leaving no doubt for future user that I will ignore “schizophrenic” syntax. 😉 Thanks for pointing that out!
      Re: documentation… There are few docs that briefly mention it, but I guess you are right, about_Functions_CmdletBindingAttribute and about_Functions_Advanced_Methods is not enough to understand how it works… 😦

      • With this in mind, and your comment above about patterns being a good thing, I’d love to see a pattern for SupportsShouldProcess which doesn’t muddy the water with -force. Any thoughts?

      • Well, that’s not that hard as it seams. Test this function:

        function Test-Force {
        [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High'
        )]
        param (
        [switch]$Force
        )

        process {
        If ($Force) {
        $ConfirmPreference = 'None'
        }
        if ($PSCmdlet.ShouldProcess(
        'Target',
        'Action'
        )) {
        'Doing my stuff'
        }
        }
        }

        It won’t prompt implicitly when current $ConfirmPreference is equal or lower than ConfirmImpact of command. At the same time – it won’t proceed if we explicitly pass -Confirm or -WhatIf.

  7. Great job, Bartek, I recently wrote a script that called several Windows executables. One was potentially harmful, so I wanted to add a Confirm to just that one.

    If ($a) { }
    elseif ($b) { } #-Confirm
    else { }

    I sought help from Lee Holmes, but could not get this to work, because the executable files did not have an inherent confirm value.

    Is there a way to do this?

    Thanks,
    June

    • If I understand correctly – you are in more comfortable situation, because you do not have to “take over” control. You are in charge with -WhatIf and -Confirm here:

      function Invoke-Batch {
      [CmdletBinding(
      SupportsShouldProcess = $true
      )]param ()
      ping ::1 -n 1
      if ($PSCmdlet.ShouldProcess(
      'localhost',
      'ping'
      )) {
      ping localhost -n 1
      }
      }

      I assume first ping is “not harmful” executable, the one in “if” is dangerous one. If you run Invoke-Batch -Confirm you will be prompted before second ping runs… But I sense that this was something else, I’m just missing some hook here. 😉

  8. Thanks! It now works when I use -Confirm — I had the $PSCmdlet in the script block instead of surrounding the condition. I want it to prompt me even if I don’t use -Confirm. I set the $ConfirmPreference to “Low”, but it doesn’t work without -Confirm. Is there a way around this?

  9. I fixed it! I used Invoke-Expression to call a string version of the executable. I added -Confirm and set $ConfirmPreference to “Low”. I’ve been working on this for two week. I really appreciate the help.

    [CmdletBinding(SupportsShouldProcess = $true)]
    param …
    $ConfirmPreference = “Low”
    if ($a) {…}
    elseif ($PSCmdlet.ShouldProcess($b)) {iex “scaryThing.exe” -Confirm}
    else {…}

    • Two alternatives… 🙂
      You can set ConfirmImpact to ‘High’ rather than leave default ‘Medium’:
      function Invoke-Batch {
      [CmdletBinding(
      SupportsShouldProcess = $true,
      ConfirmImpact = 'High'
      )]param ()

      This will force -Confirm unless $ConfirmPreference will be set to ‘None’.
      Second:
      $PSDefaultParameterValues += @{
      'Invoke-Batch:Confirm' = $true
      }

      BTW: my iex does not support -Confirm… 😦 Proxy? vNext? 😉

  10. ” If you would run it with –Confirm parameter you will see it will modify value of ConfirmPreference from default (usually: High) to Low (read: any command that has ConfirmImpact will prompt you). This value won’t change no matter how many times you will say “Yes to All”. ”

    From the above statement should we take this as a bug? If somebody reports it to MS connect then I will for sure support a fix for it in next version of PS.

    Thanks a lot for great blog and they way you show it is clear.

    • No, I wouldn’t see it as a bug, just a way to make sure that SupportsShouldProcess alone grants some kind of support for -WhatIf and -Confirm. “Yes to all” can’t have any effect on $ConfirmPreference, because it would mean that “child” (cmdlet) modified something in parent (our function). *That* would be buggy implementation, if you would ask me… 🙂

  11. Pingback: SupportsShouldProcess | clan8blog

  12. Hi Bartek!

    I think you have to revisit this Article.
    Adam Weigert wrote even a good Article about shouldprocess and he is pointing out that using
    if ($Force -or $PSCmdlet.ShouldProcess( …
    is a bad practice.
    If you run this, the -WhatIf is ignored

    See here: http://www.iheartpowershell.com/2013/05/powershell-supportsshouldprocess-worst.html

    greets Peter Kriegel
    founder member of the German speaking PowerShell Community
    http://www.PowerShell-Group.eu

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s