Skip to main content

PowerShell SupportsShouldProcess Worst & Best Practices

This has been a very big discussion within the Scripting Games 2013 community and I want to add my two cents in an official blog post.

I've left several people comments on how they might be misunderstanding how SupportsShouldProcess works, but I also realize, everyone of these individuals has given me more insight into its use and perhaps, how it should best be utilized.

For those of you that don't know, SupportsShouldProcess is a parameter on the CmdletBinding attribute you can place on your cmdlets that automatically adds the -WhatIf and -Confirm parameters. These will naturally flow into other cmdlets you use that also SupportsShouldProcess, e.g. New-Item, Move-Item.

The major discussion has been around, should you just let the other cmdlets handle the $PSCmdlet.ShouldProcess feature, and if not how should you implement it. ShouldProcess has the following definitions.
  1. OverloadDefinitions                                                                                                                                                                                           
  2. -------------------                                                                                                                                                                                           
  3. bool ShouldProcess(string target)                                                                                                                                                                             
  4. bool ShouldProcess(string target, string action)                                                                                                                                                              
  5. bool ShouldProcess(string verboseDescription, string verboseWarning, string caption)                                                                                                                          
  6. bool ShouldProcess(string verboseDescription, string verboseWarning, string caption, [ref] System.Management.Automation.ShouldProcessReason shouldProcessReason)  
First, a bare bones example of how ShouldProcess works, it really is simple, this is the bare bones what you need to implement (minus the else statement, will discuss that later).
  1. function Test-ShouldProcess {  
  2.     [CmdletBinding(SupportsShouldProcess)]  
  3.     param()  
  4.   
  5.     process {  
  6.         if ($PSCmdlet.ShouldProcess('TARGET')) {  
  7.             'It will be done'  
  8.         } else {  
  9.             'Maybe later then'  
  10.         }  
  11.     }  
  12. }  
  13.   
  14. Test-ShouldProcess -Confirm  
  15. Test-ShouldProcess -WhatIf  
