Nesta aula, vamos construir um módulo PowerShell completo e reutilizável, aplicando as melhores práticas de estruturação, documentação e testes. Um módulo bem projetado não apenas organiza seu código, mas também facilita a manutenção, o compartilhamento e a automação de tarefas repetitivas. Você verá na prática como transformar funções soltas em um pacote coeso, pronto para ser distribuído e usado em diversos projetos.

O projeto que criaremos será um módulo chamado MeuModulo, com funções utilitárias para manipulação de arquivos e logs. Ao longo da aula, exploraremos cada etapa: desde a estrutura de pastas até a validação com testes automatizados. Ao final, você terá um módulo funcional e um roteiro claro para criar seus próprios módulos.

Estrutura

A estrutura de um módulo PowerShell é fundamental para sua reutilização e manutenção. Um módulo típico possui uma pasta raiz com o nome do módulo, contendo um manifesto (.psd1) e um ou mais arquivos de script (.psm1). A organização interna pode ser simples ou escalável, dependendo da complexidade.

Para nosso projeto, adotaremos uma estrutura modular que separa funções em arquivos individuais dentro de uma pasta Functions. Isso facilita a localização de código e permite que cada função seja desenvolvida e testada de forma isolada. A estrutura final será:

MeuModulo/
├── MeuModulo.psd1
├── MeuModulo.psm1
├── Functions/
│   ├── Get-ArquivoInfo.ps1
│   ├── Write-Log.ps1
│   └── New-Pasta.ps1
└── Tests/
    └── MeuModulo.Tests.ps1

O manifesto .psd1 define metadados, versão, autor e as funções exportadas. O .psm1 é o script principal que carrega as funções. Vamos criar o manifesto com o cmdlet New-ModuleManifest e depois editá-lo para incluir as funções exportadas.

New-ModuleManifest -Path .\MeuModulo\MeuModulo.psd1 -RootModule MeuModulo.psm1 -ModuleVersion "1.0.0" -Author "Seu Nome" -Description "Módulo de utilitários para automação"

O arquivo .psm1 deve carregar os arquivos de funções. Podemos usar Get-ChildItem para importar dinamicamente. Exemplo:

# MeuModulo.psm1
$functionsPath = Join-Path $PSScriptRoot 'Functions'
Get-ChildItem -Path $functionsPath -Filter '*.ps1' | ForEach-Object {
    . $_.FullName
}

Dessa forma, todas as funções definidas nos arquivos da pasta Functions estarão disponíveis no módulo. A estrutura modular facilita a adição de novas funções sem alterar o arquivo principal.

Funções exportadas

No PowerShell, as funções definidas em um módulo não são automaticamente expostas ao usuário. Precisamos exportá-las explicitamente no manifesto ou no próprio script. A prática recomendada é listar as funções no manifesto para controlar a superfície pública do módulo.

No manifesto .psd1, a chave FunctionsToExport controla isso. Podemos especificar uma lista ou usar * para exportar todas, mas é melhor ser explícito. Vamos editar o manifesto para exportar nossas três funções:

FunctionsToExport = @('Get-ArquivoInfo', 'Write-Log', 'New-Pasta')

Agora, vamos implementar as funções. Exemplo de Get-ArquivoInfo:

function Get-ArquivoInfo {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path
    )
    if (Test-Path $Path) {
        Get-Item $Path | Select-Object Name, Length, LastWriteTime
    } else {
        Write-Warning "Arquivo não encontrado: $Path"
    }
}

E Write-Log:

function Write-Log {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Message,
        [string]$LogPath = ".\app.log"
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    Add-Content -Path $LogPath -Value "[$timestamp] $Message"
}

Além disso, New-Pasta:

function New-Pasta {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path
    )
    if (-not (Test-Path $Path)) {
        New-Item -ItemType Directory -Path $Path -Force
    } else {
        Write-Warning "A pasta $Path já existe."
    }
}

Essas funções são simples, mas demonstram o padrão: parâmetros com validação, uso de cmdlets nativos e mensagens de aviso. Ao exportar explicitamente, evitamos expor funções internas auxiliares que possam poluir o namespace do usuário.

Ajuda

Documentação é essencial para qualquer módulo reutilizável. O PowerShell suporta comentários de ajuda baseados em # .SYNOPSIS, # .DESCRIPTION, # .PARAMETER e # .EXAMPLE. Esses comentários são exibidos com Get-Help e podem ser convertidos em arquivos de ajuda externos, mas o mais simples é incorporá-los nas funções.

Vamos adicionar ajuda à função Get-ArquivoInfo:

function Get-ArquivoInfo {
    <#
    .SYNOPSIS
        Obtém informações de um arquivo.
    .DESCRIPTION
        Retorna nome, tamanho e data de modificação do arquivo especificado.
    .PARAMETER Path
        Caminho completo do arquivo.
    .EXAMPLE
        Get-ArquivoInfo -Path "C:\temp\arquivo.txt"
    #>
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path
    )
    # ...
}

Da mesma forma, adicione ajuda a Write-Log e New-Pasta. Após importar o módulo, o usuário pode executar Get-Help Get-ArquivoInfo e ver a documentação. Isso melhora a experiência e reduz a curva de aprendizado.

Além dos comentários, podemos gerar um arquivo de ajuda externo (.xml) usando o cmdlet New-HelpFile (disponível no módulo Plaster ou manualmente), mas para este projeto os comentários são suficientes.

Testes

