Todo lo que le interesa sobre la instrucción if

Al igual que muchos otros lenguajes, PowerShell tiene instrucciones para ejecutar código condicionalmente en los scripts. Una de esas instrucciones es la instrucción If. Hoy vamos a profundizar en uno de los comandos más fundamentales 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.

Ejecución condicional

A menudo, los scripts tienen que tomar decisiones y realizar una lógica diferente en función de esas decisiones. A eso me refiero con "ejecución condicional". Tiene una instrucción o un valor para evaluar y, a continuación, ejecuta una sección diferente de código basada en esa evaluación. Esto es exactamente lo que hace la instrucción if.

Instrucción if

Este es un ejemplo básico de la instrucción if:

$condition = $true
if ( $condition )
{
    Write-Output "The condition was true"
}

Lo primero que hace la instrucción if es evaluar la expresión entre paréntesis. Si se evalúa como $true, ejecuta el valor scriptblock en las llaves. Si el valor fuera $false, se omitiría el bloque de script.

En el ejemplo anterior, la instrucción if simplemente estaba evaluando la variable $condition. Era $true y habría ejecutado el comando Write-Output dentro del bloque de script.

En algunos lenguajes, puede colocar una sola línea de código después de la instrucción if y se ejecuta. Este no es el caso en PowerShell. Debe proporcionar un valor scriptblock completo con llaves para que funcione correctamente.

Operadores de comparación

El uso más común de la instrucción if es comparar dos elementos entre sí. PowerShell tiene operadores especiales para diferentes escenarios de comparación. Cuando se usa un operador de comparación, el valor del lado izquierdo se compara con el valor del lado derecho.

-eq para igualdad

El elemento -eq realiza una comprobación de igualdad entre dos valores para asegurarse de que son iguales entre sí.

$value = Get-MysteryValue
if ( 5 -eq $value )
{
    # do something
}

En este ejemplo, voy a tomar un valor conocido de 5 y a compararlo con $value para ver si coinciden.

Un caso de uso posible es comprobar el estado de un valor antes de realizar una acción en él. Puede obtener un servicio y comprobar que el estado se estaba ejecutando antes de llamar a Restart-Service en él.

Es habitual en otros lenguajes como C# usar == para la igualdad (por ejemplo: 5 == $value), pero eso no funciona con PowerShell. Otro error común es usar el signo igual (por ejemplo: 5 = $value), que está reservado para asignar valores a variables. Al colocar el valor conocido a la izquierda, es más difícil que se produzca este error.

Este operador tiene algunas variaciones (otros también).

  • -eq igualdad sin distinción entre mayúsculas y minúsculas
  • -ieq igualdad sin distinción entre mayúsculas y minúsculas
  • -ceq igualdad con distinción entre mayúsculas y minúsculas

-ne no es igual a

Muchos operadores tienen un operador relacionado que está comprobando el resultado opuesto. -ne comprueba que los valores no son iguales entre sí.

if ( 5 -ne $value )
{
    # do something
}

Úselo para asegurarse de que la acción solo se ejecuta si el valor no es 5. Un buen caso de uso sería verificar si un servicio estaba en ejecución antes de intentar iniciarlo.

Variaciones:

  • -ne sin distinción de mayúsculas y minúsculas, no es igual a
  • -ine sin distinción de mayúsculas y minúsculas, no es igual a
  • -cne con distinción de mayúsculas y minúsculas, no es igual a

Se trata de variaciones inversas de -eq. Agruparé estos tipos al mostrar las variaciones de otros operadores.

-gt -ge -lt -le para mayor que o menor que

Estos operadores se utilizan al comprobar si un valor es mayor o menor que otro valor. -gt -ge -lt -le representa GreaterThan, GreaterThanOrEqual, LessThan y LessThanOrEqual.

if ( $value -gt 5 )
{
    # do something
}

