Tutto quello che c'è da sapere su ShouldProcess

Le funzioni di PowerShell includono diverse funzionalità che migliorano significativamente il modo in cui gli utenti interagiscono con loro. Una funzionalità importante che è spesso trascurata è il supporto di -WhatIf e -Confirm ed è facile da aggiungere alle funzioni. Questo articolo illustra in dettaglio come implementare questa funzionalità.

Nota

La versione originale di questo articolo è apparsa sul blog scritto da @KevinMarquette. Il team di PowerShell ringrazia Kevin per averne condiviso il contenuto. È possibile visitare il suo blog all'indirizzo PowerShellExplained.com.

Si tratta di una funzionalità semplice che è possibile abilitare nelle funzioni per fornire una rete di sicurezza per gli utenti che ne hanno bisogno. Non c'è niente di più spaventoso dell'esecuzione per la prima volta di un comando che si sa che potrebbe essere pericoloso. La possibilità di eseguirlo con -WhatIf può fare una grande differenza.

CommonParameters

Prima di esaminare l'implementazione di questi parametri comuni, esamineremo rapidamente il modo in cui vengono usati.

Utilizzo di -WhatIf

Quando un comando supporta il parametro -WhatIf, questo consente di visualizzare le operazioni che verrebbero eseguite dal comando anziché apportare modifiche. Si tratta di un metodo efficace per verificare l'effetto di un comando, soprattutto prima di eseguire un'operazione dannosa.

PS C:\temp> Get-ChildItem
    Directory: C:\temp
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         4/19/2021   8:59 AM              0 importantfile.txt
-a----         4/19/2021   8:58 AM              0 myfile1.txt
-a----         4/19/2021   8:59 AM              0 myfile2.txt

PS C:\temp> Remove-Item -Path .\myfile1.txt -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".

Se il comando implementa correttamente ShouldProcess, verranno visualizzate tutte le modifiche che verrebbero apportate. Di seguito è riportato un esempio di utilizzo di un carattere jolly per eliminare più file.

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".

Utilizzo di -Confirm

I comandi che supportano -WhatIf supportano anche -Confirm. Ciò consente di confermare un'azione prima di eseguirla.

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 questo caso, sono disponibili più opzioni che consentono di continuare, ignorare una modifica o arrestare lo script. Il prompt della guida descrive tutte le opzioni come questa.

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"):

Localizzazione

Questo prompt è localizzato in PowerShell in modo che la lingua cambi in base alla lingua del sistema operativo in uso. Questo è un altro aspetto che PowerShell gestisce automaticamente.

Parametri opzionali

Vediamo rapidamente i modi per passare un valore a un parametro opzionale. Il motivo principale per cui trattiamo questo argomento è che spesso si desidera passare i valori dei parametri alle funzioni chiamate.

Il primo approccio è la sintassi di un parametro specifico che può essere usata per tutti i parametri, ma viene usata per lo più per i parametri opzionali. È possibile specificare i due punti per collegare un valore al parametro.

Remove-Item -Path:* -WhatIf:$true

È possibile eseguire la stessa operazione con una variabile.

$DoWhatIf = $true
Remove-Item -Path * -WhatIf:$DoWhatIf

Il secondo approccio consiste nell'usare una tabella hash per eseguire lo splatting del valore.

$RemoveSplat = @{
    Path = '*'
    WhatIf = $true
}
Remove-Item @RemoveSplat

Se non si ha familiarità con le tabelle hash o lo splatting, è disponibile un altro articolo che illustra tutto ciò che c’è da sapere sulle tabelle hash.

SupportsShouldProcess

Il primo passaggio per abilitare il supporto -WhatIf e -Confirm consiste nello specificare SupportsShouldProcess in CmdletBinding nella funzione.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    Remove-Item .\myfile1.txt
}

Specificando SupportsShouldProcess in questo modo, è ora possibile chiamare la funzione con -WhatIf (o -Confirm).

PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".