Next, lets look at ways you should not implement it, anti-patterns, always wanted to discuss one of these, but I digress.
  1. function Test-ShouldProcess {  
  2.     [CmdletBinding(SupportsShouldProcess)]  
  3.     param([switch$Force)  
  4.   
  5.     process {  
  6.         if ($Force -or $PSCmdlet.ShouldProcess('TARGET')) {  
  7.             'It will be done'  
  8.         } else {  
  9.             'Maybe later then'  
  10.         }  
  11.     }  
  12. }  
  13.   
  14. Test-ShouldProcess -WhatIf -Force  
Why exactly is this bad? If you run this, the -WhatIf is ignored - try this on Move-Item, it does not behave this way, and is not what the -Force parameter does to the logic of the cmdlet. There may be other cmdlets that behave slightly differently, but this comes back to my first rule of PowerShell.

Behave like native PowerShell cmdlets, when possible. 

Nothing makes for a harder to utilize cmdlet that doesn't follow the existing native cmdlet patterns, e.g. verb, noun, parameter names, pipeline behavior, etc...

Another way to not implement SupportsShouldProcess is the following.
  1. function Test-ShouldProcess {  
  2.     [CmdletBinding(SupportsShouldProcess)]  
  3.     param([string] $Path)  
  4.   
  5.     process {  
  6.         if ($PSCmdlet.ShouldProcess($Path)) {  
  7.             New-Item -Path $Path -ItemType File  
  8.         } else {  
  9.             "The item '$Path' will not be created."  
  10.         }  
  11.     }  
  12. }  
  13.   
  14. $ConfirmPreference = 'Low'  
  15. Test-ShouldProcess -Path .\file.txt -Confirm  
The $ConfirmPreference is changed to trigger a confirm from New-Item (assuming you use a different cmdlet that is a ConfirmPriority='High' cmdlet you wouldn't need to do this, it is just for demonstration purposes.

In this case, this design is flawed because you will be prompted twice, once but the Test-ShouldProcess cmdlet, and again by the New-Item cmdlet.

This test, leads me into my next best practice statement.

Don't do what PowerShell does for you natively.

If you run the previous example with the -WhatIf statement, you will see two outputs.
  1. Test-ShouldProcess -Path .\file.txt -Confirm -WhatIf  
  2. What if: Performing operation "Test-ShouldProcess" on Target ".\file.txt".  
  3. The item '.\file.txt' will not be created.  
Line #2 is the ShouldProcess statement that PowerShell does for you. Line #3 is from the else statement. PowerShell already informed the user what was going on, the cmdlet really doesn't need to help out here.


And lastly, lets talk confirmation impact. ConfirmImpact goes hand-in-hand with SupportsShouldProcess. If you do use it, you should set it to one of the following values: Low, Medium, High. The default if you don't use it is Medium.

Now, you can certainly use $PSCmdlet.ShouldProcess without SupportsShouldProcess, but you don't get native -WhatIf or -Confirm parameters, and if ConfirmImpact is as high or higher than $ConfirmPreference then it will trigger a confirmation prompt, otherwise it will just continue on because ShouldProcess will return true.
  1. function Test-ShouldProcess {  
  2.     [CmdletBinding(ConfirmImpact = 'High')]  
  3.     param([string] $Path)  
  4.   
  5.     process {  
  6.         if ($PSCmdlet.ShouldProcess($Path)) {  
  7.             New-Item -Path $Path -ItemType File  
  8.         } else {  
  9.             "The item '$Path' will not be created."  
  10.         }  
  11.     }  
  12. }  
  13.   
  14. $ConfirmPreference = 'High'  
  15. Test-ShouldProcess -Path .\file.txt  
This is bad because you are now not behaving like a native PowerShell cmdlet. Also, provides no real value, because if $ConfirmPreference = 'Low' then you will be prompted twice. This leads me to my next best practice, ok, well more of a general guideline.

Just because you can do something doesn't mean you should.

Also, if you do this, it makes it hard to know your cmdlet supports ShouldProcess/Confirm. By the way, I saw a tweet that asked how to know what cmdlets use SupportsShouldProcess, this is quick and dirty, but would not work for the above cmdlet as it
  1. Get-Command -CommandType Cmdlet -ParameterName Confirm  
Alright, so now we have seen some bad ways, now we are ready to see one (of many) good way to implement SupportsShouldProcess. I'm going to use my updated script from my previous post on the Scripting Games 2013 Advanced Event 1, but with native SupportsShouldProcess.

The benefit of implementing this is that you would be able to say Yes to All for the entire pipeline. As it stands now, you can say Yes to All for the New-Item cmdlet confirmation, and Yes to All for the Move-Item cmdlet as they are considered two separate pipelines within the cmdlet, so at a minimum two prompts. With these enhancements you would only be prompted once, answer Yes to All and would not be prompted again.

Note: I renamed the cmdlet/function to Move-File as others pointed out, nouns are never plural. Just something obvious I never realized, but glad to know.
  1. function Move-File {    
  2.     [CmdletBinding(ConfirmImpact = 'Medium', SupportsShouldProcess, DefaultParameterSetName='Days')]    
  3.     param (    
  4.         [Parameter(Position=0, Mandatory, ValueFromPipeline)]    
  5.         [SupportsWildcards()]    
  6.         [ValidateNotNullOrEmpty()]    
  7.         [string[]] $Path,    
  8.     
  9.         [Parameter(Position=1, Mandatory)]    
  10.         [ValidateNotNullOrEmpty()]    
  11.         [string] $Destination,    
  12.     
  13.         [Parameter(ParameterSetName='Days')]    
  14.         [PSDefaultValue(Help='90 Days')]    
  15.         [int] $Days = 90,    
  16.     
  17.         [Parameter(Mandatory, ParameterSetName='TimeSpan')]    
  18.         [TimeSpan] $TimeSpan = [TimeSpan]::FromDays($Days),    
  19.     
  20.         [Parameter(Mandatory, ParameterSetName='DateTime')]    
  21.         [DateTime] $DateTime = ((Get-Date) - $TimeSpan),    
  22.     
  23.         [ValidateSet('CreationTime''LastWriteTime''LastAccessTime')]    
  24.         [string] $FileProperty = 'CreationTime',    
  25.     
  26.         [switch$Force    
  27.     )    
  28.     
  29.     begin {    
  30.         Set-StrictMode -Version Latest    
  31.     
  32.         Write-Verbose "Files created before $DateTime will be moved to $Destination"    
  33.     }    
  34.     
  35.     process {    
  36.         $Files = Get-ChildItem -Path $Path -File | Where-Object $FileProperty -lt $DateTime    
  37.             
  38.         $Directories = $Files | Select-Object -ExpandProperty Directory -Unique |    
  39.                                     ForEach-Object { Join-Path -Path $Destination -ChildPath $_.Name } |     
  40.                                         Where-Object { !(Test-Path $_ -PathType Container) }   
  41.   
  42.         $Directories | ForEach-Object {  
  43.             if ($PSCmdlet.ShouldProcess($_'Create Directory')) {  
  44.                 New-Item -Path $_ -ItemType Container -Force -Confirm:$false | Out-Null  
  45.             }  
  46.         }  
  47.     
  48.         $Files | Select-Object -Property @{Name='Path';Expression={$_.FullName}},@{Name='Destination';Expression={Join-Path $Destination $_.Directory.Name}} | ForEach-Object {  
  49.             if ($PSCmdlet.ShouldProcess("Item: $($_.Path) Destination: $($_.Destination)"'Move File')) {  
  50.                 Move-Item -Path $_.Path -Destination $_.Destination -Force:$Force -Confirm:$false  
  51.             }  
  52.         }  
  53.     }    
  54. }   
SupportsShouldProcess with ConfirmImpact (Line 2)
Do you always need to include ConfirmImpact, I generally tend to not specify attributes or values when the default is the same value, but in this case, it helps to be explicit to the reader about the $ConfirmPreference required to receive the prompt without the explicit -Confirm parameter. And remember, PowerShell 3.0 simplified syntax no longer requires = $true on attribute parameters, just specify it and it is turned on.

$PSCmdlet.ShouldProcess (Line 43, 49)
These must be called, and should provide all the detail about the item, or in the case of line 49, items, that will be impacted by the operation and also give it a good operation name so the user knows what is happening.

Stop the Madness (Line 44, 50)
If you are going to handle the ShouldProcess prompting, make sure on the cmdlets you use, you pass the -Confirm:$false parameter to disable their ShouldProcess prompting so your user isn't double, triple, quadruple prompted and driven to blog about their loathing of your cmdlet because of a sloppy implementation. This behavior won't prevent -WhatIf from functioning properly, so you get the best of both worlds.

Conclusion
That really is all it takes, the Move-File cmdlet now can be answered Yes to All and no more prompts happening for the remainder of its pipeline processing.

I hope this has addressed the issues people are seeing and given a nice summary of how to implement SupportsShouldProcess in the future. Though I do admit, I am not a proficient implementer of SupportsShouldProcess, and I believe I understand it. But I also know there is so much more about PowerShell I need to learn, if Script Games event 1 has proven anything. So, if you have further insight about ShouldProcess and its behavior that would be valuable for me or the community, please share it!

Also, Event 2 has started for the 2013 Scripting Games and I hope it sparks as much conversation as the first event has so far.

Happy Scripting!

Note: I interchange the term cmdlet with function, as the design of functions are essentially the same as cmdlets though the backing code is different from a core PowerShell perspective anyway.

Comments

  1. I was involved in the discussion about ShouldProcess during the scripting games and I guess I missed this. Very good analysis and I appreciate the fully worked out and explained example. Bookmarking this page for later reference (as in the next time I need ShouldProcess).

    ReplyDelete

Post a Comment

Popular posts from this blog

FIM 2010 R2 Self-Service Password Reset Auto-Registration

I have been working on our FIM 2010 R2 SP1 lab environment looking for ways to simplify some of the overly complicated scenarios we had to implement to workaround the limitations in FIM 2010. One of those workarounds was the auto-registration of SSPR for new employees. When we onboard a new employee, we want to create a simple SSPR for them to get their first-time password reset.

Prior to the R2 release we were using the http://fim2010client.codeplex.com/ client and PowerShell to complete the registration process on a daily schedule. I was working on converting this to use the R2 registration cmdlets and combined it with PowerShell 3.0 Workflows to get to a solution similar to this.
[CmdletBinding()]  param()  begin {      workflow Invoke-RegisterSSPR {          InlineScript {              Import-Module FIM          }  $workflow = InlineScript { Get-FIMResource -Filter '/WorkflowDefinition[DisplayName = "Password Reset AuthN Workflow for New Employees"]' }  $members  …

PowerShell Error Handling Behavior Debunked

Note: I am using simple error messages as an example, please reference the best practices and guidelines I outlined on when to use custom error messages.

I have been churning in my mind for the last few days all the entries in the 2013 Scripting Games and how they handle errors, or lack thereof.

I am coming to the conclusion through some testing that the simple fact of seeing a try..catch or throw statements does not mean there is proper error handling.

I've been testing several variations and forms of error handling, so lets start with the basics.
function Test-WriteError {      [CmdletBinding()] param()  "Test-WriteError::ErrorActionPreference = $ErrorActionPreference"Move-Item -Path 'C:\Does\Not\Exists.log' -Destination 'C:\No\Where'"Test-WriteError::End"}   Test-WriteError::ErrorActionPreference = Continue
Move-Item : Cannot find path 'C:\Does\Not\Exists.log' because it does not exist.
At line:6 char:5
+     Move-Item -Path 'C:\Does\N…