Compartilhar via


Capítulo 9 – Funções

Os scripts e one-liners do PowerShell que precisam ser modificados geralmente são bons candidatos para se transformar em funções reutilizáveis.

Sempre que possível, crie funções porque elas são mais orientadas a ferramentas. Você pode adicioná-las a um módulo de script, colocar esse módulo em um local definido no $env:PSModulePath e chamar as funções sem precisar localizar onde as salvou. Usando o módulo PowerShellGet, é fácil compartilhar módulos do PowerShell em um repositório do NuGet. O PowerShellGet acompanha o PowerShell versão 5.0 e superior. Ele também está disponível como download separado para o PowerShell versão 3.0 e superior.

Não complique demais as coisas. Simplifique e use a maneira mais direta de realizar uma tarefa. Evite aliases e parâmetros posicionais em qualquer código que você reutilize. Formate seu código para facilitar a leitura. Não embuta os valores em código. Use parâmetros e variáveis. Não escreva código desnecessário mesmo que isso não prejudique nada. Ele adiciona complexidade desnecessária. A atenção aos detalhes ajuda muito ao escrever qualquer código do PowerShell.

Nomenclatura

Ao nomear suas funções no PowerShell, use um nome Pascal Case com um verbo aprovado e um substantivo singular. Para obter uma lista dos verbos aprovados no PowerShell, execute Get-Verb. O exemplo a seguir classifica os resultados de Get-Verb pela propriedade Verb.

Get-Verb | Sort-Object -Property Verb

A propriedade Group dá uma ideia de como usar os verbos.

Verb        Group
----        -----
Add         Common
Approve     Lifecycle
Assert      Lifecycle
Backup      Data
Block       Security
Checkpoint  Data
Clear       Common
Close       Common
Compare     Data
Complete    Lifecycle
Compress    Data
Confirm     Lifecycle
Connect     Communications
Convert     Data
ConvertFrom Data
ConvertTo   Data
Copy        Common
Debug       Diagnostic
Deny        Lifecycle
Disable     Lifecycle
Disconnect  Communications
Dismount    Data
Edit        Data
Enable      Lifecycle
Enter       Common
Exit        Common
Expand      Data
Export      Data
Find        Common
Format      Common
Get         Common
Grant       Security
Group       Data
Hide        Common
Import      Data
Initialize  Data
Install     Lifecycle
Invoke      Lifecycle
Join        Common
Limit       Data
Lock        Common
Measure     Diagnostic
Merge       Data
Mount       Data
Move        Common
New         Common
Open        Common
Optimize    Common
Out         Data
Ping        Diagnostic
Pop         Common
Protect     Security
Publish     Data
Push        Common
Read        Communications
Receive     Communications
Redo        Common
Register    Lifecycle
Remove      Common
Rename      Common
Repair      Diagnostic
Request     Lifecycle
Reset       Common
Resize      Common
Resolve     Diagnostic
Restart     Lifecycle
Restore     Data
Resume      Lifecycle
Revoke      Security
Save        Data
Search      Common
Select      Common
Send        Communications
Set         Common
Show        Common
Skip        Common
Split       Common
Start       Lifecycle
Step        Common
Stop        Lifecycle
Submit      Lifecycle
Suspend     Lifecycle
Switch      Common
Sync        Data
Test        Diagnostic
Trace       Diagnostic
Unblock     Security
Undo        Common
Uninstall   Lifecycle
Unlock      Common
Unprotect   Security
Unpublish   Data
Unregister  Lifecycle
Update      Data
Use         Other
Wait        Lifecycle
Watch       Common
Write       Communications

É importante usar um verbo aprovado para suas funções do PowerShell. Os módulos que contêm funções com verbos não aprovados geram uma mensagem de aviso quando importados para uma sessão do PowerShell. Essa mensagem de aviso faz com que suas funções pareçam não profissionais. Os verbos não aprovados também limitam a capacidade de descoberta de suas funções.

Uma função simples

Uma função no PowerShell é declarada com a palavra-chave function seguida pelo nome da função e, em seguida, por uma chave de abertura e fechamento ({ }). O código executado pela função está contido nessas chaves.

function Get-Version {
    $PSVersionTable.PSVersion
}