Variaciones:

  • -gt mayor que
  • -igt mayor que, sin distinción de mayúsculas y minúsculas
  • -cgt mayor que, con distinción de mayúsculas y minúsculas
  • -ge mayor que o igual a
  • -ige mayor que o igual a, sin distinción de mayúsculas y minúsculas
  • -cge mayor que o igual a, con distinción de mayúsculas y minúsculas
  • -lt menor que
  • -ilt menor que, sin distinción de mayúsculas y minúsculas
  • -clt menor que, con distinción de mayúsculas y minúsculas
  • -le menor que o igual a
  • -ile menor que o igual a, sin distinción de mayúsculas y minúsculas
  • -cle menor que o igual a, con distinción de mayúsculas y minúsculas

No sé qué le llevaría a usar las opciones que distinguen mayúsculas de minúsculas y las que no las distinguen para estos operadores.

Coincidencias con caracteres comodín -like

PowerShell tiene su propia sintaxis de coincidencia de patrones basada en comodines, que se puede usar con el operador -like. Estos patrones de caracteres comodín son bastante básicos.

  • ? coincide con cualquier carácter individual.
  • * coincide con cualquier número de caracteres.
$value = 'S-ATX-SQL01'
if ( $value -like 'S-*-SQL??')
{
    # do something
}

Es importante señalar que el patrón coincide con toda la cadena. Si necesita buscar coincidencias con algo en el medio de la cadena, debe colocar * en ambos extremos de la cadena.

$value = 'S-ATX-SQL02'
if ( $value -like '*SQL*')
{
    # do something
}

Variaciones:

  • -like carácter comodín sin distinción de mayúsculas y minúsculas
  • -ilike carácter comodín sin distinción de mayúsculas y minúsculas
  • -clike carácter comodín con distinción de mayúsculas y minúsculas
  • -notlike carácter comodín sin distinción de mayúsculas y minúsculas no coincidente
  • -inotlike carácter comodín sin distinción de mayúsculas y minúsculas no coincidente
  • -cnotlike carácter comodín con distinción de mayúsculas y minúsculas no coincidente

Expresión regular -match

El operador -match permite comprobar una cadena para buscar coincidencias basadas en una expresión regular. Úselo cuando considere que los patrones de caracteres comodín no son lo suficientemente flexibles.

$value = 'S-ATX-SQL01'
if ( $value -match 'S-\w\w\w-SQL\d\d')
{
    # do something
}

De forma predeterminada, un patrón regex coincide con cualquier parte de la cadena. Por lo tanto, puede especificar una subcadena que desee que coincida de la siguiente manera:

$value = 'S-ATX-SQL01'
if ( $value -match 'SQL')
{
    # do something
}

Regex es un lenguaje complejo propio y vale la pena analizarlo. Hablo más sobre -match y las muchas maneras de usar regex en otro artículo.

Variaciones:

  • -match regex sin distinción de mayúsculas y minúsculas
  • -imatch regex sin distinción de mayúsculas y minúsculas
  • -cmatch regex con distinción de mayúsculas y minúsculas
  • -notmatch regex sin distinción de mayúsculas y minúsculas no coincidente
  • -inotmatch regex sin distinción de mayúsculas y minúsculas no coincidente
  • -cnotmatch regex con distinción de mayúsculas y minúsculas no coincidente

-is de tipo

Puede comprobar el tipo de un valor con el operador -is.

if ( $value -is [string] )
{
    # do something
}

Puede usarlo si está trabajando con clases o acepta varios objetos a través de la canalización. Puede tener como entrada un servicio o un nombre de servicio. A continuación, compruebe si tiene un servicio y capture el servicio si solo tiene el nombre.

if ( $Service -isnot [System.ServiceProcess.ServiceController] )
{
    $Service = Get-Service -Name $Service
}

Variaciones:

  • -is de tipo
  • -isnot no de tipo

Operadores de recopilación

Cuando se usan los operadores anteriores con un solo valor, el resultado es $true o $false. Esto se trata de forma ligeramente diferente cuando se trabaja con una recopilación. Cada elemento de la recopilación se evalúa y el operador devuelve todos los valores que se evalúan como $true.

PS> 1,2,3,4 -eq 3
3

Esto sigue funcionando correctamente en una instrucción if. Por lo tanto, el operador devuelve un valor y toda la instrucción es $true.

