I'm writing a Powershell cmdlet that supports ShouldProcess
. Instead of having a fixed ConfirmImpact
value, I'd like a 'dynamic' value that depends upon the value of a parameter passed to the cmdlet. Let me illustrate with an example.
Let's pretend I'm a web hosting provider. I have many websites and each website belongs to one of the following categories, ordered by importance: Production
, Test
and Development
. As part of my hosting management, I have a Remove-WebSite
cmdlet for destroying websites. The following code illustrates this:
Class WebSite {
[string] $Name
[string] $Category # Can be one of: Production, Test, Development
}
Function Remove-WebSite {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[WebSite] $WebSite
)
Write-Host "$($WebSite.Name) was destroyed"
}
At the moment websites are destroyed without confirmation. While this is convenient, too many interns have been destroying production sites by mistake so I'd like a bit more of a safety net on the Remove-WebSite
cmdlet by taking advantage of the ShouldProcess feature of Powershell.
So I add the SupportsShouldProcess
and ConfirmImpact
values to the CmdletBinding
attribute. My cmdlet definition becomes:
Function Remove-WebSite {
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')]
Param(
[Parameter(Mandatory=$true)]
[WebSite] $WebSite
)
if ($PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)")) {
Write-Host "$($WebSite.Name) was destroyed"
}
}
With this definition, anyone calling the Remote-Website
cmdlet is now asked to confirm that they really want to destroy the site. Hardly any production sites are being destroyed by mistake now, except the web developers are complaining that their automated scripts have stopped working.
What I'd really like is that the ConfirmImpact
value for the cmdlet to vary at runtime depending on the importance of the category of the web site - High
for production sites, Medium
for test sites and Low
for development sites. The following function definition illustrates this:
Function CategoryToImpact([string]$Category) {
Switch ($Category) {
'Production' {
[System.Management.Automation.ConfirmImpact]::High
break
}
'Test' {
[System.Management.Automation.ConfirmImpact]::Medium
break
}
'Development' {
[System.Management.Automation.ConfirmImpact]::Low
break
}
default {
[System.Management.Automation.ConfirmImpact]::None
break
}
}
}
Function Remove-WebSite {
[CmdletBinding(SupportsShouldProcess=$true<#,ConfirmImpact="Depends!"#>)]
Param(
[Parameter(Mandatory=$true)]
[WebSite] $WebSite
)
# This doesn't work but I hope it illustrates what I'd *like* to do
#$PSCmdLet.ConfirmImpact = CategoryToImpact($WebSite.Category)
if ($PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)")) {
Write-Host "$($WebSite.Name) was destroyed"
}
}
Assuming it's possible, how can this be done?
Here's a paste a the complete script plus some test code: http://pastebin.com/kuk6HNm6
This isn't exactly what you're asking for (which I think is strictly speaking, impossible), but it might be a better approach.
Leave ConfirmImpact
alone and instead prompt the user with $PSCmdlet.ShouldContinue()
.
According to the guidance given in Requesting Confirmation from Cmdlets (emphasis mine):
For most cmdlets, you do not have to explicitly specify ConfirmImpact. Instead, use the default setting of the parameter, which is Medium. If you set ConfirmImpact to High, the operation will be confirmed by default. Reserve this setting for highly disruptive actions, such as reformatting a hard-disk volume.
Further:
Most cmdlets request confirmation using only the ShouldProcess method. However, some cases might require additional confirmation. For these cases, supplement the ShouldProcess call with a call to the ShouldContinue method.
...
If a cmdlet calls the ShouldContinue method, the cmdlet must also provide a Force switch parameter. If the user specifies Force when the user invokes the cmdlet, the cmdlet should still call ShouldProcess, but it should bypass the call to ShouldContinue.
Given this guidance, I propose the following changes:
Function Remove-WebSite {
[CmdletBinding(SupportsShouldProcess=$true)]
Param(
[Parameter(Mandatory=$true)]
[WebSite] $WebSite ,
[Switch] $Force
)
if ($PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)")) {
$destroy =
$Force -or
$WebSite.Category -ne 'Production' -or
$PSCmdlet.ShouldContinue("Are you sure you want to destroy $($WebSite.Name)?", "Really destroy this?")
if ($destroy) {
Write-Host "$($WebSite.Name) was destroyed"
}
}
}
The simplest solution is to remove the $PSCmdlet.ShouldProcess
method call and conditionally call the $PSCmdlet.ShouldContinue
method according to our own criteria. The problem with this is that we loose -WhatIf
functionality. As briantist points out, $PSCmdlet.ShouldContinue
should be used along side $PSCmdlet.ShouldProcess
, except this can result in superfluous confirm prompts i.e. the user is prompt twice when once would have sufficed.
With experimentation, I've found that by setting ConfirmImpact='None'
in the CmdletBinding
attribute declaration, ShouldProcess
no longer displays a prompt, but still returns $false
if -WhatIf
is specified. As a result ShouldProcess
and ShouldContinue
can be both called and still only have a single prompt displayed to the user. I can then use my own logic to determine whether to call ShouldContinue
or not.
Here's a complete solution:
# Represents a website
Class WebSite {
# The name of the web site
[string] $Name
# The category of the website, which can be one of: Production, Test, Development
[string] $Category # Can be one of
<#
Gets the ConfirmImpact level based on Category, as follows:
Category ConfirmImpact
----------- -------------
Production High
Test Medium
Development Low
Default None
#>
[System.Management.Automation.ConfirmImpact] GetImpact() {
Switch ($this.Category) {
'Production' {
return [System.Management.Automation.ConfirmImpact]::High
}
'Test' {
return [System.Management.Automation.ConfirmImpact]::Medium
}
'Development' {
return [System.Management.Automation.ConfirmImpact]::Low
}
}
return [System.Management.Automation.ConfirmImpact]::None
}
# String representation of WebSite
[string] ToString() {
return "$($this.Category) site $($this.Name)"
}
}
<#
.SYNOPSIS
Destroys a WebSite
.DESCRIPTION
The Remove-WebSite cmdlet permanently destroys a website so use with care.
To avoid accidental deletion, the caller will be prompted to confirm the
invocation of the command if the value of $ConfirmPreference is less than
or equal to the Impact level of the WebSite. The Impact level is based
upon the category, as follows:
Category ConfirmImpact
----------- -------------
Production High
Test Medium
Development Low
Default None
.PARAMETER Website
The WebSite to destroy.
.PARAMETER Force
Destroys website without prompt
.PARAMETER Confirm
Require confirmation prompt always regardless of $ConfirmPreference
.PARAMETER WhatIf
Show what would happen if the cmdlet was run. The cmdlet is not run.
#>
Function Remove-WebSite {
# Set ConfirmImpact to 'None' so that ShouldProcess automatically returns
# true without asking for confirmation, regardless of the value of
# $ConfirmPreference.
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='None')]
Param(
[Parameter(Mandatory=$true)]
[WebSite] $WebSite,
[Switch] $Force
)
# Returns true without prompt unless -WhatIf is specified when, in which case
# false is returned without prompt
if ($PSCmdlet.ShouldProcess($WebSite)) {
# Determine whether to continue with the command. Only destroy website if...
$continue =
# ...forced to by Force parameter...
$Force -or
#...or the Impact level of the Website is less than $ConfirmPreference...
$WebSite.GetImpact() -lt $ConfirmPreference -or
#...or the user clicked 'Yes' in ShouldContinue prompt
$PSCmdlet.ShouldContinue("Are you sure you want to destroy $($WebSite)?", $null)
if ($continue) {
Write-Host "$($WebSite.Name) was destroyed"
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With