28

I have a PowerShell script cmdlet that supports the -WhatIf & -Confirm parameters.

It does this by calling the $PSCmdlet.ShouldProcess() method before performing the change.
This works as expected.

The problem I have is that my Cmdlet is implemented by calling other Cmdlets and the -WhatIf or -Confirm parameters are not passed along to the Cmdlets I invoke.

How can I pass along the values of -WhatIf and -Confirm to the Cmdlets I call from my Cmdlet?

For example, if my Cmdlet is Stop-CompanyXyzServices and it uses Stop-Service to implement its action.

If -WhatIf is passed to Stop-CompanyXyzServices I want it to also be passed to Stop-Service.

Is this possible?

Rob Kielty
  • 7,958
  • 8
  • 39
  • 51
Dan Finucane
  • 1,547
  • 2
  • 18
  • 27

5 Answers5

19

Passing parameters explicitly

You can pass the -WhatIf and -Confirm parameters with the $WhatIfPreference and $ConfirmPreference variables. The following example achieves this with parameter splatting:

if($ConfirmPreference -eq 'Low') {$conf = @{Confirm = $true}}

StopService MyService -WhatIf:([bool]$WhatIfPreference.IsPresent) @conf

$WhatIfPreference.IsPresent will be True if the -WhatIf switch is used on the containing function. Using the -Confirm switch on the containing function temporarily sets $ConfirmPreference to low.

Passing parameters implicitly

Since the -Confirm and -WhatIf temporarily set the $ConfirmPreference and $WhatIfPreference variables automatically, is it even necessary to pass them?

Consider the example:

function ShouldTestCallee {
    [cmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')] 
    param($test)

    $PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Confirm?")
}


function ShouldTestCaller {
    [cmdletBinding(SupportsShouldProcess=$true)]
    param($test)

    ShouldTestCallee
}

$ConfirmPreference = 'High'
ShouldTestCaller
ShouldTestCaller -Confirm

ShouldTestCaller results in True from ShouldProcess()

ShouldTestCaller -Confirm results in an confirm prompt even though I didn't pass the switch.

Edit

@manojlds answer made me realize that my solution was always setting $ConfirmPreference to 'Low' or 'High'. I have updated my code to only set the -Confirm switch if the confirm preference is 'Low'.

Charlie Joynt
  • 4,411
  • 1
  • 24
  • 46
Rynant
  • 23,153
  • 5
  • 57
  • 71
  • +1 for `$WhatIfPreference.IsPresent` . Not really sure if this can be used into a solution that the OP wants. – manojlds Aug 25 '11 at 15:29
  • 1
    Used this to get a full solution. See my answer. Didn't know about $WhatIfPreference before, nice to know. – manojlds Aug 25 '11 at 16:37
  • @Dan said he already had a script that supports the `-WhatIf` and `-Confirm` parameters. I didn't think I needed to write the other code as well. – Rynant Aug 25 '11 at 18:03
  • 1
    `-Confirm` and `-WhatIf` will get inherited by commands you call. – JasonMArcher Aug 31 '11 at 20:21
  • 1
    Good answer, but these edits made the conclusion confusing and difficult to grasp. So, what's the correct way: `$ConfirmPreference.IsPresent` or rely on the passthrough mechanism of the preference variables? – alecov Aug 10 '13 at 17:34
  • @Alek, personally I prefer the pass-through mechanism (implicit passing of `-WhatIf` or `-Confirm`) to function and cmdlets. You do need to be aware of whether those switches are supported by each and/or wrap the the call to them with `$PSCmdlet.ShouldProcess()`. – Charlie Joynt Jan 17 '17 at 12:14
7

After some googling I came up with a good solution for passing common parameters along to called commands. You can use the @ splatting operator to pass along all the parameters that were passed to your command. For example, if

Start-Service -Name ServiceAbc @PSBoundParameters

is in the body of your script powershell will pass all the parameters that were passed to your script to the Start-Service command. The only problem is that if your script contains say a -Name parameter it will be passed too and PowerShell will complain that you included the -Name parameter twice. I wrote the following function to copy all the common parameters to a new dictionary and then I splat that.

function Select-BoundCommonParameters
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        $BoundParameters
    )
    begin
    {
        $boundCommonParameters = New-Object -TypeName 'System.Collections.Generic.Dictionary[string, [Object]]'
    }
    process
    {
        $BoundParameters.GetEnumerator() |
            Where-Object { $_.Key -match 'Debug|ErrorAction|ErrorVariable|WarningAction|WarningVariable|Verbose' } |
            ForEach-Object { $boundCommonParameters.Add($_.Key, $_.Value) }

        $boundCommonParameters
    }
}