$array = 1..6
if ( $array -gt 3 )
{
    # do something
}

Hay una pequeña trampa oculta en estos detalles que debo señalar. Cuando se utiliza el operador -ne de esta manera, es fácil mirar erróneamente la lógica hacia atrás. El uso de -ne con una recopilación devuelve $true si algún elemento de la recopilación no coincide con su valor.

PS> 1,2,3 -ne 4
1
2
3

Esto puede parecer un truco inteligente, pero tenemos operadores -contains y -in que se ocupan de esto de forma más eficaz. Y -notcontains hace lo que espera.

-contains

El operador -contains busca su valor en la recopilación. En cuanto encuentra una coincidencia, devuelve $true.

$array = 1..6
if ( $array -contains 3 )
{
    # do something
}

Esta es la manera preferida de ver si una recopilación contiene su valor. El uso de Where-Object (o -eq) recorre toda la lista cada vez y es significativamente más lento.

Variaciones:

  • -contains coincidencia sin distinción de mayúsculas y minúsculas
  • -icontains coincidencia sin distinción de mayúsculas y minúsculas
  • -ccontains coincidencia con distinción de mayúsculas y minúsculas
  • -notcontains sin distinción de mayúsculas y minúsculas no coincidente
  • -inotcontains sin distinción de mayúsculas y minúsculas no coincidente
  • -cnotcontains con distinción de mayúsculas y minúsculas no coincidente

-in

El operador -in es igual que el operador -contains, excepto que la recopilación está en el lado derecho.

$array = 1..6
if ( 3 -in $array )
{
    # do something
}

Variaciones:

  • -in coincidencia sin distinción de mayúsculas y minúsculas
  • -iin coincidencia sin distinción de mayúsculas y minúsculas
  • -cin coincidencia con distinción de mayúsculas y minúsculas
  • -notin sin distinción de mayúsculas y minúsculas no coincidente
  • -inotin sin distinción de mayúsculas y minúsculas no coincidente
  • -cnotin con distinción de mayúsculas y minúsculas no coincidente

Operadores lógicos

Los operadores lógicos se utilizan para invertir o combinar otras expresiones.

-not

El operador -not invierte una expresión de $false a $true o de $true a $false. Este es un ejemplo en el que queremos realizar una acción cuando Test-Path es $false.

if ( -not ( Test-Path -Path $path ) )

La mayoría de los operadores de los que hemos hablado tienen una variación en la que no es necesario usar el operador -not. Pero hay ocasiones en las que resulta útil.

! operador

Puede usar ! como alias para -not.

if ( -not $value ){}
if ( !$value ){}

Puede percibir que ! se usa por más gente que proviene de otros lenguajes, como C#. Prefiero escribirlo completo porque me resulta difícil de ver cuando miro rápidamente mis scripts.

-and

Puede combinar expresiones con el operador -and. Al hacerlo, los dos lados deben ser $true para que la expresión completa sea $true.

if ( ($age -gt 13) -and ($age -lt 55) )

En ese ejemplo, $age debe ser 13 o más años para el lado izquierdo y menor que 55 para el lado derecho. He añadido paréntesis adicionales para que el ejemplo estuviera más claro, pero son opcionales dado que la expresión es simple. Este es el mismo ejemplo sin ellos.

if ( $age -gt 13 -and $age -lt 55 )

La evaluación se produce de izquierda a derecha. Si el primer elemento se evalúa como $false, se cierra pronto y no realiza la comparación adecuada. Esto resulta útil cuando es necesario asegurarse de que existe un valor antes de usarlo. Por ejemplo, Test-Path produce un error si se le asigna una ruta $null.

if ( $null -ne $path -and (Test-Path -Path $path) )

-or

-or permite especificar dos expresiones y devuelve $true si cualquiera de ellas es $true.

if ( $age -le 13 -or $age -ge 55 )

Al igual que con el operador -and, la evaluación se produce de izquierda a derecha. Excepto que si la primera parte es $true, toda la declaración es $true y no procesa el resto de la expresión.

