Tudo o que você queria saber sobre ShouldProcess

As funções do PowerShell têm vários recursos que melhoram muito a maneira como os usuários interagem com elas. Uma característica importante que muitas vezes é negligenciada é -WhatIf-Confirm o suporte e é fácil de adicionar às suas funções. Neste artigo, nos aprofundamos em como implementar esse recurso.

Nota

A versão original deste artigo apareceu no blog escrito por @KevinMarquette. A equipe do PowerShell agradece Kevin por compartilhar esse conteúdo conosco. Por favor, confira seu blog em PowerShellExplained.com.

Este é um recurso simples que você pode habilitar em suas funções para fornecer uma rede de segurança para os usuários que precisam dela. 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 pode -WhatIf fazer uma grande diferença.

CommonParameters

Antes de analisarmos a implementação desses parâmetros comuns, quero dar uma olhada rápida em como eles são usados.

Usando -WhatIf

Quando um comando suporta o -WhatIf parâmetro, ele permite que você veja o que o comando teria feito em vez de fazer alterações. É 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 for implementado ShouldProcesscorretamente, ele deverá mostrar todas as alterações que ele teria feito. Aqui está um exemplo usando 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

Os comandos que suportam -WhatIf também o .-Confirm Isso lhe dá 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 como esta.

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

Esse prompt é localizado no PowerShell para que o idioma seja alterado com base no idioma do seu sistema operacional. Essa é mais uma coisa que o PowerShell gerencia para você.

Parâmetros do interruptor

Vamos dar uma olhada rápida em maneiras de passar um valor para um parâmetro switch. A principal razão pela qual eu chamo isso é que muitas vezes você quer passar valores de parâmetro para funções que você 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 parâmetros de switch. Você especifica dois pontos para anexar 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 hashtable para splat o valor.

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

Se você é novo em hashtables ou splatting, tenho outro artigo sobre isso que cobre tudo o que você queria saber sobre hashtables.

ApoiosDevemProcesso

O primeiro passo para habilitar -WhatIf e -Confirm dar suporte é especificar SupportsShouldProcess na CmdletBinding sua função.

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

Especificando SupportsShouldProcess desta forma, podemos agora chamar a 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. Especificar SupportsShouldProcess automaticamente cria-o para nós. Quando especificamos o -WhatIf parâmetro em Test-ShouldProcess, algumas coisas que chamamos também executam -WhatIf processamento.

Confiar, mas verificar

Há algum perigo aqui confiando que tudo o que você chama herda -WhatIf valores. Para o resto dos exemplos, vou assumir que não funciona e ser muito explícito ao fazer chamadas para outros comandos. Eu recomendo que você faça o mesmo.

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

Vou revisitar as nuances muito mais tarde, uma vez que você tenha uma melhor compreensão de todas as peças em jogo.

$PSCmdlet.DeveProcessar

O método que permite implementar SupportsShouldProcess é $PSCmdlet.ShouldProcess. Você liga $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) verificar o -WhatIf (e parâmetro) então o manipula -Confirm de acordo. As -WhatIf causas ShouldProcess para produzir uma descrição da alteração e retorno $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 retorna $true se o usuário selecionou 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"):

Uma característica impressionante é $PSCmdlet.ShouldProcess que ele funciona como saída detalhada. Eu dependo disso muitas vezes ao implementar ShouldProcess.

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

Sobrecargas

Existem algumas sobrecargas diferentes para $PSCmdlet.ShouldProcess com diferentes parâmetros para personalizar as mensagens. Já vimos o primeiro no exemplo acima. Vamos dar uma olhada mais de perto.

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

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

Isso produz uma 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".

Especificando um segundo parâmetro como a operação usa o valor da operação 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 totalmente a mensagem. Quando três parâmetros são usados, o primeiro é a mensagem inteira. Os dois segundos parâmetros ainda são usados na saída da -Confirm mensagem.

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

Referência rápida de parâmetros

Apenas no caso de você vir aqui apenas para descobrir quais parâmetros você deve usar, aqui está uma referência rápida mostrando como os parâmetros mudam a mensagem nos diferentes -WhatIf cenários.

## $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 tendo a usar aquele com dois parâmetros.

ShouldProcessReason

