Tudo o que sempre quis saber sobre a instrução switch

Tal como muitas outras linguagens, o PowerShell tem comandos para controlar o fluxo de execução nos scripts. Uma dessas instruções é a instrução switch e, no PowerShell, oferece funcionalidades que não se encontram noutros idiomas. Hoje, vamos aprofundar o trabalho com o PowerShell switch.

Nota

A versão original deste artigo apareceu no blogue escrito por @KevinMarquette. A equipa do PowerShell agradece ao Kevin por partilhar este conteúdo connosco. Veja o blogue dele no PowerShellExplained.com.

A if instrução

Uma das primeiras afirmações que aprende é a if instrução. Permite-lhe executar um bloco de script se uma instrução for $true.

if ( Test-Path $Path )
{
    Remove-Item $Path
}

Pode ter lógicas elseif e else instruções muito mais complicadas. Eis um exemplo em que tenho um valor numérico para o dia da semana e quero obter o nome como uma cadeia.

$day = 3

if ( $day -eq 0 ) { $result = 'Sunday'        }
elseif ( $day -eq 1 ) { $result = 'Monday'    }
elseif ( $day -eq 2 ) { $result = 'Tuesday'   }
elseif ( $day -eq 3 ) { $result = 'Wednesday' }
elseif ( $day -eq 4 ) { $result = 'Thursday'  }
elseif ( $day -eq 5 ) { $result = 'Friday'    }
elseif ( $day -eq 6 ) { $result = 'Saturday'  }

$result
Wednesday

Acontece que este é um padrão comum e há muitas formas de lidar com isto. Um deles é com um switch.

Mudar instrução

A switch instrução permite-lhe fornecer uma variável e uma lista de valores possíveis. Se o valor corresponder à variável, o scriptblock é executado.

$day = 3

switch ( $day )
{
    0 { $result = 'Sunday'    }
    1 { $result = 'Monday'    }
    2 { $result = 'Tuesday'   }
    3 { $result = 'Wednesday' }
    4 { $result = 'Thursday'  }
    5 { $result = 'Friday'    }
    6 { $result = 'Saturday'  }
}

$result
'Wednesday'

Para este exemplo, o valor de $day corresponde a um dos valores numéricos e, em seguida, o nome correto é atribuído a $result. Neste exemplo, estamos apenas a fazer uma atribuição de variáveis, mas qualquer PowerShell pode ser executado nesses blocos de script.

Atribuir a uma variável

Podemos escrever este último exemplo de outra forma.

$result = switch ( $day )
{
    0 { 'Sunday'    }
    1 { 'Monday'    }
    2 { 'Tuesday'   }
    3 { 'Wednesday' }
    4 { 'Thursday'  }
    5 { 'Friday'    }
    6 { 'Saturday'  }
}

Estamos a colocar o valor no pipeline do PowerShell e a atribuí-lo ao $result. Pode fazer o mesmo com as if instruções e foreach .

Predefinição

Podemos utilizar a default palavra-chave para identificar o que deve acontecer se não existir correspondência.

$result = switch ( $day )
{
    0 { 'Sunday' }
    # ...
    6 { 'Saturday' }
    default { 'Unknown' }
}

Aqui, devolvemos o valor Unknown no caso predefinido.

Cadeias

Estava a corresponder números nesses últimos exemplos, mas também podes corresponder às cadeias.

$item = 'Role'

switch ( $item )
{
    Component
    {
        'is a component'
    }
    Role
    {
        'is a role'
    }
    Location
    {
        'is a location'
    }
}
is a role

Decidi não embrulhar as ComponentRole correspondências e Location aspas aqui para realçar que são opcionais. Trata-os switch como uma cadeia na maioria dos casos.

Matrizes

Uma das funcionalidades interessantes do PowerShell switch é a forma como processa as matrizes. Se fornecer uma switch matriz, esta processa cada elemento nessa coleção.

$roles = @('WEB','Database')

switch ( $roles ) {
    'Database'   { 'Configure SQL' }
    'WEB'        { 'Configure IIS' }
    'FileServer' { 'Configure Share' }
}
Configure IIS
Configure SQL

Se tiver itens repetidos na sua matriz, estes serão correspondidos várias vezes pela secção adequada.

PSItem

Pode utilizar o $PSItem ou $_ para referenciar o item atual que foi processado. Quando fazemos uma correspondência simples, $PSItem é o valor que estamos a corresponder. Vou realizar algumas correspondências avançadas na próxima secção onde esta variável é utilizada.

Parâmetros

Uma funcionalidade exclusiva do PowerShell switch é que tem vários parâmetros de comutador que alteram o seu desempenho.

