Tudo o que você queria saber sobre o ShouldProcess

As funções do PowerShell têm vários recursos que aprimoram muito a interação com os usuários. Um recurso importante, mas que muitas vezes é ignorado é o suporte para -WhatIf e -Confirm, e é muito fácil adicioná-lo às funções. Neste artigo, vamos nos aprofundar em como implementar esse recurso.

Observação

A versão original deste artigo foi publicada no blog escrito por @KevinMarquette. A equipe do PowerShell agradece a Kevin por compartilhar o conteúdo conosco. Confira o blog dele em PowerShellExplained.com.

É um recurso simples e que você pode habilitar nas funções para fornecer uma rede de segurança aos usuários que precisarem. Não há nada mais assustador do que executar um comando que você sabe que pode ser perigoso pela primeira vez. A opção de executá-lo com -WhatIf pode fazer muita diferença.

ParâmetrosComuns

Antes de vermos como implementar esses parâmetros comuns, vamos dar uma olhada em como eles são usados.

Usando -WhatIf

Quando um comando dá suporte ao parâmetro -WhatIf, ele permite que você veja o que o comando teria feito em vez de fazer as alterações. Essa é uma boa maneira de testar o impacto de um comando, especialmente antes de fazer algo destrutivo.

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 o comando ShouldProcess for implementado corretamente, ele deve mostrar todas as alterações que teria feito. Aqui está um exemplo que usa um curinga para excluir vários arquivos.

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

Usando -Confirm

Comandos que oferecem suporte ao -WhatIf também oferecem suporte ao -Confirm. Isso dá a você a chance de confirmar uma ação antes de executá-la.

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

Nesse caso, você tem várias opções que permitem continuar, ignorar uma alteração ou parar o script. O prompt de ajuda descreve cada uma dessas opções assim.

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

Localização

Este prompt é localizado no PowerShell e o idioma é alterado com base no idioma do seu sistema operacional. Essa é mais uma das coisas que o PowerShell gerencia para você.

Parâmetros [switch]

Vamos dar um momento rápido para examinar maneiras de passar um valor para um [switch] parâmetro. O principal motivo pelo qual eu enfatizo isso é que, muitas vezes, você deseja passar valores de parâmetro para as funções que chama.

A primeira abordagem é uma sintaxe de parâmetro específica que pode ser usada para todos os parâmetros, mas você a vê principalmente usada para [switch] parâmetros. Você especifica um dois-pontos para atribuir um valor ao parâmetro.

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

Você pode fazer o mesmo com uma variável.

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

A segunda abordagem é usar uma tabela de hash para espalhar o valor.

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

Se você é novo em tabelas de hash ou splatting, eu tenho outro artigo que cobre tudo o que você queria saber sobre as tabelas de hash.

SupportsShouldProcess

A primeira etapa para habilitar o suporte a -WhatIf e -Confirm é especificar SupportsShouldProcess no CmdletBinding da função.

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

Ao especificar SupportsShouldProcess dessa forma, podemos chamar nossa função com -WhatIf (ou -Confirm).

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

Observe que eu não criei um parâmetro chamado -WhatIf. Ao especificar SupportsShouldProcess, ele é automaticamente criado para nós. Quando especificamos o parâmetro -WhatIf no Test-ShouldProcess, algumas coisas que chamamos também executam o processamento -WhatIf.

Observação

Quando você usa SupportsShouldProcess, o PowerShell não adiciona a variável $WhatIf à função. Você não precisa verificar o valor de $WhatIf porque o método ShouldProcess() cuida disso para você.

Confiar sem deixar de verificar

Há certo perigo em confiar que tudo o que você chama herdará valores -WhatIf. Para o restante dos exemplos, vou supor que não funcionará assim e serei bastante explícito ao fazer chamadas para outros comandos. Recomendo que você faça o mesmo.

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

Revisitarei as nuances mais adiante, quando você já tiver uma compreensão melhor de todas as peças em jogo.

$PSCmdlet.ShouldProcess