Además, fíjese en cómo funciona la sintaxis para estos operadores. Necesita dos expresiones independientes. He visto a los usuarios intentar hacer algo como esto $value -eq 5 -or 6 sin darse cuenta de su error.

-xor OR exclusivo

Este es poco habitual. -xor permite que solo una expresión se evalúe como $true. Por tanto, si los dos elementos son $false o ambos elementos son $true, la expresión completa es $false. Otra forma de verlo es que la expresión es solo $true cuando los resultados de la expresión son diferentes.

Es raro que alguien use este operador lógico y no se me ocurre un buen ejemplo que justifique su uso.

Operadores bit a bit

Los operadores bit a bit realizan cálculos en los bits dentro de los valores y generan un nuevo valor como resultado. Las claves de los operadores bit a bit no se analizan en este artículo, pero aquí los enumeramos.

  • -band AND binario
  • -bor OR binario
  • -bxor OR exclusivo binario
  • -bnot NOT binario
  • -shl desplazar a la izquierda
  • -shr desplazar a la derecha

Expresiones de PowerShell

Podemos usar PowerShell normal dentro de la instrucción de condición.

if ( Test-Path -Path $Path )

Test-Path devuelve $true o $false cuando se ejecuta. Esto también se aplica a los comandos que devuelven otros valores.

if ( Get-Process Notepad* )

Se evalúa como $true si hay un proceso devuelto y $false si no es así. Es perfectamente válido usar expresiones de canalización u otras instrucciones de PowerShell como esta:

if ( Get-Process | Where Name -eq Notepad )

Estas expresiones se pueden combinar entre sí con los operadores -and y -or, pero puede que tenga que usar paréntesis para dividirlas en subexpresiones.

if ( (Get-Process) -and (Get-Service) )

Comprobación de $null

Si no hay ningún resultado o un valor $null se evalúa como $false en la instrucción if. Cuando se comprueba específicamente $null, se recomienda colocar $null en el lado izquierdo.

if ( $null -eq $value )

Hay algunos matices al tratar con valores $null en PowerShell. Si está interesado en profundizar más, hay un artículo sobre todo lo que interesa sobre $null.

Asignación de variables dentro de la condición

Casi me olvido de tratar esta cuestión, menos mal que Prasoon Karunan V me lo ha recordado.

if ($process=Get-Process notepad -ErrorAction ignore) {$process} else {$false}

Normalmente, cuando se asigna un valor a una variable, el valor no se pasa a la canalización o la consola. Cuando se realiza una asignación de variables en una subexpresión, esta se pasa a la canalización.

PS> $first = 1
PS> ($second = 2)
2

¿Ve cómo la asignación de $first no tiene ninguna salida y la asignación de $second sí la tiene? Cuando se realiza una asignación en una instrucción if, se ejecuta exactamente igual que la asignación de $second anterior. Este es un ejemplo claro de cómo podría usarlo:

if ( $process = Get-Process Notepad* )
{
    $process | Stop-Process
}

Si se asigna un valor a $process, la instrucción es $true y $process se detiene.

No confunda esto con -eq, porque no se trata de una comprobación de igualdad. Esta es una característica más confusa que la mayoría de las personas no se dan cuenta de que funciona de este modo.

Asignación de variables desde el bloque de scripts

También puede usar el bloque de scripts de la instrucción if para asignar un valor a una variable.

$discount = if ( $age -ge 55 )
{
    Get-SeniorDiscount
}
elseif ( $age -le 13 )
{
    Get-ChildDiscount
}
else
{
    0.00
}

Cada bloque de scripts escribe los resultados de los comandos, o el valor, como salida. Se puede asignar el resultado de la instrucción if a la variable $discount. En ese ejemplo se pueden asignar fácilmente esos valores a la variable $discount directamente en cada bloque de scripts. No puedo decir que use esto con la instrucción if a menudo, pero tengo un ejemplo donde lo he usado hace poco.

Ruta de acceso de ejecución alternativa

La declaración if le permite especificar una acción no solo cuando la declaración es $true, sino también cuando es $false. Aquí es donde entra en juego la instrucción else.