Temos uma quarta sobrecarga que é mais avançada do que as outras. Ele permite que você obtenha o motivo pelo qual ShouldProcess foi executado. Eu só estou adicionando isso aqui para completar porque podemos apenas verificar se $WhatIfPreference é $true em vez disso.

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

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

Onde colocá-lo

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

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

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

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

A razão pela qual eu coloco ShouldProcess firmemente em torno da mudança, é que eu quero o máximo de código para executar quanto possível quando -WhatIf é especificado. Quero que a configuração e a validação sejam executadas, se possível, para que o usuário veja esses erros.

Eu também gosto de usar isso em meus testes Pester que validam meus projetos. Se eu tenho um pedaço de lógica que é difícil de zombar em pester, muitas vezes posso embrulhá-lo ShouldProcess e chamá-lo com -WhatIf nos meus testes. É melhor testar parte do seu código do que nenhum.

$WhatIfPreference

A primeira variável de preferência que temos é $WhatIfPreference. Isso ocorre $false por padrão. Se você defini-lo para $true então sua função é executada como se você especificou -WhatIf. Se você definir isso em sua sessão, todos os comandos executarão -WhatIf a execução.

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

ConfirmarImpacto

A maioria dos meus exemplos são para -WhatIf , mas tudo até agora também funciona com -Confirm para avisar o usuário. Você pode definir o ConfirmImpact da função como alto e ele solicita ao usuário como se fosse chamado com -Confirm.

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

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

Esta chamada para Test-ShouldProcess está executando a -Confirm ação devido ao High impacto.

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

A questão óbvia é que agora é mais difícil usar em outros scripts sem avisar o usuário. Neste caso, podemos passar um $false para -Confirm para suprimir o prompt.

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

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

$ConfirmPreference

$ConfirmPreference é uma variável automática que controla quando ConfirmImpact lhe pede para confirmar a execução. Aqui estão os valores possíveis para ambos $ConfirmPreference e 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 para um valor maior que ConfirmImpact, então você não será solicitado a confirmar a execução.

Por padrão, $ConfirmPreference está definido como High e ConfirmImpact é Medium. Se você quiser que sua função avise automaticamente o usuário, defina como ConfirmImpactHigh. Caso contrário, defina-o para Medium se for destrutivo e use Low se o comando for sempre seguro executado em produção. Se você defini-lo como none, ele não solicitará mesmo se -Confirm foi especificado (mas ainda lhe dará -WhatIf suporte).

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

Supressão de prompts de confirmação aninhados

O $ConfirmPreference pode ser captado por funções que você chama. Isso pode criar cenários em que você adiciona um prompt de confirmação e a função chamada também solicita ao usuário.

O que eu tendo a fazer é especificar -Confirm:$false os comandos que eu chamo quando eu já tenho manipulado o prompting.

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 um aviso anterior: há nuances sobre quando -WhatIf não é passado para uma função e quando -Confirm passa para uma função. Prometo que voltarei a isso mais tarde.

$PSCmdlet.DeveContinuar

Se você precisar de mais controle do que ShouldProcess o fornecido, você pode acionar o prompt diretamente com ShouldContinue. ShouldContinue$ConfirmPreferenceignora , ConfirmImpact, -Confirm, $WhatIfPreference, e -WhatIf porque ele solicita toda vez que é executado.

Em um rápido relance, é fácil confundir ShouldProcess e ShouldContinue. Eu tendo a lembrar de usar ShouldProcess porque o parâmetro é chamado SupportsShouldProcess no CmdletBinding. Você deve usar ShouldProcess em quase todos os cenários. Foi por isso que abordei primeiro esse método.

Vamos dar uma olhada ShouldContinue em ação.

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

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

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

Test-ShouldContinue

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

O maior problema é ShouldContinue que ele exige que o usuário o execute interativamente, porque ele sempre solicita ao usuário. Você deve sempre criar ferramentas que podem ser usadas por outros scripts. A maneira de fazer isso é implementando -Forceo . Voltarei a esta ideia mais tarde.

Yes to all

Isso é tratado automaticamente, ShouldProcess mas temos que fazer um pouco mais de trabalho para ShouldContinue. Há uma segunda sobrecarga de método onde temos que passar alguns valores por referência para controlar 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]"
        }
    }
}