Testes automatizados garantem que o módulo se comporte como esperado e facilitam a detecção de regressões. O PowerShell não possui uma estrutura de testes nativa, mas podemos usar o módulo Pester, que é o padrão de fato. Instale o Pester com Install-Module Pester -Force.

Vamos criar um arquivo de teste MeuModulo.Tests.ps1 na pasta Tests. O Pester usa blocos Describe, Context e It para definir os testes. Exemplo:

# Tests\MeuModulo.Tests.ps1
BeforeAll {
    Import-Module .\MeuModulo\MeuModulo.psd1 -Force
}

Describe "Get-ArquivoInfo" {
    It "Retorna informações de um arquivo existente" {
        $tempFile = New-TemporaryFile
        $result = Get-ArquivoInfo -Path $tempFile.FullName
        $result.Name | Should -Be $tempFile.Name
        Remove-Item $tempFile
    }

    It "Emite aviso para arquivo inexistente" {
        { Get-ArquivoInfo -Path "C:\nao\existe.txt" } | Should -Throw
    }
}

Describe "Write-Log" {
    It "Adiciona mensagem ao arquivo de log" {
        $logFile = Join-Path $TestDrive "test.log"
        Write-Log -Message "Teste" -LogPath $logFile
        (Get-Content $logFile).Count | Should -Be 1
    }
}

Executamos os testes com Invoke-Pester:

Invoke-Pester -Script .\Tests\MeuModulo.Tests.ps1

O Pester fornece saída colorida indicando quais testes passaram ou falharam. Escrever testes para todas as funções é uma boa prática, e você pode expandir este exemplo para cobrir mais cenários, como parâmetros inválidos e casos de borda.

Boas práticas e observações finais

Ao criar módulos, considere as seguintes boas práticas:

  • Use nomes de funções no padrão Verbo-Substantivo (aprovados pelo PowerShell).
  • Exporte apenas o que for necessário, mantendo funções auxiliares internas.
  • Documente com comentários de ajuda e, se possível, gere arquivos de ajuda externos.
  • Estruture o código em pastas para facilitar a navegação e manutenção.
  • Escreva testes com Pester para garantir a qualidade.
  • Versionize o módulo e publique em um repositório (ex.: PowerShell Gallery) para compartilhar.

Lembre-se de que um módulo bem projetado é um investimento: você economizará tempo no futuro ao reutilizar código confiável e documentado. Experimente adicionar novas funções e testes ao seu módulo para solidificar o aprendizado.

Referências

Exercícios

  1. Exercício 1: Crie a estrutura de pastas para um módulo chamado MeuModulo com as pastas Functions e Tests. Dentro de Functions, crie um arquivo vazio para uma função chamada Get-Saudacao. Use comandos PowerShell para criar a estrutura.
  2. ✓ Resposta:
    New-Item -Path .\MeuModulo\Functions -ItemType Directory -Force
    New-Item -Path .\MeuModulo\Tests -ItemType Directory -Force
    New-Item -Path .\MeuModulo\Functions\Get-Saudacao.ps1 -ItemType File -Force
    
  3. Exercício 2: No manifesto do módulo, configure FunctionsToExport para exportar apenas as funções Get-ArquivoInfo e Write-Log. Suponha que o manifesto já exista.
  4. ✓ Resposta: Edite o manifesto .psd1 e altere a linha:
    FunctionsToExport = @('Get-ArquivoInfo', 'Write-Log')
    
  5. Exercício 3: Adicione comentários de ajuda (SYNOPSIS, DESCRIPTION, PARAMETER, EXAMPLE) à função Write-Log.
  6. ✓ Resposta:
    function Write-Log {
        <#
        .SYNOPSIS
            Adiciona uma mensagem a um arquivo de log.
        .DESCRIPTION
            Escreve uma mensagem com timestamp no arquivo especificado.
        .PARAMETER Message
            Texto da mensagem a ser registrada.
        .PARAMETER LogPath
            Caminho do arquivo de log. Padrão é .\app.log.
        .EXAMPLE
            Write-Log -Message "Início do processo" -LogPath "C:\logs\app.log"
        #>
        param(
            [Parameter(Mandatory=$true)]
            [string]$Message,
            [string]$LogPath = ".\app.log"
        )
        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        Add-Content -Path $LogPath -Value "[$timestamp] $Message"
    }
    
  7. Exercício 4: Escreva um teste Pester para a função New-Pasta que verifica se a pasta é criada e se um aviso é emitido quando a pasta já existe.
  8. ✓ Resposta:
    Describe "New-Pasta" {
        It "Cria uma nova pasta" {
            $testPath = Join-Path $TestDrive "nova"
            New-Pasta -Path $testPath
            Test-Path $testPath | Should -Be $true
        }
        It "Emite aviso se a pasta já existe" {
            $testPath = Join-Path $TestDrive "existente"
            New-Item -ItemType Directory -Path $testPath -Force | Out-Null
            { New-Pasta -Path $testPath } | Should -Throw
        }
    }
    
  9. Exercício 5: Execute os testes do módulo com Pester e explique o que a saída indica.
  10. ✓ Resposta: Execute Invoke-Pester -Script .\Tests\MeuModulo.Tests.ps1. A saída mostra o número de testes executados, quantos passaram e quantos falharam, além de detalhes de cada falha. Uma saída verde indica sucesso, enquanto vermelho indica problemas. Isso ajuda a validar o comportamento do módulo.