else

La instrucción else siempre es la última parte de la instrucción if cuando se usa.

if ( Test-Path -Path $Path -PathType Leaf )
{
    Move-Item -Path $Path -Destination $archivePath
}
else
{
    Write-Warning "$path doesn't exist or isn't a file."
}

En este ejemplo, comprobamos $path para asegurarnos de que es un archivo. Si encontramos el archivo, lo movemos. Si no es así, escribimos una advertencia. Este tipo de lógica de bifurcación es muy común.

if anidado

Las instrucciones if y else toman un bloque de script, por lo que podemos colocar cualquier comando de PowerShell dentro de ellas, incluida otra instrucción if. Esto le permite hacer uso de una lógica mucho más complicada.

if ( Test-Path -Path $Path -PathType Leaf )
{
    Move-Item -Path $Path -Destination $archivePath
}
else
{
    if ( Test-Path -Path $Path )
    {
        Write-Warning "A file was required but a directory was found instead."
    }
    else
    {
        Write-Warning "$path could not be found."
    }
}

En este ejemplo, primero probamos la ruta de acceso ideal y luego tomamos medidas al respecto. Si se produce un error, hacemos otra comprobación y proporcionamos información más detallada al usuario.

elseif

No estamos limitados a una única comprobación condicional. Podemos encadenar instrucciones if y else juntas en lugar de anidarlas mediante la instrucción elseif.

if ( Test-Path -Path $Path -PathType Leaf )
{
    Move-Item -Path $Path -Destination $archivePath
}
elseif ( Test-Path -Path $Path )
{
    Write-Warning "A file was required but a directory was found instead."
}
else
{
    Write-Warning "$path could not be found."
}

La ejecución se produce desde la parte superior a la parte inferior. Primero se evalúa la instrucción if superior. Si es $false, se desplaza hacia abajo hasta el siguiente elseif o else de la lista. Esa última else es la acción predeterminada que se realizará si ninguna de las otras devuelven $true.

switch

En este punto, es necesario mencionar la instrucción switch. Proporciona una sintaxis alternativa para realizar varias comparaciones con un valor. Con switch, especifica una expresión y el resultado se compara con varios valores diferentes. Si uno de esos valores coincide, se ejecuta el bloque de código correspondiente. Eche un vistazo a este ejemplo:

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

Existen tres valores posibles que pueden coincidir con $itemType. En este caso, coincide con Role. He usado un ejemplo sencillo para mostrarle un poco del operador switch. Hablo más sobre todo lo que le interesa saber sobre la instrucción switch en otro artículo.

Matriz insertada

Tengo una función llamada Invoke-SnowSql que inicia un ejecutable con varios argumentos de la línea de comandos. Aquí hay un clip de esa función donde compilo la matriz de argumentos.

$snowSqlParam = @(
    '--accountname', $Endpoint
    '--username', $Credential.UserName
    '--option', 'exit_on_error=true'
    '--option', 'output_format=csv'
    '--option', 'friendly=false'
    '--option', 'timing=false'
    if ($Debug)
    {
        '--option', 'log_level=DEBUG'
    }
    if ($Path)
    {
        '--filename', $Path
    }
    else
    {
        '--query', $singleLineQuery
    }
)

Las variables $Debug y $Path son parámetros de la función que proporciona el usuario final. Los evaluamos insertados dentro de la inicialización de la matriz. Si $Debug es verdadero, esos valores se ubican en $snowSqlParam en el lugar correcto. Lo mismo se aplica a la variable $Path.

Simplificación de las operaciones complejas

Es inevitable que se produzca una situación en la que tenga demasiadas comparaciones que comprobar y que la instrucción If se desplace fuera del lado derecho de la pantalla.

$user = Get-ADUser -Identity $UserName
if ( $null -ne $user -and $user.Department -eq 'Finance' -and $user.Title -match 'Senior' -and $user.HomeDrive -notlike '\\server\*' )
{
    # Do Something
}

Pueden resultar difíciles de leer, por lo que es más propenso a cometer errores. Podemos tomar algunas medidas al respecto.

