PowerShell functions are very robust with several features that greatly improves the way users interact with them. One important feature that is often overlooked is -WhatIf
and -Confirm
support and it is easy to add to your functions. In this article, we will dive deep into how to implement this feature.
This is a really easy feature that you can enable in your functions that provides a safety net for your users that need it. There is nothing scarier than running a command that you know can be dangerous for the first time. The option to run it with -WhatIf
can make a big difference.
Index
- Index
- CommonParameters
- Using -WhatIf
- Using -Confirm
- SupportsShouldProcess
- $PSCmdlet.ShouldProcess
- ConfirmImpact
- $PSCmdlet.ShouldContinue
- Implementing -Force
- Scope issues
- In closing
CommonParameters
Before we look at implementing these common parameters, I want to take a quick look at how they are used.
Using -WhatIf
When a command supports the -WhatIf
parameter, it allows you to see what the command would have done instead of making changes. It is a good way to test out the impact of a command, especially before you do something destructive.
PS C:\temp> Remove-Item -Path .\myfile1.txt -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".
If the commands correctly implements ShouldProcess
, you should be able to see all the changes that it would have made. Here is an example where using a wildcard would delete multiple files.
PS C:\temp> Remove-Item -Path * -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".
What if: Performing the operation "Remove File" on target "C:\Temp\myfile2.txt".
What if: Performing the operation "Remove File" on target "C:\Temp\importantfile.txt".
Using -Confirm
Commands that support -WhatIf
also support -Confirm
. This gives you a chance confirm an action before performing it.
PS C:\temp> Remove-Item .\myfile1.txt -Confirm
Confirm
Are you sure you want to perform this action?
Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"):
In this case, you are given multiple options that allow you to continue, skip a change, or stop the script. The help prompt will describe each of those options like this.
Y - Continue with only the next step of the operation.
A - Continue with all the steps of the operation.
N - Skip this operation and proceed with the next operation.
L - Skip this operation and all subsequent operations.
S - Pause the current pipeline and return to the command prompt. Type "exit" to resume the pipeline.
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"):
Localization
This prompt is localized in PowerShell so the language will change based on the language of your operating system. This is just one more thing that PowerShell manages for you so you don’t have to.
Switch parameters
Let’s take quick moment to look at ways to pass a value to a switch parameter. The main reason I call this out is that you will often want to pass parameter values to functions you call.
The first approach is a specific parameter syntax that can be used for all parameters but you mostly see it used for switch parameters. You specify a colon to attach a value to the parameter.
Remove-Item -Path:* -WhatIf:$true
You can do the same with a variable.
$DoWhatIf = $true
Remove-Item -Path * -WhatIf:$DoWhatIf
The second approach is to use a hashtable to splat the value.
$RemoveSplat = @{
Path = '*'
WhatIf = $true
}
Remove-Item @RemoveSplat
If you are new to hashtables or splatting, I have another article on that covers everything you wanted to know about hashtables.
SupportsShouldProcess
The first step to enable -WhatIf
and -Confirm
support is to specify SupportsShouldProcess
in the CmdletBinding
of your function.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
Remove-Item .\myfile1.txt
}
By specifying SupportsShouldProcess
in this way, we can now call our function with -WhatIf
(or -Confirm
).
PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".
Notice that I did not create a parameter called -WhatIf
. Specifying SupportsShouldProcess
automatically creates it for us. When we specify the -WhatIf
parameter on Test-ShouldProcess
, some things we call will also perform -WhatIf
processing.
Trust but verify
There is some danger here trusting everything you call will inherit -WhatIf
values. For the rest of the examples, I am going to assume that it does not work and be very explicit when making calls to other commands. I recommend that you do the same.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
Remove-Item .\myfile1.txt -WhatIf:$WhatIf
}
I will revisit the nuances much later once you have a better understanding of all the pieces in play.
$PSCmdlet.ShouldProcess
The method that allows you to implement SupportsShouldProcess
is $PSCmdlet.ShouldProcess
. You call $PSCmdlet.ShouldProcess(...)
to see if you should process some logic and PowerShell takes care of the rest. Let’s start with an example:
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
$file = Get-ChildItem './myfile1.txt'
if($PSCmdlet.ShouldProcess($file.Name)){
$file.Delete()
}
}
The call to $PSCmdlet.ShouldProcess($file.name)
checks for the -WhatIf
(and -Confirm
parameter) then handles it accordingly. The -WhatIf
will cause ShouldProcess
to output a description of the change and return $false
:
PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".
A call using -Confirm
will pause the script and prompt the user with the option to continue. It will return $true
if the user selected Y
.
PS> Test-ShouldProcess -Confirm
Confirm
Are you sure you want to perform this action?
Performing the operation "Test-ShouldProcess" on target "myfile1.txt".
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"):
An awesome feature of $PSCmdlet.ShouldProcess
is that it doubles as verbose output. I depend on this often when implementing ShouldProcess
.
PS> Test-ShouldProcess -Verbose
VERBOSE: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".
Overloads
There are a few different overloads for $PSCmdlet.ShouldProcess
with different parameters for customizing the messaging. We already saw the first one in the example above. Let’s take a closer look at it.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
if($PSCmdlet.ShouldProcess('TARGET')){
# ...
}
}
This produces output that includes both the function name and the target (value of the parameter).
What if: Performing the operation "Test-ShouldProcess" on target "TARGET".
Specifying a second parameter as the operation will use the operation value instead of the function name in the message.
# $PSCmdlet.ShouldProcess('TARGET','OPERATION')
What if: Performing the operation "OPERATION" on target "TARGET".
The next option is to specify three parameters to fully customize the message. When three parameters are used, the first one is the entire message. The second two parameters are still used in the -Confirm
message output.
# $PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION')
What if: MESSAGE
Quick parameter reference
Just in case you came here only to figure out what parameters you should use, here is a quick reference showing how the parameters change the message in the different -WhatIf
scenarios.
# $PSCmdlet.ShouldProcess('TARGET')
What if: Performing the operation "FUNCTION_NAME" on target "TARGET".
# $PSCmdlet.ShouldProcess('TARGET','OPERATION')
What if: Performing the operation "OPERATION" on target "TARGET".
# $PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION')
What if: MESSAGE
I tend to use the one with 2 parameters.
ShouldProcessReason
We have a fourth overload thats more advanced than the others that allows you to get the reason ShouldProcess
was executed. I am only adding this here for completeness because we can just check if $WhatIf
is $true
instead.
$reason = ''
if($PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION',[ref]$reason)){
Write-Output "Some Action"
}
$reason
We have to pass the $reason
variable into the 4th parameter as a reference variable with [ref]
and ShouldProcess
will populate $reason
with the value None
or WhatIf
. I didn’t say this was very useful and I have no reason to ever use it.
Where to place it
You use ShouldProcess
to make your scripts safer. So you use it when your scripts are making changes. I like to place the $PSCmdlet.ShouldProcess
call as close to the change as possible.
# general logic and variable work
if ($PSCmdlet.ShouldProcess('TARGET','OPERATION')){
# Change goes here
}
If I am processing a collection of items, I will call it for each item. So the call gets placed inside the foreach loop.
foreach ($node in $collection){
# general logic and variable work
if ($PSCmdlet.ShouldProcess($node,'OPERATION')){
# Change goes here
}
}
The reason why I place ShouldProcess
tightly around the change, is that I want as much code to execute as possible when -WhatIf
is specified. I want the setup and validation to run if possible so the user gets to see those errors.
I also like to use this in my Pester tests that validate my projects. If I have a piece of logic that is hard to mock in pester, I can often wrap it in ShoudProcess
and call it with -WhatIf
in my tests. It’s better to test some of your code than none of it.
$WhatIfPreference
The first preference variable we have is $WhatIfPreference
. This is $false
by default. If you set it to $true
then your function will execute as if you specified -WhatIf
. If you set this in your session, all commands will perform -WhatIf
execution.
When you call a function with -WhatIf
, the value of $WhatIfPreference
gets set to $true
inside the scope of your function.
ConfirmImpact
Most of my examples are for -WhatIf
but everything so far also works with -Confirm
to prompt the user. You can set the ConfirmImpact
of the function to high and it will auto prompt the user as if it was called with -Confirm
.
function Test-ShouldProcess {
[CmdletBinding(
SupportsShouldProcess,
ConfirmImpact = 'High'
)]
param()
if ($PSCmdlet.ShouldProcess('TARGET')){
Write-Output "Some Action"
}
}
This call to Test-ShouldProcess
is performing the -Confirm
action because of the High
impact.
PS> Test-ShouldProcess
Confirm
Are you sure you want to perform this action?
Performing the operation "Test-ShouldProcess" on target "TARGET".
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): y
Some Action
The obvious issue is that now it’s harder to use in other scripts without prompting the user. In this case, we can pass a $false
to -Confirm
to suppress the prompt.
PS> Test-ShouldProcess -Confirm:$false
Some Action
I’ll cover how to add -Force
support in a later section.
$ConfirmPreference
$ConfirmPreference
is an automatic variable that controls when ConfirmImpact
will ask you to confirm execution. Here are the possible values for both $ConfirmPreference
and ConfirmImpact
.
High
Medium
Low
None
With these values, you can specify different levels of impact for each function. If you have $ConfirmPreference
set to a value higher than ConfirmImpact
, then you will not be prompted to confirm execution.
By default, $ConfirmPreference
is set to High
and ConfirmImpact
is Medium
. If you want your function to automatically prompt the user, set your ConfirmImpact
to High
. Otherwise set it to Medium
if its destructive and use Low
if the command is always safe run in production. If you set it to none
, it will not prompt even if -Confirm
was specified (but it will still give you -WhatIf
support).
When calling a function with -Confirm
, the value of $ConfirmPreference
gets set to Low
inside the scope of your function.
Suppressing nested confirm prompts
The $ConfirmPreference
can get picked up by functions that you call. This can create scenarios where you add a confirm prompt and the function you call will also prompt the user.
What I tend to do is specify -Confirm:$false
on the commands that I call when I have already handled the prompting.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param()
$file = Get-ChildItem './myfile1.txt'
if($PSCmdlet.ShouldProcess($file.Name)){
Remove-Item -Path $file.fullname -Confirm:$false
}
}
This brings us back to an earlier warning: There are nuances as to when -WhatIf
will not get passed to a function and when -Confirm
will pass to a function. I promise I’ll get back to this later.
$PSCmdlet.ShouldContinue
If you need more control than ShouldProcess
provides, you can trigger the prompt directly with ShouldContinue
. ShouldContinue
ignores $ConfirmPreference
, ConfirmImpact
, -Confirm
, $WhatIfPreference
, and -WhatIf
because it will prompt every time it is executed.
At a quick glance, it is easy to confuse ShouldProcess
and ShouldContinue
. I tend to remember to use ShouldProcess
because the parameter is called SupportsShouldProcess
in the CmdletBinding
. You should use ShouldProcess
in almost every scenario. That is why I covered that method first.
Let’s take a look at ShouldContinue
in action.
function Test-ShouldContinue {
[CmdletBinding()]
param()
if($PSCmdlet.ShouldContinue('TARGET','OPERATION')){
Write-Output "Some Action"
}
}
This will provide us a more basic prompt with fewer options.
Test-ShouldContinue
Second
TARGET
[Y] Yes [N] No [S] Suspend [?] Help (default is "Y"):
The biggest issue with ShouldContinue
is that it requires the user to run it interactively because it will alway prompt the user. You should always be building tools that can be used by other scripts. The way you do this is by implementing -Force
. I’ll revisit this idea later.
Yes to all
This is automatically handled with ShouldProcess
but we have to do a little more work for ShouldContinue
. There is a second method overload where we have to pass in a few values by reference to control the logic.
function Test-ShouldContinue {
[CmdletBinding()]
param()
$collection = 1..5
$yesToAll = $false
$noToAll = $false
foreach($target in $collection) {
$continue = $PSCmdlet.ShouldContinue(
"TARGET_$target",
'OPERATION',
[ref]$yesToAll,
[ref]$noToAll
)
if ($continue){
Write-Output "Some Action [$target]"
}
}
}
I added a foreach
loop and a collection to show it in action. I pulled the ShouldContinue
call out of the if
statement to make it easier to read. Calling a method with 4 parameters starts to get a little ugly, but I tried to make it look as clean as I could.
Implementing -Force
ShouldProcess
and ShouldContinue
need to implement -Force
in very different ways. The trick to these implementations is that ShouldProcess
should always get executed, but ShouldContinue
should not get executed if -Force
is specified.
ShouldProcess -Force
If you set your ConfirmImpact
to high
, the first thing your users are going to try is to suppress it with -Force
. Thats the first thing I do anyway.
Test-ShouldProcess -Force
Error: Test-ShouldProcess: A parameter cannot be found that matches parameter name 'force'.
If you recall from the ConfirmImpact
section, they actually need to call it like this:
Test-ShouldProcess -Confirm:$false
Not everyone realizes they need to do that and -Confirm:$false
will not work for suppressing ShouldContinue
. So we should implement -Force
for the sanity of our users. Take a look at this full example here:
function Test-ShouldProcess {
[CmdletBinding(
SupportsShouldProcess,
ConfirmImpact = 'High'
)]
param(
[Switch]$Force
)
if ($Force -and -not $Confirm){
$ConfirmPreference = 'None'
}
if ($PSCmdlet.ShouldProcess('TARGET')){
Write-Output "Some Action"
}
}
We add our own -Force
switch as a parameter and we will use the $Confirm
automatic parameter that is available when adding SupportsShouldProcess
in the CmdletBinding
.
[CmdletBinding(
SupportsShouldProcess,
ConfirmImpact = 'High'
)]
param(
[Switch]$Force
)
Focusing in on the -Force
logic here:
if ($Force -and -not $Confirm){
$ConfirmPreference = 'None'
}
If the user specifies -Force
, we want to suppress the confirm prompt unless they also specify -Confirm
. This allows a user to force a change but still confirm the change. Then we set $ConfirmPreference
in the local scope where our call to ShouldProcess
discoverers it.
if ($PSCmdlet.ShouldProcess('TARGET')){
Write-Output "Some Action"
}
If someone specifies both -Force
and -WhatIf
, then -WhatIf
needs to take priority. This approach preserves -WhatIf
processing because ShouldProcess
will always get executed.
Do not add a check for the $Force
value inside the if statement with the ShouldProcess
. That is an antipattern for this specific scenario even though thats what I will show you in the next section for ShouldContinue
.
ShouldContinue -Force
This is the correct way to implement -Force
with ShouldContinue
.
function Test-ShouldContinue {
[CmdletBinding()]
param(
[Switch]$Force
)
if($Force -or $PSCmdlet.ShouldContinue('TARGET','OPERATION')){
Write-Output "Some Action"
}
}
By placing the $Force
to the left of the -or
operator, it will get evaluated first. Writing it this way short circuits the execution of the if statement. If $force
is $true
, then the ShouldContinue
will not get executed.
PS> Test-ShouldContinue -Force
Some Action
We don’t have to worry about -Confirm
or -WhatIf
in this scenario because they are not supported by ShouldContinue
. This is why it needs to be handled differently than ShouldProcess
.
Scope issues
Using -WhatIf
and -Confirm
are supposed to apply to everything inside your functions and everything they call. They do this by setting $WhatIfPreference
to $true
or setting $ConfirmPreference
to Low
in the local scope of the function. When you call another function, calls to ShouldProcess
will use those values.
This actually works correctly most of the time. Any time you call builtin Cmdlet or a function in your same scope, it will work. It also works when you call a script or a function in a script module from the console.
The one very specific place where it does not work is when a script or a script module calls a function in another script module. This may not sound like a big problem, but most of the modules you will create or pull from the PSGallery are script modules.
The core issue is that script modules do not inherit the values for $WhatIfPreference
or $ConfirmPreference
(and several others) when called from functions in other script modules.
The best way to summarize this as a general rule is that this works correctly for binary modules and never trust it to work for script modules. If you are not sure, either test it or just assume it does not work correctly.
I personally feel this is very dangerous because it creates scenarios where you add -WhatIf
support to multiple modules that work correctly in isolation, but fail to work correctly when they call each other.
We do have a Github RFC (Propagate execution preferences beyond script module scope) working to get this issue fixed and it goes into more details.
In closing
I have to look up how to use ShouldProcess
every time I need to use it. It took me a long time to be able to distinguish ShouldProcess
from ShouldContinue
and I almost always need to look up what parameters to use. So don’t worry if you still get confused from time to time. This article will be here when you need it. I’m sure I will reference it often myself.
If you liked this post, please share your thoughts with me on Twitter using the link below. I always like hearing from people that get value from my content.