Si noti che non è stato creato un parametro denominato -WhatIf. Specificando SupportsShouldProcess, viene creato automaticamente. Quando si specifica il parametro -WhatIf su Test-ShouldProcess, vengono eseguite anche altre operazioni di elaborazione -WhatIf.

Fidarsi ma verificare

Ci sono alcuni rischi quando si dà per scontato che tutto ciò che si chiama eredita valori -WhatIf. Per il resto degli esempi, presupponiamo che questo non funzioni e saremo molto espliciti quando effettuiamo chiamate ad altri comandi. È consigliabile che gli utenti facciano lo stesso.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    Remove-Item .\myfile1.txt -WhatIf:$WhatIfPreference
}

L’articolo tratterà le sfumature molto più avanti una volta che il lettore avrà una conoscenza più approfondita di tutti i componenti in gioco.

$PSCmdlet.ShouldProcess

Il metodo che consente di implementare SupportsShouldProcess è $PSCmdlet.ShouldProcess. Si chiama $PSCmdlet.ShouldProcess(...) per verificare se è necessario elaborare una logica e PowerShell si occupa del resto. Iniziamo con un esempio:

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    $file = Get-ChildItem './myfile1.txt'
    if($PSCmdlet.ShouldProcess($file.Name)){
        $file.Delete()
    }
}

La chiamata a $PSCmdlet.ShouldProcess($file.name) verifica -WhatIf (e il parametro -Confirm) e lo gestisce di conseguenza. -WhatIf causa ShouldProcess l'output di una descrizione della modifica e la restituzione $false:

PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".

Una chiamata che usa -Confirm sospende lo script e chiede all'utente se desidera continuare. Restituisce $true se l'utente ha selezionato 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"):

Una funzionalità eccezionale di $PSCmdlet.ShouldProcess è che funziona anche come output dettagliato. Si fa spesso affidamento su questo quando si implementa ShouldProcess.

PS> Test-ShouldProcess -Verbose
VERBOSE: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".

Overload

Esistono alcuni overload diversi per $PSCmdlet.ShouldProcess con parametri diversi per la personalizzazione della messaggistica. Abbiamo già visto il primo nell'esempio precedente. Ecco informazioni più approfondite a riguardo.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    if($PSCmdlet.ShouldProcess('TARGET')){
        # ...
    }
}

Viene generato l'output che include sia il nome della funzione che la destinazione (valore del parametro).

What if: Performing the operation "Test-ShouldProcess" on target "TARGET".

Se si specifica un secondo parametro come operazione viene utilizzato il valore dell'operazione anziché il nome della funzione nel messaggio.

## $PSCmdlet.ShouldProcess('TARGET','OPERATION')
What if: Performing the operation "OPERATION" on target "TARGET".

L'opzione successiva consiste nello specificare tre parametri per personalizzare completamente il messaggio. Quando si utilizzano tre parametri, il primo è l'intero messaggio. I due parametri successivi vengono comunque usati nell'output del messaggio -Confirm.

## $PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION')
What if: MESSAGE

Riferimento rapido ai parametri

Nel caso in cui si stia leggendo solo per sapere quali parametri usare, di seguito è riportato un riferimento rapido che mostra il modo in cui i parametri modificano il messaggio nei diversi scenari di -WhatIf.

## $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

Generalmente l’autore utilizza quello con due parametri.

ShouldProcessReason

Il quarto overload è più avanzato rispetto agli altri. Consente di ottenere il motivo per cui è stato eseguito ShouldProcess. Aggiungiamo questo solo per motivi di completezza, in quanto è possibile verificare solo se $WhatIfPreference è $true.

$reason = ''
if($PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION',[ref]$reason)){
    Write-Output "Some Action"
}
$reason

È necessario passare la variabile $reason al quarto parametro come variabile di riferimento con [ref]. ShouldProcess compila $reason con il valore None o WhatIf. Questa operazione non è necessariamente utile e l’autore non ha mai avuto motivo di usarla.

Dove posizionarla

Si utilizza ShouldProcess per rendere più sicuri gli script. Quindi, viene usato quando gli script apportano modifiche. Spesso si posiziona la chiamata $PSCmdlet.ShouldProcess il più vicino possibile alla modifica.

