Todo lo que le interesa sobre la instrucción switch

Al igual que muchos otros lenguajes, PowerShell tiene comandos para controlar el flujo de ejecución dentro de los scripts. Una de esas instrucciones es switch, que, en PowerShell, ofrece características que no existen en otros lenguajes. Hoy, vamos a profundizar en cómo trabajar con switch de PowerShell.

Nota

La versión original de este artículo apareció en el blog escrito por @KevinMarquette. El equipo de PowerShell agradece a Kevin que comparta este contenido con nosotros. Visite su blog en PowerShellExplained.com.

Instrucción if

Una de las primeras instrucciones que se aprenden es if. Permite ejecutar un bloque de script si una instrucción es $true.

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

Puede tener una lógica mucho más complicada mediante las instrucciones elseif y else. A continuación, se muestra un ejemplo en el que tengo un valor numérico para el día de la semana y quiero obtener el nombre como una cadena.

$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

Se trata de un patrón común y hay muchas maneras de abordarlo. Una de ellas es con switch.

Instrucción switch

La instrucción switch permite proporcionar una variable y una lista de valores posibles. Si el valor coincide con la variable, se ejecuta su bloque de script.

$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'

En este ejemplo, el valor de $day coincide con uno de los valores numéricos y, después, se asigna el nombre correcto a $result. En este ejemplo, solo se realiza la asignación a una variable, pero cualquier comando de PowerShell puede ejecutarse en esos bloques de script.

Asignación a una variable

Podemos escribir ese último ejemplo de otra manera.

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

Se coloca el valor en la canalización de PowerShell y se asigna a $result. Puede hacer lo mismo con las instrucciones if y foreach.

Valor predeterminado

Podemos usar la palabra clave default para identificar qué debe ocurrir si no hay ninguna coincidencia.

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

Aquí se devuelve el valor Unknown en el caso predeterminado.

Cadenas

Creé correspondencias con los números en los últimos ejemplos, pero también se puede establecer una coincidencia entre cadenas.

$item = 'Role'

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

Decidí no encapsular aquí las coincidencias Component, Role y Location entre comillas para resaltar que son opcionales. switch las trata como una cadena en la mayoría de los casos.

Matrices

Una de las características interesantes de la instrucción switch de PowerShell es la forma en que trata las matrices. Si proporciona una matriz a una instrucción switch, esta procesa cada elemento de dicha colección.

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

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

Si hay elementos repetidos en la matriz, la sección correspondiente establece coincidencias entre ellos varias veces.

PSItem

Puede usar $PSItem o $_ para hacer referencia al elemento actual que se procesó. Cuando establecemos una coincidencia simple, $PSItem es el valor con el que se crea la coincidencia. Realizaré algunas coincidencias avanzadas en la sección siguiente donde se usa esta variable.

Parámetros

Una característica exclusiva de switch de PowerShell es que tiene una serie de Parámetros de modificador que cambian la forma en que se ejecuta.

-CaseSensitive

Las coincidencias no distinguen mayúsculas de minúsculas de forma predeterminada. Si necesita que se distingan mayúsculas de minúsculas, puede usar -CaseSensitive. Se puede utilizar en combinación con los otros parámetros switch.

-Wildcard

Se puede habilitar la compatibilidad con los caracteres comodín mediante el modificador -wildcard. Usa la misma lógica del carácter comodín que el operador -like para establecer cada coincidencia.

$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

Aquí se procesa un mensaje y, después, se transmite en secuencias diferentes en función del contenido.

-Regex

La instrucción switch admite las coincidencias regex del mismo modo que los caracteres comodín.

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

Hay más ejemplos de uso de regex en otro artículo que escribí: Las distintas formas de usar regex.

-File

Una característica poco conocida de la instrucción switch es que puede procesar un archivo con el parámetro -File. Utilice -file con una ruta de acceso a un archivo, en lugar de asignarle una expresión variable.

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

Funciona igual que el procesamiento de una matriz. En este ejemplo, se combina con la coincidencia con caracteres comodín y se usa $PSItem. Esto procesaría un archivo de registro y lo convertiría en mensajes de advertencia y error en función de las coincidencias regex.

Detalles avanzados

Ahora que conoce todas estas características documentadas, podemos usarlas en el contexto de un procesamiento más avanzado.

Expresiones

switch puede estar en una expresión en lugar de en una variable.

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

Cualquiera que sea la expresión evaluada, será el valor que se utilizará para la coincidencia.

Varias coincidencias

Puede que ya lo sepa, pero switch puede coincidir con varias condiciones. Esto sucede especialmente cuando se usan las coincidencias -wildcard o -regex. Puede agregar la misma condición varias veces, y todas se activarán.

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

Estas tres instrucciones se activan. Esto muestra que se comprueba cada condición (en orden). Esto se aplica al procesamiento de matrices en las que cada elemento comprueba cada condición.

Continuar

Normalmente, aquí es donde se introduciría la instrucción break, pero es mejor aprender a usar continue primero. Al igual que con un bucle foreach, continue continúa en el siguiente elemento de la colección o sale de switch si no hay más elementos. Podemos reescribir ese último ejemplo con instrucciones continue, para que solo se ejecute una instrucción.

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