A função mostrada a seguir é um exemplo simples que retorna a versão do PowerShell.

Get-Version
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  693

Quando você usa um nome genérico para suas funções, como Get-Version, pode gerar conflitos de nomenclatura. Comandos padrão adicionados no futuro ou comandos criados por outras pessoas podem entrar em conflito com elas. Use prefixos na parte dos nomes de função que correspondem a um substantivo para ajudar a evitar conflitos de nomenclatura. Por exemplo: <ApprovedVerb>-<Prefix><SingularNoun>.

O exemplo a seguir usa o prefixo PS.

function Get-PSVersion {
    $PSVersionTable.PSVersion
}

Além do nome, essa função é idêntica à anterior.

Get-PSVersion
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  693

Pode acontecer um conflito de nome mesmo quando você adiciona um prefixo ao substantivo. Gosto de usar as minhas iniciais como prefixo dos nomes das funções que crio. Desenvolva um padrão e se atenha a ele.

function Get-MrPSVersion {
    $PSVersionTable.PSVersion
}

Essa função não é diferente das duas anteriores, exceto por usar um nome mais exclusivo e, assim, tentar evitar conflitos de nomenclatura com outros comandos do PowerShell.

Get-MrPSVersion
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  693

Uma vez carregadas na memória, você poderá ver funções na PSDrive Function.

Get-ChildItem -Path Function:\Get-*Version
CommandType     Name                                               Version
-----------     ----                                               -------
Function        Get-Version
Function        Get-PSVersion
Function        Get-MrPSVersion

Para remover essas funções da sessão atual, você deve removê-las da PSDrive Function ou fechar e reabrir o PowerShell.

Get-ChildItem -Path Function:\Get-*Version | Remove-Item

Verifique se as funções foram realmente removidas.

Get-ChildItem -Path Function:\Get-*Version

Se as funções foram carregadas como parte de um módulo, você pode descarregar o módulo para removê-las.

Remove-Module -Name <ModuleName>

O cmdlet Remove-Module remove módulos do PowerShell da memória na sessão atual do PowerShell. Ele não os remove do sistema ou do disco.

Parâmetros

Não atribua valores estaticamente. Em vez disso, use parâmetros e variáveis. Ao nomear os parâmetros, sempre que possível, use o mesmo nome que os cmdlets padrão.

Na função a seguir, observe que usei ComputerName e não Computer, ServerName, ou Host para o nome do parâmetro. Usar ComputerName padroniza o nome do parâmetro para corresponder ao nome e ao caso do parâmetro, como os cmdlets padrão.