## general logic and variable work
if ($PSCmdlet.ShouldProcess('TARGET','OPERATION')){
    # Change goes here
}

Se si sta elaborando una raccolta di elementi, si esegue la chiamata per ogni elemento. Quindi, la chiamata viene inserita all'interno per ogni ciclo.

foreach ($node in $collection){
    # general logic and variable work
    if ($PSCmdlet.ShouldProcess($node,'OPERATION')){
        # Change goes here
    }
}

Il motivo per cui si pone ShouldProcess così vicino alla modifica è che si desidera eseguire la maggior quantità di codice possibile quando -WhatIf è specificato. Si desidera che il setup e la convalida vengano eseguiti se possibile in modo che l’utente possa vedere questi errori.

È inoltre utile usare questa funzionalità nei test Pester che convalidano i progetti. Se è presente un elemento di logica difficile da simulare in Pester, è spesso possibile incapsularlo in ShouldProcess e chiamarlo con -WhatIf nei test. È preferibile testare parte del codice anziché non testarlo per niente.

$WhatIfPreference

La prima variabile di preferenza è $WhatIfPreference. Per impostazione predefinita è $false. Se si imposta su $true, la funzione viene eseguita come se fosse stata specificata -WhatIf. Se si seleziona questa impostazione nella sessione, tutti i comandi eseguono -WhatIf.

Quando si chiama una funzione con -WhatIf, il valore di $WhatIfPreference viene impostato su $true all'interno dell'ambito della funzione.

ConfirmImpact

La maggior parte degli esempi è destinata a -WhatIf, ma tutto ciò funziona anche con -Confirm per richiedere conferma all'utente. È possibile impostare ConfirmImpact della funzione su Alto e richiedere conferma all'utente come se fosse stato chiamato con -Confirm.

function Test-ShouldProcess {
    [CmdletBinding(
        SupportsShouldProcess,
        ConfirmImpact = 'High'
    )]
    param()

    if ($PSCmdlet.ShouldProcess('TARGET')){
        Write-Output "Some Action"
    }
}

Questa chiamata a Test-ShouldProcess esegue l'azione -Confirm a causa dell’effetto di High.

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

Il problema ovvio è che ora è più difficile da usare in altri script senza richiedere conferma all'utente. In questo caso, è possibile passare un $false a -Confirm per disattivare la richiesta.

PS> Test-ShouldProcess -Confirm:$false
Some Action

Illustreremo le procedure per aggiungere supporto -Force in una sezione successiva.

$ConfirmPreference

$ConfirmPreference è una variabile automatica che controlla quando ConfirmImpact richiede di confermare l'esecuzione. Ecco i valori possibili per $ConfirmPreference e ConfirmImpact.

  • High
  • Medium
  • Low
  • None

Con questi valori, è possibile specificare diversi livelli di influenza per ogni funzione. Se $ConfirmPreference è impostato su un valore maggiore di ConfirmImpact, non viene richiesto di confermare l'esecuzione.

Per impostazione predefinita, $ConfirmPreference è impostato su High e ConfirmImpact è Medium. Se si desidera che la funzione chieda automaticamente conferma all'utente, impostare ConfirmImpact su High. In caso contrario, impostarlo su Medium se è distruttivo e utilizzare Low se il comando è sempre in esecuzione sicura nell'ambiente di produzione. Se lo si imposta su none, non viene richiesta conferma neanche se è stato specificato -Confirm (ma fornisce comunque supporto -WhatIf).

Quando si chiama una funzione con -Confirm, il valore di $ConfirmPreference viene impostato su Low all'interno dell'ambito della funzione.

Eliminazione di richieste di conferma annidate

$ConfirmPreference può essere prelevato dalle funzioni chiamate. Questo può creare scenari in cui si aggiunge una richiesta di conferma e la funzione chiamata richiede anche conferma all'utente.