-CaseSensitive

As correspondências não são sensíveis a maiúsculas e minúsculas por predefinição. Se precisar de ser sensível a maiúsculas e minúsculas, pode utilizar -CaseSensitive. Isto pode ser utilizado em combinação com os outros parâmetros de comutador.

-Carateres universais

Podemos ativar o suporte de carateres universais com o -wildcard comutador. Esta ação utiliza a mesma lógica universal que o -like operador para fazer cada correspondência.

$Message = 'Warning, out of disk space'

switch -Wildcard ( $message )
{
    'Error*'
    {
        Write-Error -Message $Message
    }
    'Warning*'
    {
        Write-Warning -Message $Message
    }
    default
    {
        Write-Information $message
    }
}
WARNING: Warning, out of disk space

Aqui, estamos a processar uma mensagem e, em seguida, a transmiti-la em diferentes fluxos com base nos conteúdos.

-Regex

A instrução switch suporta correspondências regex tal como os carateres universais.

switch -Regex ( $message )
{
    '^Error'
    {
        Write-Error -Message $Message
    }
    '^Warning'
    {
        Write-Warning -Message $Message
    }
    default
    {
        Write-Information $message
    }
}

Tenho mais exemplos de utilização do regex noutro artigo que escrevi: As muitas formas de utilizar o regex.

-Ficheiro

Uma funcionalidade pouco conhecida da instrução switch é que pode processar um ficheiro com o -File parâmetro . -file Utilize com um caminho para um ficheiro em vez de lhe dar uma expressão variável.

switch -Wildcard -File $path
{
    'Error*'
    {
        Write-Error -Message $PSItem
    }
    'Warning*'
    {
        Write-Warning -Message $PSItem
    }
    default
    {
        Write-Output $PSItem
    }
}

Funciona como processar uma matriz. Neste exemplo, combino-o com correspondência de carateres universais e utilizo o $PSItem. Isto processaria um ficheiro de registo e converteria-o em mensagens de aviso e erro, consoante as correspondências regex.

Detalhes avançados

Agora que tem conhecimento de todas estas funcionalidades documentadas, podemos utilizá-las no contexto de um processamento mais avançado.

Expressions (Expressões)

O switch pode estar numa expressão em vez de numa variável.

switch ( ( Get-Service | Where status -eq 'running' ).name ) {...}

O que quer que a expressão avalie é o valor utilizado para a correspondência.

Várias correspondências

Pode já ter apanhado isto, mas uma switch pode corresponder a múltiplas condições. Isto é especialmente verdade ao utilizar -wildcard ou -regex corresponder. Pode adicionar a mesma condição várias vezes e todas são acionadas.

switch ( 'Word' )
{
    'word' { 'lower case word match' }
    'Word' { 'mixed case word match' }
    'WORD' { 'upper case word match' }
}
lower case word match
mixed case word match
upper case word match

Todas estas três declarações foram despedidas. Isto mostra que todas as condições são verificadas (por ordem). Isto aplica-se ao processamento de matrizes em que cada item verifica cada condição.

Continuar

Normalmente, é aqui que introduzo a break declaração, mas é melhor aprendermos a usar continue primeiro. Tal como acontece com um foreach ciclo, continue continua para o item seguinte na coleção ou sai do switch se não existirem mais itens. Podemos reescrever esse último exemplo com instruções de continuação para que apenas uma instrução seja executada.

switch ( 'Word' )
{
    'word'
    {
        'lower case word match'
        continue
    }
    'Word'
    {
        'mixed case word match'
        continue
    }
    'WORD'
    {
        'upper case word match'
        continue
    }
}
lower case word match

Em vez de corresponder aos três itens, o primeiro é correspondido e o comutador continua para o valor seguinte. Uma vez que não existem valores para processar, o comutador sai. Este exemplo seguinte mostra como um caráter universal pode corresponder a vários itens.

switch -Wildcard -File $path
{
    '*Error*'
    {
        Write-Error -Message $PSItem
        continue
    }
    '*Warning*'
    {
        Write-Warning -Message $PSItem
        continue
    }
    default
    {
        Write-Output $PSItem
    }
}

Uma vez que uma linha no ficheiro de entrada pode conter a palavra Error e Warning, só queremos que a primeira seja executada e, em seguida, continue a processar o ficheiro.

Divisão

Uma break instrução sai do comutador. Este é o mesmo comportamento que continue apresenta para valores únicos. A diferença é apresentada ao processar uma matriz. break para todo o processamento no comutador e continue passa para o item seguinte.