function Test-MrParameter {

    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

A função a seguir consulta todos os comandos no sistema e retorna o número com nomes de parâmetro específicos.

function Get-MrParameterCount {
    param (
        [string[]]$ParameterName
    )

    foreach ($Parameter in $ParameterName) {
        $Results = Get-Command -ParameterName $Parameter -ErrorAction SilentlyContinue

        [pscustomobject]@{
            ParameterName   = $Parameter
            NumberOfCmdlets = $Results.Count
        }
    }
}

Como você pode ver nos resultados a seguir, 39 comandos têm um parâmetro ComputerName. Não há comandos com parâmetros como Computer, ServerName, Host ou Machine.

Get-MrParameterCount -ParameterName ComputerName, Computer, ServerName,
    Host, Machine
ParameterName NumberOfCmdlets
------------- ---------------
ComputerName               39
Computer                    0
ServerName                  0
Host                        0
Machine                     0

Use a mesma capitalização para os nomes de seus parâmetros como fazem os cmdlets padrão. Por exemplo, use ComputerName, e não computername. Esse esquema de nomenclatura ajuda as pessoas familiarizadas com o PowerShell a descobrir suas funções, que funcionam e se apresentam como os cmdlets padrão.

A instrução param permite definir um ou mais parâmetros. Uma vírgula (,) separa as definições de parâmetro. Para obter mais informações, consulte about_Functions_Advanced_Parameters.

Funções avançadas

Transformar uma função em função avançada no PowerShell é simples. Uma das diferenças entre função e função avançada é que as funções avançadas têm parâmetros comuns que são adicionados automaticamente. Os parâmetros comuns incluem parâmetros como Verbose e Debug.

Comece com a função Test-MrParameter que foi usada na seção anterior.

function Test-MrParameter {

    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

Há algumas maneiras diferentes de ver os parâmetros comuns. Uma delas é exibir a sintaxe com Get-Command.

Get-Command -Name Test-MrParameter -Syntax

Observe que a função Test-MrParameter não tem parâmetros comuns.

Test-MrParameter [[-ComputerName] <Object>]

Outra opção é detalhar a propriedade de parâmetros de Get-Command.

(Get-Command -Name Test-MrParameter).Parameters.Keys
ComputerName

Adicione o atributo CmdletBinding para transformar a função em uma função avançada.

function Test-MrCmdletBinding {

    [CmdletBinding()] # Turns a regular function into an advanced function
    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

Quando você especifica CmdletBinding, os parâmetros comuns são adicionados automaticamente. CmdletBinding requer um bloco param, mas o bloco param pode estar vazio.

Get-Command -Name Test-MrCmdletBinding -Syntax
Test-MrCmdletBinding [[-ComputerName] <Object>] [<CommonParameters>]

O aprofundamento na propriedade de parâmetros do Get-Command mostra os nomes reais dos parâmetros, incluindo os comuns.

(Get-Command -Name Test-MrCmdletBinding).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable

SuportaProcessamento

O atributo SupportsShouldProcess adiciona os parâmetros de mitigação de risco WhatIf e Confirm. Esses parâmetros são necessários apenas para comandos que fazem alterações.

function Test-MrSupportsShouldProcess {

    [CmdletBinding(SupportsShouldProcess)]
    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

Observe que agora há parâmetros WhatIf e Confirm.

Get-Command -Name Test-MrSupportsShouldProcess -Syntax
Test-MrSupportsShouldProcess [[-ComputerName] <Object>] [-WhatIf] [-Confirm]
[<CommonParameters>]

Mais uma vez, você também pode usar Get-Command para retornar uma lista dos nomes de parâmetro propriamente ditos, incluindo os comuns, juntamente com WhatIf e Confirm.

(Get-Command -Name Test-MrSupportsShouldProcess).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable
WhatIf
Confirm

Validação do parâmetro

Valide a entrada no início. Não permita que o código continue em um caminho quando ele não puder ser concluído sem entrada válida.

Sempre especifique um tipo de dados para as variáveis usadas para os parâmetros. No exemplo a seguir, String, é especificado como o tipo de dados para o parâmetro ComputerName. Essa validação o limita a permitir que apenas um único nome de computador seja especificado para o parâmetro ComputerName.

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [string]$ComputerName
    )

    Write-Output $ComputerName

}

Um erro será gerado se for especificado mais de um nome de computador.

Test-MrParameterValidation -ComputerName Server01, Server02
Test-MrParameterValidation : Cannot process argument transformation on
parameter 'ComputerName'. Cannot convert value to type System.String.
At line:1 char:42
+ Test-MrParameterValidation -ComputerName Server01, Server02
+                                          ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Test-MrParameterValidation]
   , ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-MrP
   arameterValidation

O problema com a definição atual é que ela é válida para omitir o valor do parâmetro ComputerName, mas um valor é necessário para que a função seja concluída com êxito. Esse cenário é onde o atributo de parâmetro Mandatory traz benefícios.

A sintaxe usada no exemplo a seguir é compatível com o PowerShell versão 3.0 e superior. [Parameter(Mandatory=$true)] pode ser especificado em vez de tornar a função compatível com o PowerShell versão 2.0 ou superior.

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ComputerName
    )

    Write-Output $ComputerName

}

Agora que o ComputerName é necessário, se não for especificado, a função solicitará um.

Test-MrParameterValidation
cmdlet Test-MrParameterValidation at command pipeline position 1
Supply values for the following parameters:
ComputerName:

Se quiser permitir mais de um valor para o parâmetro ComputerName, use o tipo de dados String, mas adicione colchetes de abertura e de fechamento ([]) ao tipo de dados para permitir uma matriz de cadeias de caracteres.

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string[]]$ComputerName
    )

    Write-Output $ComputerName

}