O método que permite implementar SupportsShouldProcess é $PSCmdlet.ShouldProcess. Você chama $PSCmdlet.ShouldProcess(...) para ver se deve processar alguma lógica e o PowerShell cuida do resto. Vamos começar com um exemplo:

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

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

A chamada para $PSCmdlet.ShouldProcess($file.Name) verifica -WhatIf (e o parâmetro -Confirm) e então o processa adequadamente. O -WhatIf faz com que ShouldProcess gere uma descrição da alteração e retorne $false:

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

Uma chamada usando -Confirm pausa o script e solicita ao usuário a opção de continuar. Ele retornará $true se o usuário tiver selecionado 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"):

Um recurso incrível do $PSCmdlet.ShouldProcess é que ele também serve como saída verbosa. Eu frequentemente recorro a isso ao implementar ShouldProcess.

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

Sobrecargas

Há algumas sobrecargas diferentes para $PSCmdlet.ShouldProcess com parâmetros diferentes para personalizar as mensagens. Já vimos a primeira no exemplo acima. Vamos examinar cada uma mais detalhadamente.

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

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

Isso produz a saída que inclui o nome da função e o destino (valor do parâmetro).

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

Ao especificar um segundo parâmetro como a operação, o valor da operação é usado em vez do nome da função na mensagem.

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

A próxima opção é especificar três parâmetros para personalizar a mensagem. Quando três parâmetros são usados, o primeiro é a mensagem inteira. Os dois últimos parâmetros ainda são usados na saída da mensagem de -Confirm.

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

Referência de parâmetro rápida

Caso você esteja aqui apenas para descobrir quais parâmetros devem ser usados, aqui está uma referência rápida que mostra como os parâmetros alteram a mensagem nos diferentes cenários de -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

Eu costumo usar a opção com dois parâmetros.

ShouldProcessReason

Existe uma quarta sobrecarga que é mais avançada do que as demais. Ela permite que você entenda o motivo pelo qual ShouldProcess foi executado. Estou adicionando isso aqui apenas para completar, porque podemos simplesmente verificar se $WhatIfPreference é $true.

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

Precisamos passar a variável $reason para o quarto parâmetro como uma variável de referência com [ref]. ShouldProcess preenche $reason com o valor None ou WhatIf. Eu nunca disse que isso era útil e nunca tive razão para usá-lo.

Onde colocar

Você usa ShouldProcess para tornar os scripts mais seguros. Assim, você deve usá-lo quando seus scripts estiverem fazendo alterações. Gosto de colocar a chamada $PSCmdlet.ShouldProcess o mais próximo possível da alteração.

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

Se eu estiver processando uma coleção de itens, eu o chamo para cada item. Portanto, a chamada é colocada dentro do foreach loop.

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

O motivo pelo qual eu coloco ShouldProcess rigidamente ao redor da alteração é porque eu quero que o máximo de códigos possível seja executado quando -WhatIf for especificado. Quero que a instalação e a validação sejam executadas, se possível, de maneira que o usuário veja esses erros.

Também gosto de usar isso em testes do Pester que validem meus projetos. Se eu tiver uma parte da lógica difícil de simular em Pester, eu geralmente a encapsulo em ShouldProcess e a chamo com -WhatIf em meus testes. É melhor testar parte do código do que nenhum código.

$WhatIfPreference

A primeira variável de preferência que temos é $WhatIfPreference. Por padrão, é $false. Se você a definir como $true, a função será executada como se você tivesse especificado -WhatIf. Se você definir isso em sua sessão, todos os comandos executarão -WhatIf.

Quando você chama uma função com -WhatIf, o valor de $WhatIfPreference é definido como $true dentro do escopo da sua função.

ConfirmImpact

A maioria dos meus exemplos são para -WhatIf, mas tudo o que vimos até agora também funciona com -Confirm para solicitar ao usuário. Você pode definir a ConfirmImpact da função como alta, e ela exibirá um prompt ao usuário como se tivesse sido chamada com -Confirm.

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

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

Essa chamada para Test-ShouldProcess está executando a ação -Confirm por causa do impacto 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

