Adding -WhatIf Support to your scripts the right way, and how you shouldn't do it
If you are a prudent sysadmin or consultant, you probably rely on using the -WhatIf command before ever running any serious PowerShell cmdlets in your environment. Since we love it so much, shouldnāt we build this very same courtesy into our tools that others will use?
Before I learned how easy it is to add -WhatIf support to my Functions and Scripts, Iād often write something ugly like this:
Function Do-Stuff{
param([switch]$WhatIf)
ForEach($item in $args){
if ($WhatIf){
Write-host "WhatIf: Would be performing \`$Action on $item" -ForegroundColor Cyan
}
ELSE {
"Actually performing \`$Action on $item"
}
}
}
Aside: This is amended from an actual script in production that Iāve created. It was made a LONG time ago.
Yep, itās not very nice and Iām deeply, deeply ashamed.
I mean, it worked:
Itās almost an example of what not to do! This little snippet has been simplified to be as easy to read as it gets, the thing would get VERY nasty when working with a complex script, as Iād pretty much have to write all of my code twice. Once to actually do the thing, and another to describe what I was doing in this pseudo-WhatIf message.
It wasnāt uncommon to see this sort of thing over and over and over. Every single decision tree in my scripts would be made an extra six or seven lines longer because of this forking. Ā In fact, people often called me something that sounded just like āThat Forking PowerShell guyā. Ā :(
BUT NO MORE!
Changing this little sucker to work the right way is easy, and only requires a few edits.
First off, weāre going to delete the [switch], and weāre going to add in a proper array/parameter to capture our input. Weāll also change line 5 from $args to $Objects, to match.
Function Do-Stuff{
param([string[]]$Objects)
ForEach($item in $Objects){
if ($WhatIf){
Write-host "WhatIf: Would be performing \`$Action on $item" -ForegroundColor Cyan }
ELSE {
"Actually performing \`$Action on $item"
}
}
}
We have to dump the Ā use of $args
because weāll be telling PowerShell to treat our code as a proper cmdlet, which means we canāt use catchalls like $args anymore.
Next, weāll add in the magic sauce:
[CmdletBinding(SupportsShouldProcess=$true)]
. Add this little guy right after the Function Declaration like so:
Function Do-Stuff{ [CmdletBinding(SupportsShouldProcess=$true)] param([string[]]$Objects)
Finally, weāll gut the existing decision structure, and replace it with this:
ForEach($item in $Objects){
if ($pscmdlet.ShouldProcess("$item", "DoStuff")){
"Actually performing \`$Action on $item"
}
}
In line 2 above, weāre specifying the syntax to be displayed to the user. In this case, if the user were to run
Do-Stuff -Objects "hello","sweetie" -WhatIf
it would say:
What if: Performing the operation "DoStuff" on target "hello". What if: Performing the operation "DoStuff" on target "sweetie".
You also use this same structure to provide messages to the user which are displayed with Warning and ConfirmImpact, but Iāll get into those examples in a later date.
āBut Stephen/FoxDeploy/PowerShell guyā, you ask, voice raising up a plaintive octaveā what IS all of this $pscmdlet stuff anyway? You never defined $pscmdlet anywhere in that code!ā
Iām happy you asked. $pscmdlet
is the guts of what is letting us do a -WhatIf so easily. Well, this and [cmdletbinding]
.
In simple terms, what weāre doing here is replacing our janky $WhatIf switch for proper -WhatIf support, which is provided to us via the $pscmdlet, a special PowerShell variable created at runtime, and that objectās method .ShouldProcess(). We donāt need to define or mess with $pscmdlet at all, it is provided to our function to use during execution because we told PowerShell to treat this like real compiled code (think the built-in cmdlets).
See, adding that [CmdletBinding] declaration to our function told PowerShell that we wanted to treat this code as a default cmdlet, which exposes a lot of additional functionality. For instance, adding CmdletBinding lets us specify all sorts of additional tools our script can use, from ConfirmImpact to Paging, to WhatIf support. You can read more about CmdletBinding in the built in help system.
Get-help about_Functions_CmdletBindingAttribute
Some of this stuff is hard to approach, which is why a deep-dive into what Cmdlet Binding is and what it offers is coming in a future post.
As for $PSCmdlet itself, weāre essentially peering under the covers into the PowerShell runspace here with this variable. Itās a bit confusing, but for now, know that you canāt use $pscmdlet.ShouldProcess() without adding [CmdletBinding] to your code.
Here is the completed sample of how to use -WhatIf
Function Do-Stuff{
[CmdletBinding(SupportsShouldProcess=$true)] param([string[]]$Objects)
ForEach($item in $Objects){
if ($pscmdlet.ShouldProcess("$item", "DoStuff")){
"Actually performing `$Action on $item"
}
}
}
And the final result:
I hope you liked this shallow dive into how-to -WhatIf and how not to do it!