Talvez você queira especificar um valor padrão para o parâmetro ComputerName se um não tiver especificado. O problema é que os valores padrão não podem ser usados com parâmetros obrigatórios. Em vez disso, use o atributo de validação de parâmetro ValidateNotNullOrEmpty com um valor padrão.

Mesmo ao definir um valor padrão, tente não usar valores estáticos. No exemplo a seguir, $env:COMPUTERNAME é usado como o valor padrão, que é automaticamente convertido no nome do computador local se um valor não é fornecido.

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    Write-Output $ComputerName

}

Saída detalhada

Comentários embutidos são úteis se você estiver escrevendo código complexo, mas os usuários não os veem, a menos que examinem o código.

A função mostrada no exemplo anterior tem um comentário embutido no loop foreach. Embora esse comentário específico possa não ser tão difícil de localizar, imagine se a função contivesse centenas de linhas de código.

function Test-MrVerboseOutput {

    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    foreach ($Computer in $ComputerName) {
        #Attempting to perform an action on $Computer <<-- Don't use
        #inline comments like this, use write verbose instead.
        Write-Output $Computer
    }

}

Uma opção melhor é usar Write-Verbose em vez de comentários em linha.

function Test-MrVerboseOutput {

    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    foreach ($Computer in $ComputerName) {
        Write-Verbose -Message "Attempting to perform an action on $Computer"
        Write-Output $Computer
    }

}

A saída verbosa não é exibida quando a função é chamada sem o parâmetro Verbose.

Test-MrVerboseOutput -ComputerName Server01, Server02

A saída detalhada é exibida quando a função é chamada com o parâmetro Verbose.

Test-MrVerboseOutput -ComputerName Server01, Server02 -Verbose

Entrada do pipeline.

Um código extra é necessário quando você deseja que a função aceite a entrada do pipeline. Como mencionado anteriormente neste livro, os comandos podem aceitar a entrada do pipeline por valor (por tipo) ou por nome da propriedade. Você pode criar suas funções como os comandos nativos para que aceitem um ou ambos os tipos de entrada.

Para aceitar a entrada de pipeline por valor, especifique o atributo de parâmetro ValueFromPipeline para esse parâmetro particular. Você só pode aceitar a entrada de pipeline como valor de um parâmetro de cada tipo de dado. Se você tiver dois parâmetros que aceitam entrada de cadeia de caracteres, apenas um deles poderá aceitar entrada de pipeline por valor. Se você especificou por valor para ambos os parâmetros de cadeia de caracteres, a entrada não saberia a qual parâmetro associar. Esse cenário é outra razão pela qual eu chamo esse tipo de entrada de pipeline por tipo, em vez de por valor.

A entrada do pipeline é recebida um item de cada vez, de forma semelhante a como os itens são tratados em um loop de foreach. É necessário um bloco process para processar cada item se a função aceita uma matriz como entrada. Se a função aceitar apenas um único valor como entrada, um bloco process não será necessário, mas é recomendado para fins de consistência.

function Test-MrPipelineInput {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipeline)]
        [string[]]$ComputerName
    )

    process {
        Write-Output $ComputerName
    }

}

Aceitar a entrada do pipeline pelo nome da propriedade é semelhante, exceto se você o especificar com o atributo de parâmetro ValueFromPipelineByPropertyName e pode ser especificado para qualquer número de parâmetros, independentemente do tipo de dados. A chave é que a saída do comando que está sendo canalizado deve ter um nome de propriedade que corresponda ao nome do parâmetro ou a um alias de parâmetro da sua função.

function Test-MrPipelineInput {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

    process {
            Write-Output $ComputerName
    }

}

Os blocos begin e end são opcionais. begin é especificado antes do bloco process e usado para executar qualquer trabalho inicial antes de os itens serem recebidos do pipeline. Os valores que são canalizados para dentro não são acessíveis no bloco de begin. O bloco end é especificado após o bloco process e é usado para limpeza depois que todos os itens canalizados são processados.

Tratamento de erros

A função mostrada no exemplo a seguir gera uma exceção sem tratamento quando um computador não pode ser contatado.

function Test-MrErrorHandling {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipeline,
                   ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

    process {
        foreach ($Computer in $ComputerName) {
            Test-WSMan -ComputerName $Computer
        }
    }

}