O problema óbvio é que agora é mais difícil usar em outros scripts sem avisar o usuário. Nesse caso, podemos passar um $false para -Confirm para suprimir o prompt.

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

Abordarei como adicionar o suporte a -Force em uma seção posterior.

$ConfirmPreference

$ConfirmPreference é uma variável automática que controla quando ConfirmImpact solicita que você confirme a execução. Estes são os valores possíveis tanto para $ConfirmPreference, como para ConfirmImpact.

  • High
  • Medium
  • Low
  • None

Com esses valores, você pode especificar diferentes níveis de impacto para cada função. Se você tiver $ConfirmPreference definido com um valor maior que ConfirmImpact, não será solicitado a confirmar a execução.

Por padrão, $ConfirmPreference é definido como High e ConfirmImpact, como Medium. Se você quiser que sua função solicite automaticamente o usuário, defina ConfirmImpact como High. Caso contrário, defina-o como Medium se for destrutivo e use Low se o comando for sempre seguro para execução em ambiente de produção. Se você o definir como none, ele não será solicitado mesmo que -Confirm tenha sido especificado (mas ainda oferece suporte a -WhatIf).

Ao chamar uma função com -Confirm, o valor de $ConfirmPreference é definido como Low dentro do escopo da função.

Suprimir avisos de confirmação aninhados

As funções que você chama podem captar o $ConfirmPreference. Isso pode criar cenários em que você adiciona uma solicitação de confirmação e a função que você chama também solicita confirmação ao usuário.

O que costumo fazer é especificar -Confirm:$false nos comandos que eu chamo quando já gerenciei o prompt.

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

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

Isso nos leva de volta a algo que já alertei antes: Há nuances sobre quando -WhatIf não é passado para uma função e quando -Confirm passa para uma função. Prometo que tratarei disso depois.

$PSCmdlet.ShouldContinue

Se precisar de mais controle do que ShouldProcess fornece, você poderá disparar o prompt diretamente com ShouldContinue. ShouldContinue ignora $ConfirmPreference, ConfirmImpact, -Confirm, $WhatIfPreference e -WhatIf porque eles são solicitados toda vez que são executados.

De relance, é fácil confundir ShouldProcess e ShouldContinue. Eu geralmente me lembro de usar ShouldProcess porque o parâmetro é chamado SupportsShouldProcess no CmdletBinding. Você deve usar ShouldProcess em quase todos os cenários. É por isso que abordei esse método primeiro.

Vamos dar uma olhada no ShouldContinue em ação.

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

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

Isso nos dá um prompt mais simples com menos opções.

Test-ShouldContinue

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

O maior problema com ShouldContinue é que ele requer que o usuário o execute interativamente, pois sempre solicita uma resposta do usuário. Você deve sempre criar ferramentas que podem ser usadas por outros scripts. A maneira de fazer isso é implementar -Force. Voltarei a essa ideia mais tarde.

Sim para todos

Isso é manipulado automaticamente com ShouldProcess, mas temos que nos esforçar um pouco mais para ShouldContinue. Há uma segunda sobrecarga de método em que precisamos transmitir alguns valores por referência para gerir a lógica.

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

Adicionei um loop foreach e uma coleção para mostrá-los em ação. Eu retirei a chamada ShouldContinue da instrução if para torná-la mais legível. Chamar um método com quatro parâmetros começa a ficar um pouco feio, mas tentei fazer com que ficasse o mais limpo possível.

Implementando o comando -Force

ShouldProcess e ShouldContinue precisam implementar -Force de maneiras diferentes. O truque para essas implementações é que ShouldProcess sempre deve ser executado, mas ShouldContinue não deve ser executado se -Force for especificado.

ShouldProcess -Force

Se você definir ConfirmImpact como high, a primeira coisa que os usuários tentarão é suprimi-lo com -Force. Essa é a primeira coisa que eu faço, pelo menos.

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

Se você se lembra da seção ConfirmImpact, eles precisam chamá-la da seguinte maneira:

Test-ShouldProcess -Confirm:$false