The end result is you pass parameters like -Verbose along to the commands called in your script and they honor the callers intention.

Dan Finucane
  • 1,547
  • 2
  • 18
  • 27
  • I had some trouble with this when I slotted it into some existing code - PSBoundParameters is actually of type System.Management.Automation.PSBoundParametersDictionary. I worked around this by building up an array of non-matching keys in an array and then calling $BoundParameters.Remove() to strip off the non-command parameters and then returning the modified PSBoundParameters in an end{} block. – Stephen Connolly Oct 03 '14 at 12:42
3

Here is a complete solution based on @Rynant and @Shay Levy's answers:

function Stop-CompanyXyzServices
{
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')]

    Param(
        [Parameter(
            Position=0,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true
        )]      
        [string]$Name
    )

    process
    {
        if($PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Stop XYZ services '$Name'")){  
            ActualCmdletProcess
        }
        if([bool]$WhatIfPreference.IsPresent){
            ActualCmdletProcess
        }
    }
}

function ActualCmdletProcess{
# add here the actual logic of your cmdlet, and any call to other cmdlets
Stop-Service $name -WhatIf:([bool]$WhatIfPreference.IsPresent) -Confirm:("Low","Medium" -contains $ConfirmPreference)
}

We have to see if -WhatIf is passed separately as well so that the whatif can be passed on to the individual cmdlets. ActualCmdletProcess is basically a refactoring so that you don't call the same set of commands again just for the WhatIf. Hope this helps someone.

manojlds
  • 290,304
  • 63
  • 469
  • 417
  • I don't think you should be checking `"Low","Medium" -contains $ConfirmPreference`. Setting `-Confirm:$true` sets `$ConfirmPreference` to 'low', but your code will set `$ConfirmPreference` to 'Low' if it is 'Medium'. But I'm wondering if `-Confirm` and `-WhatIF` even need to be passed; see my edit. – Rynant Aug 25 '11 at 17:57
  • @Rynant - They have to be so that it is done for each individual cmdlet AND for the custom cmdlet we are writing. So there will be multiple confirm. – manojlds Aug 25 '11 at 18:11
  • Did you try my example? The function `ShouldTestCallee` confirms depending on whether or not `-Confirm` is used on `ShouldTestCaller` even though I don't pass the confirm parameter. – Rynant Aug 25 '11 at 18:24
1

Updated per @manojlds comment

Cast $WhatIf and $Confirm to Boolean and pass the values to the the underlying cmdlet:

function Stop-CompanyXyzServices
{
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')]

    Param(
        [Parameter(
            Position=0,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true
        )]      
        [string]$Name
    )


    process
    {
        if($PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Stop service '$Name'"))
        {                   
            Stop-Service $name -WhatIf:([bool]$WhatIf) -Confirm:([bool]$confirm)
        }                       
    }
}
Shay Levy
  • 121,444
  • 32
  • 184
  • 206
  • Are you sure this works? In cmdlet, I don't think so. And even the whatif output shows only for the custom cmdlet, which the OP already gets from the `ShouldProcess` – manojlds Aug 25 '11 at 13:11
  • Sorry, doesn't work. This was my initial solution as well, so I was hoping it would. Two issues - $WhatIf is not set even when you do `-WhatIf`. @Rynant's suggestion of `$WhatIfPreference.IsPresent` does work though. Also, since you are doing this within the if check of `ShouldProcess`, if it comes inside, `-WhatIf` will not be set anyway. – manojlds Aug 25 '11 at 15:27
  • Not sure why It doesn't work for you. If I call the function without WhatIf or confirm, I get a confirmation, I press enter and the service stops. If I call the function with WhatIf, I get a whatif text. If I call it with Confirm, I get a confirmation, press enter, the service stops. – Shay Levy Aug 25 '11 at 17:30
  • Hey, it works THAT way, but the OP is asking for confirmation and whatif for individual cmdlets. See my answer if it makes what I mean and what I think OP wants clear. Bascially, when i give whatif, it should give whatif from our cmdlet, then whatif from each of the cmdlet being used. – manojlds Aug 25 '11 at 18:27
0

Just so you wont get run around the block for hours by this question and the answers here, I would suggest that you read this article instead:

https://powershellexplained.com/2020-03-15-Powershell-shouldprocess-whatif-confirm-shouldcontinue-everything/#suppressing-nested-confirm-prompts

The answers presented here does not work for many cases and I see a danger in people implementing the answers here, without understanding the fundamentals.

Here is how a hacked it to work across scriptmodules: enter image description here

Casper Leon Nielsen
  • 2,528
  • 1
  • 28
  • 37