Há algumas maneiras diferentes de lidar com erros no PowerShell. Try/Catch é a maneira mais moderna de lidar com erros.

function Test-MrErrorHandling {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipeline,
                   ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

    process {
        foreach ($Computer in $ComputerName) {
            try {
                Test-WSMan -ComputerName $Computer
            }
            catch {
                Write-Warning -Message "Unable to connect to Computer: $Computer"
            }
        }
    }

}

Embora a função mostrada no exemplo anterior use o tratamento de erro, ela gera uma exceção sem tratamento porque o comando não gera um erro que causa encerramento. Somente erros de encerramento são capturados. Especifique o parâmetro ErrorAction com Stop como seu valor para transformar um erro não finalização em um erro encerramento.

function Test-MrErrorHandling {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipeline,
                   ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

    process {
        foreach ($Computer in $ComputerName) {
            try {
                Test-WSMan -ComputerName $Computer -ErrorAction Stop
            }
            catch {
                Write-Warning -Message "Unable to connect to Computer: $Computer"
            }
        }
    }

}

Não modifique a variável $ErrorActionPreference global, a menos que seja absolutamente necessário. Se você o alterar em um escopo local, ele reverterá para o valor anterior quando você sair desse escopo.

Se você estiver usando algo como o .NET diretamente de dentro da função do PowerShell, não poderá especificar o parâmetro ErrorAction no próprio comando. Você pode alterar a variável $ErrorActionPreference pouco antes de chamar o método .NET.

Ajuda baseada em comentários

Adicionar ajuda às funções é considerada uma prática recomendada. A ajuda permite que as pessoas com quem você as compartilhe saibam como usá-las.

function Get-MrAutoStoppedService {

<#
.SYNOPSIS
    Returns a list of services that are set to start automatically, are not
    currently running, excluding the services that are set to delayed start.

.DESCRIPTION
    Get-MrAutoStoppedService is a function that returns a list of services
    from the specified remote computer(s) that are set to start
    automatically, are not currently running, and it excludes the services
    that are set to start automatically with a delayed startup.

.PARAMETER ComputerName
    The remote computer(s) to check the status of the services on.

.PARAMETER Credential
    Specifies a user account that has permission to perform this action. The
    default is the current user.

.EXAMPLE
     Get-MrAutoStoppedService -ComputerName 'Server1', 'Server2'

.EXAMPLE
     'Server1', 'Server2' | Get-MrAutoStoppedService

.EXAMPLE
     Get-MrAutoStoppedService -ComputerName 'Server1' -Credential (Get-Credential)

.INPUTS
    String

.OUTPUTS
    PSCustomObject

.NOTES
    Author:  Mike F. Robbins
    Website: https://mikefrobbins.com
    Twitter: @mikefrobbins
#>

    [CmdletBinding()]
    param (

    )

    #Function Body

}

Quando você adiciona ajuda baseada em comentários às suas funções, a ajuda pode ser recuperada para elas, assim como ocorre com os comandos internos predefinidos.

Toda a sintaxe para escrever uma função no PowerShell pode parecer assustadora para alguém que está começando. Se você não se lembrar da sintaxe de algo, abra uma segunda instância do Ambiente de Script Integrado (ISE) do PowerShell em um monitor separado e exiba o snippet "Cmdlet (função avançada) – Concluir" ao digitar o código das funções. Os trechos de código podem ser acessados no ISE do PowerShell usando a combinação de teclas Ctrl + J.

Resumo

Neste capítulo, você aprendeu as noções básicas de como criar funções no PowerShell, incluindo como:

  • Criar funções avançadas
  • Usar a validação de parâmetro
  • Usar saída detalhada
  • Dar suporte à entrada do pipeline
  • Tratar erros
  • Criar ajuda baseada em comentários

Revisão

  1. Como obter uma lista de verbos aprovados no PowerShell?
  2. Como transformar uma função do PowerShell em uma função avançada?
  3. Em que situações os parâmetros WhatIf e Confirm devem ser adicionados às suas funções do PowerShell?
  4. Como você transforma um erro de não finalização em um de encerramento?
  5. Por que você deve adicionar a ajuda baseada em comentários às suas funções?

Referências