Nem todos percebem que eles precisam fazer isso e que -Force não suprime ShouldContinue. Então, devemos implementar -Force pelo bem da saúde mental de nossos usuários. Dê uma olhada neste exemplo completo:

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

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

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

Adicionamos nosso próprio switch -Force como um parâmetro. O parâmetro -Confirm é adicionado automaticamente ao usar SupportsShouldProcess no CmdletBinding. No entanto, quando você usa SupportsShouldProcess, o PowerShell não adiciona a variável $Confirm à função. Se você estiver executando no Modo Estrito e tentar usar a variável $Confirm antes que ela seja definida, você obterá um erro. Para evitar o erro, você pode usar $PSBoundParameters para testar se o parâmetro foi passado pelo usuário.

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

Se o usuário especificar -Force, definimos $ConfirmPreference como None no escopo local. Se o usuário também especificar -Confirm, então ShoudProcess() respeitará os valores do parâmetro -Confirm.

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

Caso alguém especifique -Force e -WhatIf, então -WhatIf precisará ter prioridade. Essa abordagem preserva o processamento de -WhatIf porque ShouldProcess sempre é executado.

Não adicione um teste do valor $Force dentro da instrução if que utiliza ShouldProcess. Esse é um antipadrão para esse cenário específico, embora seja a mesma coisa que eu mostrarei na próxima seção para ShouldContinue.

ShouldContinue -Force

Essa é a maneira correta de implementar -Force com ShouldContinue.

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

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

Ao colocar o $Force à esquerda do operador de -or, ele é avaliado primeiro. Escrevê-lo dessa forma interrompe a execução da instrução if. Se $Force for $true, então o ShouldContinue não será executado.

PS> Test-ShouldContinue -Force
Some Action

Não precisamos nos preocupar com -Confirm ou -WhatIf nesse cenário porque eles não têm suporte do ShouldContinue. É por isso que precisamos tratá-lo de maneira diferente do ShouldProcess.

Problemas de escopo

-WhatIf e -Confirm devem ser aplicados a tudo dentro de suas funções e a tudo que elas chamam. Eles o fazem definindo $WhatIfPreference como $true ou definindo $ConfirmPreference como Low no escopo local da função. Quando você chama outra função, as chamadas para ShouldProcess usam esses valores.

Isso funciona na maioria das vezes. Sempre que você chamar um cmdlet embutido ou uma função no mesmo escopo, ele funcionará. Também funcionará quando você chamar um script ou uma função em um módulo de script do console.

O local específico em que ele não funciona é quando um script ou um módulo de script chama uma função em outro módulo de script. Isso pode não parecer um grande problema, mas a maioria dos módulos criados ou extraídos do PSGallery são módulos de script.

O principal problema é que os módulos de script não herdam os valores de $WhatIfPreference ou $ConfirmPreference (e várias outros) quando chamados de funções em outros módulos de script.

A melhor maneira de resumir isso em uma regra geral é que funciona corretamente para módulos binários, mas não é confiável para módulos de script. Caso você não tenha certeza, teste-o ou apenas presuma que ele não funciona corretamente.

Pessoalmente, eu acredito que isso é muito perigoso, pois cria cenários em que você adiciona suporte para -WhatIf a vários módulos que funcionam corretamente em isolamento, mas não quando chamam um ao outro.

Temos uma RFC GitHub trabalhando para corrigir esse problema. Consulte Propagar as preferências de execução além do escopo do módulo de script para obter mais detalhes.

Concluindo

Tenho que pesquisar como usar ShouldProcess toda vez que preciso usá-lo. Demorei muito tempo para conseguir distinguir ShouldProcess de ShouldContinue. Eu quase sempre preciso pesquisar quais parâmetros usar. Então, não se preocupe se você ainda se sentir confuso de vez em quando. Este artigo estará aqui sempre que você precisar. Tenho certeza de que eu mesmo vou consultá-lo com frequência.

Se você gostou desta postagem, compartilhe seus pensamentos comigo no Twitter usando o link abaixo. Gosto de saber das pessoas que apreciam meus conteúdos.