Eu adicionei um foreach loop e uma coleção para mostrá-lo em ação. Puxei a ShouldContinue chamada da if declaração para facilitar a leitura. Chamar um método com quatro parâmetros começa a ficar um pouco feio, mas eu tentei fazê-lo parecer o mais limpo possível.

Implementação -Force

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

DeveProcessar -Força

Se você definir o seu ConfirmImpact como high, a primeira coisa que seus usuários vão tentar é suprimi-lo com -Force. Essa é a primeira coisa que faço de qualquer maneira.

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

Se você se lembrar da ConfirmImpact seção, eles realmente precisam chamá-lo assim:

Test-ShouldProcess -Confirm:$false

Nem todo mundo percebe que precisa fazer isso e -Force não reprime ShouldContinue. Portanto, devemos implementar -Force para a sanidade de nossos usuários. Dê uma olhada neste exemplo completo aqui:

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

Adicionamos o nosso próprio -Force interruptor como parâmetro. O -Confirm parâmetro é adicionado automaticamente ao usar SupportsShouldProcess no CmdletBinding.

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

Focando na -Force lógica aqui:

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

Se o usuário especificar -Force, queremos suprimir o prompt de confirmação, a menos que eles também especifiquem -Confirm. Isso permite que um usuário force uma alteração, mas ainda confirme a alteração. Em seguida, definimos $ConfirmPreference o âmbito local. Agora, usar o -Force parâmetro define temporariamente o como nenhum, desativando o $ConfirmPreference prompt de confirmação.

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

Se alguém especificar ambos -Force e -WhatIf, então -WhatIf precisa ter prioridade. Essa abordagem preserva o -WhatIf processamento porque ShouldProcess sempre é executado.

Não adicione uma verificação para o $Force valor dentro da if instrução com o ShouldProcess. Esse é um anti-padrão para este cenário específico, embora seja isso que mostro na próxima seção para ShouldContinue.

DeveContinuar -Força

Esta é a maneira correta de implementar -Force com ShouldContinueo .

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

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

Ao colocar o $Force à esquerda do -or operador, ele é avaliado primeiro. Escrevê-lo desta forma provoca um curto-circuito na execução da if declaração. Se $force for $true, então o ShouldContinue não é executado.

PS> Test-ShouldContinue -Force
Some Action

Não temos que nos preocupar com -Confirm ou -WhatIf neste cenário porque eles não são suportados pela ShouldContinue. É por isso que tem de ser tratado de forma diferente do ShouldProcess.

Questões relativas ao âmbito de aplicação

Usando -WhatIf e -Confirm são supostos para aplicar a tudo dentro de suas funções e tudo o que eles chamam. Eles fazem isso definindo $WhatIfPreference ou $true definindo $ConfirmPreference para Low no escopo local da função. Quando você chama outra função, chama para ShouldProcess usar esses valores.

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

O único lugar específico onde 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 que você cria ou extrai do PSGallery são módulos de script.

A questão central é que os módulos de script não herdam os valores para $WhatIfPreference ou $ConfirmPreference (e vários outros) quando chamados de funções em outros módulos de script.

A melhor maneira de resumir isso como uma regra geral é que isso funcione corretamente para módulos binários e nunca confie nele para funcionar para módulos de script. Se você não tiver certeza, teste-o ou simplesmente assuma que ele não funciona corretamente.

Eu pessoalmente sinto que isso é muito perigoso porque cria cenários onde você adiciona -WhatIf suporte a vários módulos que funcionam corretamente isoladamente, mas não funcionam corretamente quando eles ligam uns para os outros.

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

Para finalizar

Eu tenho que procurar como usar ShouldProcess cada vez que eu preciso usá-lo. Demorei muito tempo a distinguir ShouldProcess de ShouldContinue. Eu quase sempre preciso procurar quais parâmetros usar. Portanto, não se preocupe se você ainda ficar confuso de vez em quando. Este artigo estará aqui quando você precisar. Tenho certeza de que farei referência a ele muitas vezes.

Se você gostou deste post, por favor, compartilhe seus pensamentos comigo no Twitter usando o link abaixo. Eu sempre gosto de ouvir de pessoas que obtêm valor do meu conteúdo.