En lugar de buscar coincidencias con los tres elementos, se establece una coincidencia con el primero, y el modificador continúa hasta el valor siguiente. Dado que ya no queda ningún valor por procesar, el modificador se cierra. En el ejemplo siguiente se muestra cómo un carácter comodín podría coincidir con varios elementos.

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

Dado que una línea del archivo de entrada podría contener las palabras Error y Warning, solo deseamos que se ejecute la primera y que, después, se siga procesando el archivo.

Break

Una instrucción break cierra el modificador. Este es el mismo comportamiento que continue presenta para los valores únicos. La diferencia se muestra al procesar una matriz. break detiene todo el procesamiento del modificador y continue pasa al siguiente elemento.

$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

En este caso, si se alcanzan las líneas que comienzan con Error, se obtiene un error y el modificador se detiene. Esto es lo que hace la instrucción break. Si encontramos Error dentro de la cadena y no solo al principio, lo escribimos como una advertencia. Haremos lo mismo con Warning. Es posible que una línea pueda tener las palabras Error y Warning, pero solo se necesita una para procesar. Esto es lo que hace la instrucción continue.

Etiquetas de interrupción

La instrucción switch admite 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
        }
    }
}

Personalmente, no me gusta usar las etiquetas de interrupción, pero quería destacarlas porque son confusas si no se han visto antes. Si tiene varias instrucciones switch o foreach anidadas, puede que quiera ir más allá del elemento más interno. Puede colocar una etiqueta en switch que puede ser el destino de break.

Enum

PowerShell 5.0 proporcionó enumeraciones que podemos usar en un modificador.

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

Si desea conservarlas todas como enumeraciones fuertemente tipadas, puede colocarlas entre paréntesis.

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

Los paréntesis son necesarios aquí para que el modificador no trate el valor [Context]::Location como una cadena literal.

Bloque de script

Se puede usar un bloque de script para realizar la evaluación de una coincidencia, si es necesario.

$age = 37

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

Esto agrega complejidad y puede hacer que switch sea difícil de leer. En la mayoría de los casos en los que usaría algo parecido a esto, sería mejor usar instrucciones if y elseif. Sería conveniente usar esto si ya existiera un modificador grande y se necesitaran dos elementos para alcanzar el mismo bloque de evaluación.

Un aspecto que considero que ayuda con la legibilidad es colocar el bloque de script entre paréntesis.

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

Todavía se ejecuta de la misma manera y ofrece una mejor interrupción visual cuando se consulta rápidamente.

$matches en regex

Es necesario revisar la información relativa a regex para tratar algo que, a primera vista, no resulta obvio. El uso de regex rellena la variable $matches. Voy a usar $matches más cuando hablo de Las distintas formas de usar regex. A continuación, se muestra un ejemplo rápido para mostrarlo en acción mediante coincidencias con nombre.

$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

Puede establecer una coincidencia con un valor $null que no tenga que ser el predeterminado.

$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

Al probar una cadena vacía en una instrucción switch, es importante usar la instrucción de comparación como se muestra en este ejemplo en lugar del valor sin formato ''. En una instrucción switch, el valor sin formato '' también coincide con $null. Por ejemplo:

$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

Además, debe tener cuidado con las devoluciones vacías de los cmdlets. Los cmdlets o las canalizaciones que no tienen salida se tratan como una matriz vacía que no coincide con nada, incluido el caso default.

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

Expresión constante

Lee Dailey señaló que se puede usar una expresión $true constante para evaluar los elementos [bool]. Imagine que hay varias comprobaciones booleanas que deben realizarse.

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

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

Se trata de una forma sencilla de evaluar y tomar medidas en relación con el estado de varios campos booleanos. Lo más interesante en este sentido es que puede haber una variable que cambie el estado de un valor que aún no se ha evaluado.

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

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

Al establecer $isEnabled en $true, se garantiza que $isVisible también se establezca en $true. Después, cuando se evalúa $isVisible, se invoca su bloque de script. Esto resulta algo contradictorio, pero supone un uso inteligente de la mecánica.

Variable automática $switch

Cuando switch procesa sus valores, crea un enumerador y lo llama $switch. Se trata de una variable automática creada por PowerShell, que se puede manipular directamente.

$a = 1, 2, 3, 4

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

Aquí se presentan los resultados de:

2
4

Al avanzar con el enumerador, switch no procesa el siguiente elemento, pero se puede acceder a dicho valor directamente. Yo lo consideraría una locura.

Otros patrones

Tablas hash

Una de mis publicaciones más populares es la que hice sobre tablas hash. Uno de los casos de uso de hashtable es que se trate de una tabla de búsqueda. Se trata de un enfoque alternativo a un patrón común que una instrucción switch suele abordar.

$day = 3

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

$lookup[$day]
Wednesday

Si solo utilizo switch como una búsqueda, a menudo uso hashtable en su lugar.

Enum

PowerShell 5.0 presentó Enum y también es una opción en este caso.

$day = 3

enum DayOfTheWeek {
    Sunday
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
}

[DayOfTheWeek]$day
Wednesday

Podríamos pasar todo el día examinando diferentes formas de solucionar este problema. Solo quería asegurarme de que sabía que tenía opciones.

Conclusiones

La instrucción switch es sencilla a primera vista, pero ofrece algunas características avanzadas que la mayoría de las personas no saben que están disponibles. La combinación de esas características convierte esto en una característica eficaz. Espero que haya aprendido algo que antes no sabía.