Home > OS >  PowerShell -Confirm only once per function call
PowerShell -Confirm only once per function call

Time:09-22

I'm obviously trying to do something wrong, so hoping someone can help since it seems easy and it is probably an easy mistake. Basically I am writing a little script to create new folders and I want to include -Confirm, but I want it to basically ask ONCE if the user wants to continue, instead of asking for each nested folder that is created.

Here's what I have so far

function Create-NewFolderStructure{
[CmdletBinding(SupportsShouldProcess)]
param (
    [Parameter(Mandatory)]
    [string]$NewFolderName,
    
    [Parameter()]
    $LiteralPath = (Get-Location),

    [Parameter()]
    $inputFile = "zChildDirectoryNames.txt"
)
process {
$here = $LiteralPath
$directories = Get-Content -LiteralPath "$($here)\$($inputFile)"
If (-not (Test-Path -Path "$($here)\$($NewFolderName)")){
    $directories | ForEach-Object {New-Item -ItemType "directory" -Path "$($here)\$($NewFolderName)" -Name $_}
}
Else {
    Write-Host "A $($NewFolderName) folder already exists"
}
}
end {
}
}

The issue is that if I use -Confirm when calling my function, or directly at New-Item - I end up getting the confirmation prompt for every single 'child folder' that is being created. Even with the first prompt, saying "Yes to all" doesn't suppress future prompts

CodePudding user response:

In order to consume the user's answer to the confirmation prompt, you have to actually ask them!

You can do so by calling $PSCmdlet.ShouldContinue():

function Create-NewFolderStructure {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$NewFolderName,
        
        [Parameter()]
        $LiteralPath = (Get-Location),
    
        [Parameter()]
        $inputFile = "zChildDirectoryNames.txt",

        [switch]$Force
    )

    process {
        if(-not $Force){
            $yesToAll = $false
            $noToAll = $false
            if($PSCmdlet.ShouldContinue("Do you want to go ahead and create new directory '${NewFolderName}'?", "Danger!", [ref]$yesToAll, [ref]$noToAll)){
                if($yesToAll){
                    $Force = $true
                }
            }
            else{
                return
            }
        }

        # Replace with actual directory creation logic here, 
        # pass `-Force` or `-Confirm:$false` to New-Item to suppress its own confirmation prompt
        Write-Host "Creating new folder ${NewFolderName}" -ForegroundColor Red
    }
}

Now, if the user presses [A] ("Yes To All"), we'll remember it by setting $Force to true, which will skip any subsequent calls to ShouldContinue():

PS ~> "folder 1","folder 2"|Create-NewFolderStructure

Danger!
Do you want to go ahead and create new directory 'folder 1'?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"): A
Creating new folder folder 1
Creating new folder folder 2
PS ~>

I'd strongly suggest reading through this deep dive to better understand the SupportsShouldProcess facility: Everything you wanted to know about ShouldProcess

CodePudding user response:

Mathias R. Jessen's helpful answer provides an alternative to using the common -Confirm parameter, via a slightly different mechanism based on a custom -Force switch that inverts the logic of your own attempt (prompt is shown by default, can be skipped with -Force).

If you want to make your -Confirm-based approach work:

  • Call .ShouldProcess() yourself at the start of the process block, which presents the familiar confirmation prompt, ...

  • ... and, if the method returns $true - indicating that the use confirmed - perform the desired operations after setting a local copy of the $ConfirmPreference preference variable to 'None', so as to prevent the cmdlets involved in your operations to each prompt as well.

function New-FolderStructure {
  [CmdletBinding(SupportsShouldProcess)]
  param (
    [Parameter(Mandatory, ValueFromPipeline)]
    [string] $NewFolderName,      
    [string] $LiteralPath = $PWD,  
    [string] $InputFile = "zChildDirectoryNames.txt"
  )

  # This block is called once, before pipeline processing begins.
  begin {
    $directories = Get-Content -LiteralPath "$LiteralPath\$InputFile"
  }

  # This block is called once if parameter $NewFolderName is bound
  # by an *argument* and once for each input string if bound via the *pipeline*.
  process {

    # If -Confirm was passed, this will prompt for confirmation
    # and return `$true` if the user confirms.
    # Otherwise `$true` is automatically returned, which also happens
    # if '[A] Yes to All' was chosen for a previous input string.
    if ($PSCmdlet.ShouldProcess($NewFolderName)) {

      # Prevent the cmdlets called below from *also* prompting for confirmation.
      # Note: This creates a *local copy* of the $ConfirmPreference
      # preference variable.
      $ConfirmPreference = 'None'

      If (-not (Test-Path -LiteralPath "$LiteralPath\$NewFolderName")) {
        $directories | ForEach-Object { New-Item -ItemType Directory -Path "$LiteralPath\$NewFolderName" -Name $_ }
      }
      Else {
        Write-Host "A $NewFolderName folder already exists"
      }

    }

  }

}

Note:

  • I've changed your function's name from Create-NewFolderStructure to New-FolderStructure to comply with PowerShell's naming conventions, using the approved "verb" New and streamlined the code in a number of ways.

  • ValueFromPipeline was added as a property to the $NewFolderName parameter so as to support passing multiple values via the pipeline (e.g.
    'foo', 'bar' | New-FolderStructure -Confirm)

    • Note that the process block is then called for each input string, and will also prompt for each, unless you respond with [A] Yes to All); PowerShell remembers this choice between process calls and makes .ShouldProcess() automatically return $true in subsequent calls; similarly, responding to [L] No to All automatically returns $false in subsequent calls.
  • The need to set $ConfirmPreference to 'None' stems from the fact that PowerShell automatically translates the caller's use of -Confirm into a function-local $ConfirmPreference variable with value 'Low', which makes all cmdlets that support -Confirm act as if -Confirm had been passed.

    • .ShouldProcess() does not pick up in-function changes to the value of this $ConfirmPreference copy, so it is OK to set it to 'None', without affecting the prompt logic of subsequent process invocations.
  • Related