Continuación de línea

Algunos operadores de PowerShell permiten ajustar el comando a la línea siguiente. Los operadores lógicos -and y -or son buenos operadores para usar en caso de que desee dividir la expresión en varias líneas.

if ($null -ne $user -and
    $user.Department -eq 'Finance' -and
    $user.Title -match 'Senior' -and
    $user.HomeDrive -notlike '\\server\*'
)
{
    # Do Something
}

Sigue siendo complejo, pero colocar cada pieza en su propia línea marca una gran diferencia. Normalmente recurro a esta opción cuando tengo más de dos comparaciones o si tengo que desplazarme hacia la derecha para leer alguna de las lógicas.

Cálculo previo de los resultados

Podemos sacar la instrucción de la instrucción if y comprobar solo el resultado.

$needsSecureHomeDrive = $null -ne $user -and
    $user.Department -eq 'Finance' -and
    $user.Title -match 'Senior' -and
    $user.HomeDrive -notlike '\\server\*'

if ( $needsSecureHomeDrive )
{
    # Do Something
}

Tiene un aspecto mucho más limpio que el ejemplo anterior. También tiene la oportunidad de usar un nombre de variable que explique lo que está comprobando realmente. Este también es un ejemplo de código autodocumentado que ahorra comentarios innecesarios.

Varias instrucciones if

Podemos dividir esto en varias instrucciones y comprobarlas de una en una. En este caso, usamos una marca o una variable de seguimiento para combinar los resultados.


$skipUser = $false

if( $null -eq $user )
{
    $skipUser = $true
}

if( $user.Department -ne 'Finance' )
{
    Write-Verbose "isn't in Finance department"
    $skipUser = $true
}

if( $user.Title -match 'Senior' )
{
    Write-Verbose "Doesn't have Senior title"
    $skipUser = $true
}

if( $user.HomeDrive -like '\\server\*' )
{
    Write-Verbose "Home drive already configured"
    $skipUser = $true
}

if ( -not $skipUser )
{
    # do something
}

Tuve que invertir la lógica para que la lógica de la marca funcionara correctamente. Cada evaluación es una instrucción if individual. La ventaja de esto es que al depurar puede saber exactamente lo que está haciendo la lógica. También pude agregar un nivel de detalle mucho mayor.

La desventaja obvia es que es mucho más código que escribir. El código es más complejo de ver porque toma una sola línea de lógica y la expande en 25 o más líneas.

Uso de funciones

También se puede trasladar toda la lógica de validación a una función. Fíjese qué limpio se ve esto de un vistazo.

if ( Test-SecureDriveConfiguration -ADUser $user )
{
    # do something
}

Todavía tiene que crear la función para realizar la validación, pero de este modo es mucho más fácil trabajar con el código. Y también es más fácil de probar. En las pruebas, puede simular la llamada a Test-ADDriveConfiguration y solo necesita dos pruebas para esta función. Una en la que devuelve $true y otra en la que devuelve $false. La prueba de la otra función es más sencilla porque es muy pequeña.

El cuerpo de esa función aún podría ser esa línea única con la que comenzamos o la lógica explotada que usamos en la última sección. Esto funciona bien en ambos escenarios y le permite cambiar fácilmente esa implementación más adelante.

Control de errores

Un uso importante de la instrucción if es comprobar las condiciones de error antes de que se produzcan errores. Un buen ejemplo es comprobar si ya existe una carpeta antes de intentar crearla.

if ( -not (Test-Path -Path $folder) )
{
    New-Item -Type Directory -Path $folder
}

Me gustaría decir que, si espera que se produzca una excepción, esta no es realmente una excepción. Compruebe los valores y valide las condiciones donde pueda hacerlo.

Si desea profundizar un poco más en el control de excepciones real, tengo un artículo sobre lo que le interesa saber sobre las excepciones.

Conclusiones

La instrucción if es una instrucción bastante sencilla, pero es una parte fundamental de PowerShell. Tendrá que usarla varias en casi todos los scripts que escriba. Espero que ahora la conozca un poco mejor.