$Messages = @(
    'Downloading update'
    'Ran into errors downloading file'
    'Error: out of disk space'
    'Sending email'
    '...'
)

switch -Wildcard ($Messages)
{
    'Error*'
    {
        Write-Error -Message $PSItem
        break
    }
    '*Error*'
    {
        Write-Warning -Message $PSItem
        continue
    }
    '*Warning*'
    {
        Write-Warning -Message $PSItem
        continue
    }
    default
    {
        Write-Output $PSItem
    }
}
Downloading update
WARNING: Ran into errors downloading file
write-error -message $PSItem : Error: out of disk space
+ CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException

Neste caso, se atingirmos quaisquer linhas que comecem com Error , obteremos um erro e o comutador para. Isto é o que essa break afirmação está a fazer por nós. Se encontrarmos Error dentro da cadeia e não apenas no início, escrevemos como um aviso. Fazemos o mesmo por Warning. É possível que uma linha possa ter a palavra Error e Warning, mas só precisamos de uma para processar. Isto é o que a continue declaração está a fazer por nós.

Quebrar etiquetas

A switch instrução suporta etiquetas break/continue como foreach.

:filelist foreach($path in $logs)
{
    :logFile switch -Wildcard -File $path
    {
        'Error*'
        {
            Write-Error -Message $PSItem
            break filelist
        }
        'Warning*'
        {
            Write-Error -Message $PSItem
            break logFile
        }
        default
        {
            Write-Output $PSItem
        }
    }
}

Pessoalmente, não gosto do uso de etiquetas de quebra, mas queria apontá-las porque são confusas se nunca as viste antes. Quando tiver múltiplas switch ou foreach instruções aninhadas, poderá querer sair de mais do que o item mais interno. Pode colocar uma etiqueta numa switch que pode ser o destino do seu break.

Enumeração

O PowerShell 5.0 deu-nos números e podemos utilizá-los num comutador.

enum Context {
    Component
    Role
    Location
}

$item = [Context]::Role

switch ( $item )
{
    Component
    {
        'is a component'
    }
    Role
    {
        'is a role'
    }
    Location
    {
        'is a location'
    }
}
is a role

Se quiser manter tudo como números escritos de forma segura, pode colocá-los em parênteses.

switch ($item )
{
    ([Context]::Component)
    {
        'is a component'
    }
    ([Context]::Role)
    {
        'is a role'
    }
    ([Context]::Location)
    {
        'is a location'
    }
}

Os parênteses são necessários aqui para que o comutador não trate o valor [Context]::Location como uma cadeia literal.

ScriptBlock

Podemos utilizar um scriptblock para efetuar a avaliação de uma correspondência, se necessário.

$age = 37

switch ( $age )
{
    {$PSItem -le 18}
    {
        'child'
    }
    {$PSItem -gt 18}
    {
        'adult'
    }
}
'adult'

Isto adiciona complexidade e pode dificultar a sua switch leitura. Na maioria dos casos em que utilizaria algo assim, seria melhor utilizar if e elseif instruções. Consideraria utilizá-lo se já tivesse uma grande mudança e precisasse de dois itens para atingir o mesmo bloco de avaliação.

Uma coisa que acho que ajuda na legibilidade é colocar o scriptblock em parênteses.

switch ( $age )
{
    ({$PSItem -le 18})
    {
        'child'
    }
    ({$PSItem -gt 18})
    {
        'adult'
    }
}

Continua a ser executado da mesma forma e proporciona uma melhor pausa visual ao olhar rapidamente para o mesmo.

Regex $matches

Temos de revisitar o Regex para tocar em algo que não seja imediatamente óbvio. A utilização do regex preenche a $matches variável. $matches Utilizo mais quando falo das muitas formas de utilizar o regex. Segue-se um exemplo rápido para mostrá-lo em ação com correspondências nomeadas.

$message = 'my ssn is 123-23-3456 and credit card: 1234-5678-1234-5678'

switch -regex ($message)
{
    '(?<SSN>\d\d\d-\d\d-\d\d\d\d)'
    {
        Write-Warning "message contains a SSN: $($matches.SSN)"
    }
    '(?<CC>\d\d\d\d-\d\d\d\d-\d\d\d\d-\d\d\d\d)'
    {
        Write-Warning "message contains a credit card number: $($matches.CC)"
    }
    '(?<Phone>\d\d\d-\d\d\d-\d\d\d\d)'
    {
        Write-Warning "message contains a phone number: $($matches.Phone)"
    }
}
WARNING: message may contain a SSN: 123-23-3456
WARNING: message may contain a credit card number: 1234-5678-1234-5678