Generalmente si specifica -Confirm:$false sui comandi che si chiamano quando la richiesta di conferma è già stata gestita.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    $file = Get-ChildItem './myfile1.txt'
    if($PSCmdlet.ShouldProcess($file.Name)){
        Remove-Item -Path $file.FullName -Confirm:$false
    }
}

Questo ci riporta a un avviso precedente: ci sono sfumature da quando -WhatIf non viene passato a una funzione e quando -Confirm passa a una funzione. Ne riparleremo seguito.

$PSCmdlet.ShouldContinue

Se è necessario un maggiore controllo rispetto a quello fornito da ShouldProcess, è possibile attivare il prompt direttamente con ShouldContinue. ShouldContinue ignora $ConfirmPreference, ConfirmImpact, -Confirm, $WhatIfPreference e -WhatIf perché richiede conferma ogni volta che viene eseguito.

A un primo sguardo, è facile confondere ShouldProcess e ShouldContinue. Si tende a ricordare di usare ShouldProcess perché il parametro viene chiamato SupportsShouldProcess nel CmdletBinding. È consigliabile utilizzare ShouldProcess in quasi tutti gli scenari. Questo è il motivo per cui si è parlato prima di tutto di questo metodo.

Vediamo ShouldContinue in azione.

function Test-ShouldContinue {
    [CmdletBinding()]
    param()

    if($PSCmdlet.ShouldContinue('TARGET','OPERATION')){
        Write-Output "Some Action"
    }
}

Questo fornisce una richiesta più semplice con meno opzioni.

Test-ShouldContinue

Second
TARGET
[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"):

Il problema più importante a proposito di ShouldContinue consiste nel fatto che richiede all'utente di eseguirlo in modo interattivo in quanto chiede sempre conferma all'utente. È consigliabile creare sempre strumenti di compilazione che possono essere usati da altri script. Per farlo, è necessario implementare -Force. Riprenderemo questa idea più avanti.

Sì a tutti

Questa operazione viene gestita automaticamente con ShouldProcess, ma è necessario eseguire alcune operazioni per ShouldContinue. Esiste un secondo overload del metodo in cui è necessario passare alcuni valori per riferimento per controllare la logica.

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]"
        }
    }
}

Sono stati aggiunti un ciclo foreach e una raccolta per visualizzarlo in azione. Abbiamo estratto la chiamata ShouldContinue dall'istruzione if per semplificare la lettura. La chiamata a un metodo con quattro parametri inizia a essere un po’ complicata, ma qui si è cercato di renderla il più semplice possibile.

Implementazione di -Force

ShouldProcess e ShouldContinue devono implementare -Force in modi diversi. Il trucco per queste implementazioni è che ShouldProcess deve sempre essere eseguito, ma ShouldContinue non deve essere eseguito se -Force è specificato.

ShouldProcess -Force

Se si imposta ConfirmImpact su high, la prima cosa che l’utente cercherà di fare è eliminarlo con -Force. In generale, questo è ciò che accade.

Test-ShouldProcess -Force
Error: Test-ShouldProcess: A parameter cannot be found that matches parameter name 'force'.

Se si ricorda la sezione ConfirmImpact, è in realtà necessario chiamarla come segue:

Test-ShouldProcess -Confirm:$false

Non tutti si rendono conto che è necessario eseguire questa operazione e che -Force non elimina ShouldContinue. Quindi, dobbiamo implementare -Force per l'integrità degli utenti. Vediamo questo esempio completo:

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"
    }
}

Viene aggiunta l'opzione -Force come parametro. Il parametro -Confirm viene aggiunto automaticamente quando si usa SupportsShouldProcess in CmdletBinding.

[CmdletBinding(
    SupportsShouldProcess,
    ConfirmImpact = 'High'
)]
param(
    [Switch]$Force
)

Facciamo attenzione alla logica di -Force:

if ($Force -and -not $Confirm){
    $ConfirmPreference = 'None'
}

Se l'utente specifica -Force, desidera disattivare la richiesta di conferma, a meno che non specifichi anche -Confirm. In questo modo un utente può forzare una modifica ma confermare comunque la modifica. In seguito viene impostato $ConfirmPreference nell'ambito locale. A questo punto, l'uso del parametro -Force imposta temporaneamente $ConfirmPreference su None, disabilitando la richiesta di conferma.

if ($PSCmdlet.ShouldProcess('TARGET')){
        Write-Output "Some Action"
    }

Se un utente specifica sia -Force che -WhatIf, allora -WhatIf deve avere priorità. Questo approccio consente di mantenere l’elaborazione di -WhatIf perché ShouldProcess viene sempre eseguita.

Non aggiungere un controllo per il valore $Force all'interno dell'istruzione if con ShouldProcess. Si tratta di un anti-modello per questo scenario specifico, anche se questo è quello che verrà illustrato nella sezione successiva per ShouldContinue.

ShouldContinue -Force

Questo è il modo corretto per implementare -Force con ShouldContinue.

function Test-ShouldContinue {
    [CmdletBinding()]
    param(
        [Switch]$Force
    )

    if($Force -or $PSCmdlet.ShouldContinue('TARGET','OPERATION')){
        Write-Output "Some Action"
    }
}

Inserendo $Force a sinistra dell'operatore -or, viene valutato per primo. In questo modo, l'esecuzione dell'istruzione if viene velocizzata. Se $force è $true, ShouldContinue non viene eseguito.

PS> Test-ShouldContinue -Force
Some Action

In questo scenario non è necessario preoccuparsi di -Confirm o -WhatIf perché non sono supportati da ShouldContinue. Questo è il motivo per cui deve essere gestito in modo diverso rispetto a ShouldProcess.

Problemi di ambito

L'uso di -WhatIf e -Confirm dovrebbe essere applicato a tutti gli elementi all'interno delle funzioni e a tutti gli elementi che chiamano. A tale scopo, è necessario impostare $WhatIfPreference su $true o impostare $ConfirmPreference su Low nell'ambito locale della funzione. Quando si chiama un'altra funzione, le chiamate a ShouldProcess utilizzano tali valori.

Questa operazione funziona in modo corretto nella maggior parte dei casi. Ogni volta che si chiama il cmdlet incorporato o una funzione nello stesso ambito, funziona. Funziona anche quando si chiama uno script o una funzione in un modulo di script dalla console.

L'unica posizione specifica in cui non funziona è quando uno script o un modulo di script chiama una funzione in un altro modulo di script. Questo potrebbe non sembrare un grosso problema, ma la maggior parte dei moduli creati o estratti da PSGallery sono moduli di script.

Il problema principale è che i moduli di script non ereditano i valori per $WhatIfPreference o $ConfirmPreference (e molti altri) quando vengono chiamati dalle funzioni in altri moduli di script.

Il modo migliore per riepilogare questo problema con una regola generale è affermare che funziona correttamente per i moduli binari e non è mai da considerarsi attendibile per i moduli di script. Se non si è certi, eseguirne il test o semplicemente presumere che non funzioni correttamente.

È possibile ritenere che questo sia molto pericoloso perché crea scenari in cui si aggiunge supporto -WhatIf a più moduli che funzionano correttamente in isolamento, ma non funzionano correttamente quando si chiamano reciprocamente.

Per risolvere il problema, è possibile usare una RFC di GitHub. Per ulteriori informazioni, vedere Propagate execution preferences beyond script module scope (Propagare le preferenze di esecuzione oltre l'ambito del modulo di script).

In conclusione

È possibile che si debba controllare come utilizzare ShouldProcess ogni volta che si desidera utilizzarlo. Serve molto tempo per distinguere ShouldProcess da ShouldContinue. È quasi sempre necessario cercare i parametri da usare. Non c’è quindi da preoccuparsi se ci si confonde ancora ogni tanto. Questo articolo sarà disponibile quando necessario. Anche l’autore vi farà spesso riferimento.

Se vi è piaciuto questo post, condividete le vostre opinioni su Twitter usando il collegamento seguente. All’autore fa piacere conoscere l’opinione di chi trova utili i suoi contenuti.