$null

Pode corresponder a um $null valor que não tem de ser a predefinição.

$values = '', 5, $null
switch ( $values )
{
    $null          { "Value '$_' is `$null" }
    { '' -eq $_ }  { "Value '$_' is an empty string" }
    default        { "Value [$_] isn't an empty string or `$null" }
}
Value '' is an empty string
Value [5] isn't an empty string or $null
Value '' is $null

Ao testar uma cadeia vazia numa switch instrução, é importante utilizar a instrução de comparação, conforme mostrado neste exemplo, em vez do valor ''não processado . Numa switch instrução, o valor '' não processado também corresponde $nulla . Por exemplo:

$values = '', 5, $null
switch ( $values )
{
    $null          { "Value '$_' is `$null" }
    ''             { "Value '$_' is an empty string" }
    default        { "Value [$_] isn't an empty string or `$null" }
}
Value '' is an empty string
Value [5] isn't an empty string or $null
Value '' is $null
Value '' is an empty string

Além disso, tenha cuidado com as devoluções vazias dos cmdlets. Os cmdlets ou pipelines que não têm saída são tratados como uma matriz vazia que não corresponde a nada, incluindo o default caso.

$file = Get-ChildItem NonExistantFile*
switch ( $file )
{
    $null   { '$file is $null' }
    default { "`$file is type $($file.GetType().Name)" }
}
# No matches

Expressão constante

Lee Dailey salientou que podemos usar uma expressão constante $true para avaliar [bool] itens. Imagine se tivermos várias verificações booleanas que precisam de acontecer.

$isVisible = $false
$isEnabled = $true
$isSecure = $true

switch ( $true )
{
    $isEnabled
    {
        'Do-Action'
    }
    $isVisible
    {
        'Show-Animation'
    }
    $isSecure
    {
        'Enable-AdminMenu'
    }
}
Do-Action
Enabled-AdminMenu

Esta é uma forma limpa de avaliar e tomar medidas sobre o estado de vários campos booleanos. O mais interessante é que pode fazer com que uma correspondência inverta o estado de um valor que ainda não foi avaliado.

$isVisible = $false
$isEnabled = $true
$isAdmin = $false

switch ( $true )
{
    $isEnabled
    {
        'Do-Action'
        $isVisible = $true
    }
    $isVisible
    {
        'Show-Animation'
    }
    $isAdmin
    {
        'Enable-AdminMenu'
    }
}
Do-Action
Show-Animation

A definição $isEnabled para $true neste exemplo garante que $isVisible também está definido como $true. Em seguida, quando $isVisible for avaliado, o scriptblock é invocado. Isto é um pouco contra-intuitivo, mas é um uso inteligente da mecânica.

$switch variável automática

Quando o switch está a processar os respetivos valores, cria um enumerador e chama-lhe $switch. Esta é uma variável automática criada pelo PowerShell e pode manipulá-la diretamente.

$a = 1, 2, 3, 4

switch($a) {
    1 { [void]$switch.MoveNext(); $switch.Current }
    3 { [void]$switch.MoveNext(); $switch.Current }
}

Isto dá-lhe os resultados de:

2
4

Ao mover o enumerador para a frente, o item seguinte não é processado pelo switch , mas pode aceder diretamente a esse valor. Chamar-lhe-ia loucura.

Outros padrões

Tabelas hash

Um dos meus cargos mais populares foi o que fiz em hash. Um dos casos de utilização de um hashtable é ser uma tabela de referência. Esta é uma abordagem alternativa a um padrão comum que uma switch instrução está frequentemente a abordar.

$day = 3

$lookup = @{
    0 = 'Sunday'
    1 = 'Monday'
    2 = 'Tuesday'
    3 = 'Wednesday'
    4 = 'Thursday'
    5 = 'Friday'
    6 = 'Saturday'
}

$lookup[$day]
Wednesday

Se estiver a utilizar apenas uma switch pesquisa, utilizo frequentemente uma hashtable .

Enumeração

O PowerShell 5.0 introduziu o Enum e também é uma opção neste caso.

$day = 3

enum DayOfTheWeek {
    Sunday
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
}

[DayOfTheWeek]$day
Wednesday

Podemos passar o dia todo a ver diferentes formas de resolver este problema. Só queria ter a certeza que sabias que tinhas opções.

Palavras finais

A instrução switch é simples na superfície, mas oferece algumas funcionalidades avançadas que a maioria das pessoas não percebe que estão disponíveis. Juntar essas funcionalidades faz com que esta seja uma funcionalidade avançada. Espero que tenha aprendido algo que nunca tinha percebido antes.