Capítulo 4. Bucle y control de flujo

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

4.0 Introducción

En empiezas a escribir scripts o comandos que interactúan con datos desconocidos, por lo que los conceptos de bucle y control de flujo cobran cada vez más importancia.

Las sentencias y comandos de bucle de PowerShell te permiten realizar una operación (o conjunto de operaciones) sin tener que repetir los propios comandos. Esto incluye, por ejemplo, hacer algo un número determinado de veces, procesar cada elemento de una colección o trabajar hasta que se cumpla una determinada condición.

Las sentencias de control de flujo y comparación de PowerShell te permiten adaptar tu script o comando a datos desconocidos. Te permiten ejecutar comandos en función del valor de esos datos, omitir comandos en función del valor de esos datos, etc.

Juntas, las sentencias de bucle y de control de flujo añaden una gran versatilidad a tu caja de herramientasPowerShell.

4.1 Toma decisiones con operadores de comparación y lógicos

Problema

En quieres comparar unos datos con otros y tomar una decisión basándote en esa comparación.

Solución

Utiliza los operadores lógicos de PowerShell para comparar datos y tomar decisiones basadas en ellos:

Operadores de comparación

-eq, -ne, -ge, -gt, -in, -notin, -lt, -le, -like, -notlike, -match, -notmatch,-contains, -notcontains, -is, -isnot

Operadores lógicos

-and, -or, -xor, -not

Para una descripción detallada (y ejemplos) de estos operadores, consulta "Operadores de comparación".

Debate

Los operadores lógicos y de comparación de PowerShell te permiten comparar datos o comprobar si cumplen alguna condición. Un operador compara dos datos (operador binario ) o comprueba un dato (operador unario ). Todos los operadores de comparación son binarios (comparan dos datos), al igual que la mayoría de los operadores lógicos. El único operador lógico unario es el operador -not, que devuelve el true/false opuesto a los datos que comprueba.

Los operadores de comparación comparan dos datos y devuelven un resultado que depende del operador de comparación concreto. Por ejemplo, puedes querer comprobar si una colección tiene al menos un determinado número de elementos:

PS > (dir).Count -ge 4
True

o comprobar si una cadena coincide con una expresión regular dada:

PS > "Hello World" -match "H.*World"
True

La mayoría de los operadores de comparación también se adaptan al tipo de su entrada. Por ejemplo, cuando los aplicas a datos simples como una cadena, los operadores de comparación -like y -match determinan si la cadena coincide con el patrón especificado. Cuando los aplicas a una colección de datos simples, esos mismos operadores de comparación devuelven todos los elementos de esa colección que coincidan con el patrón que proporciones.

Nota

El operador -match toma como argumento una expresión regular. Uno de los símbolos de expresión regular más comunes es el carácter $, que representa el final de línea. Sin embargo, ¡el carácter $ también representa el inicio de una variable PowerShell! Para evitar que PowerShell interprete los caracteres como términos de lenguaje o secuencias de escape, coloca la cadena entre comillas simples en lugar de entre comillas dobles:

PS > "Hello World" -match "Hello"
True
PS > "Hello World" -match 'Hello$'
False

Por defecto en, los operadores de comparación de PowerShell no distinguen entre mayúsculas y minúsculas. Para utilizar las versiones que distinguen mayúsculas de minúsculas, ponles como prefijo el carácter c:

-ceq, -cne, -cge, -cgt, -cin, -clt, -cle, -clike, -cnotlike,
-cmatch, -cnotmatch, -ccontains, -cnotcontains

Para una descripción detallada de los operadores de comparación, sus homólogos que distinguen mayúsculas de minúsculas y cómo se adaptan a su entrada, consulta "Operadores de comparación".

Los operadores lógicos combinan sentencias true o false y devuelven un resultado que depende del operador lógico específico. Por ejemplo, puede que quieras comprobar si una cadena coincide con el patrón comodín que proporciones y que tiene más de un determinado número de caracteres:

PS > $data = "Hello World"
PS > ($data -like "*llo W*") -and ($data.Length -gt 10)
True
PS > ($data -like "*llo W*") -and ($data.Length -gt 20)
False

Algunos de los operadores de comparación incorporan en realidad aspectos de los operadores lógicos. Dado que utilizar el opuesto de una comparación (como -like) es tan habitual, PowerShell proporciona operadores de comparación (como -notlike) que te ahorran tener que utilizar explícitamente el operador -not.

Para una descripción detallada de cada uno de los operadores lógicos, consulta "Operadores de comparación".

Los operadores de comparación y los operadores lógicos (cuando se combinan con sentencias de control de flujo) forman el núcleo de cómo escribimos un script o comando que se adapta a sus datos y a su entrada.

Consulta también "Declaraciones condicionales" para obtener información detallada sobre estas declaraciones.

Para más información sobre los operadores de PowerShell, escribe Get-Help about_Operators.

4.2 Ajustar el flujo del script mediante sentencias condicionales

Problema

En quieres controlar las condiciones en las que PowerShell ejecuta comandos o partes de tu script.

Solución

Utiliza Las sentencias condicionales de PowerShell if, elseif, y else para controlar el flujo de ejecución en tu script.

Por ejemplo:

$temperature = 90

if($temperature -le 0)
{
   "Balmy Canadian Summer"
}
elseif($temperature -le 32)
{
   "Freezing"
}
elseif($temperature -le 50)
{
   "Cold"
}
elseif($temperature -le 70)
{
   "Warm"
}
else
{
   "Hot"
}

Debate

Las declaraciones condicionales incluyen lo siguiente:

if declaración

Ejecuta el bloque de secuencia de comandos que le sigue si su condición se evalúa como true

elseif declaración

Ejecuta el bloque de secuencia de comandos que le sigue si su condición se evalúa como true y ninguna de las condiciones de las sentencias if o elseif que le preceden se evalúan como true

else declaración

Ejecuta el bloque de secuencia de comandos que le sigue si ninguna de las condiciones de las sentencias if o elseif que le preceden se evalúan como true

En además de ser útiles para el flujo de control del script, las sentencias condicionales suelen ser una forma útil de asignar datos a una variable. PowerShell lo hace muy fácil permitiéndote asignar los resultados de una sentencia condicional directamente a una variable:

$result = if(Get-Process -Name notepad) { "Running" } else { "Not running" }

Para sentencias condicionales muy sencillas como ésta, también puedes utilizar el operador ternario de PowerShell:

$result = (Get-Process -Name notepad*) ? "Running" : "Not running"

Para más información sobre estas sentencias de control de flujo, escribe Get-Help about_If.

4.3 Gestionar sentencias condicionales grandes con conmutadores

Problema

En querrás encontrar una forma más fácil o compacta de representar una gran if... elseif... else declaración condicional.

Solución

Utiliza la sentencia switch de PowerShell para representar más fácilmente una sentencia condicional grande if... elseif... else.

Por ejemplo:

$temperature = 20

switch($temperature)
{
   { $_ -lt 32 }   { "Below Freezing"; break }
   32              { "Exactly Freezing"; break }
   { $_ -le 50 }   { "Cold"; break }
   { $_ -le 70 }   { "Warm"; break }
   default         { "Hot" }
}

Debate

La sentencia switch de PowerShell te permite comprobar fácilmente su entrada con un gran número de comparaciones. La sentencia switch admite varias opciones que te permiten configurar cómo compara PowerShell la entrada con las condiciones, por ejemplo, con un comodín, una expresión regular o incluso un bloque de secuencia de comandos arbitrario. Dado que escanear el texto de un archivo es una tarea muy común, la sentencia switch de PowerShellpermite hacerlo directamente. Estas adiciones hacen que las sentencias switch de PowerShell sean mucho más potentes que las de C y C++.

Como otro ejemplo de la declaración switch en acción, considera cómo determinar el SKU del sistema operativo actual. Por ejemplo, ¿el script se ejecuta en Windows 7 Ultimate? ¿Windows Server Cluster Edition? El cmdlet Get-CimInstance te permite determinar el SKU del sistema operativo, pero lamentablemente devuelve su resultado como un simple número. Una sentencia switch te permite asignar estos números a sus equivalentes en inglés basándote en la documentación oficial:

##############################################################################
##
## Get-OperatingSystemSku
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Gets the sku information for the current operating system

.EXAMPLE

PS > Get-OperatingSystemSku
Professional with Media Center

#>

param($Sku =
    (Get-CimInstance Win32_OperatingSystem).OperatingSystemSku)

Set-StrictMode -Version 3

switch ($Sku)
{
    0   { "An unknown product"; break; }
    1   { "Ultimate"; break; }
    2   { "Home Basic"; break; }
    3   { "Home Premium"; break; }
    4   { "Enterprise"; break; }
    5   { "Home Basic N"; break; }
    6   { "Business"; break; }
    7   { "Server Standard"; break; }
    8   { "Server Datacenter (full installation)"; break; }
    9   { "Windows Small Business Server"; break; }
    10  { "Server Enterprise (full installation)"; break; }
    11  { "Starter"; break; }
    12  { "Server Datacenter (core installation)"; break; }
    13  { "Server Standard (core installation)"; break; }
    14  { "Server Enterprise (core installation)"; break; }
    15  { "Server Enterprise for Itanium-based Systems"; break; }
    16  { "Business N"; break; }
    17  { "Web Server (full installation)"; break; }
    18  { "HPC Edition"; break; }
    19  { "Windows Storage Server 2008 R2 Essentials"; break; }
    20  { "Storage Server Express"; break; }
    21  { "Storage Server Standard"; break; }
    22  { "Storage Server Workgroup"; break; }
    23  { "Storage Server Enterprise"; break; }
    24  { "Windows Server 2008 for Windows Essential Server Solutions"; break; }
    25  { "Small Business Server Premium"; break; }
    26  { "Home Premium N"; break; }
    27  { "Enterprise N"; break; }
    28  { "Ultimate N"; break; }
    29  { "Web Server (core installation)"; break; }
    30  { "Windows Essential Business Server Management Server"; break; }
    31  { "Windows Essential Business Server Security Server"; break; }
    32  { "Windows Essential Business Server Messaging Server"; break; }
    33  { "Server Foundation"; break; }
    34  { "Windows Home Server 2011"; break; }
    35  { "Windows Server 2008 without Hyper-V for Windows Essential Server"; break; }
    36  { "Server Standard without Hyper-V"; break; }
    37  { "Server Datacenter without Hyper-V (full installation)"; break; }
    38  { "Server Enterprise without Hyper-V (full installation)"; break; }
    39  { "Server Datacenter without Hyper-V (core installation)"; break; }
    40  { "Server Standard without Hyper-V (core installation)"; break; }
    41  { "Server Enterprise without Hyper-V (core installation)"; break; }
    42  { "Microsoft Hyper-V Server"; break; }
    43  { "Storage Server Express (core installation)"; break; }
    44  { "Storage Server Standard (core installation)"; break; }
    45  { "Storage Server Workgroup (core installation)"; break; }
    46  { "Storage Server Enterprise (core installation)"; break; }
    46  { "Storage Server Enterprise (core installation)"; break; }
    47  { "Starter N"; break; }
    48  { "Professional"; break; }
    49  { "Professional N"; break; }
    50  { "Windows Small Business Server 2011 Essentials"; break; }
    51  { "Server For SB Solutions"; break; }
    52  { "Server Solutions Premium"; break; }
    53  { "Server Solutions Premium (core installation)"; break; }
    54  { "Server For SB Solutions EM"; break; }
    55  { "Server For SB Solutions EM"; break; }
    56  { "Windows MultiPoint Server"; break; }
    59  { "Windows Essential Server Solution Management"; break; }
    60  { "Windows Essential Server Solution Additional"; break; }
    61  { "Windows Essential Server Solution Management SVC"; break; }
    62  { "Windows Essential Server Solution Additional SVC"; break; }
    63  { "Small Business Server Premium (core installation)"; break; }
    64  { "Server Hyper Core V"; break; }
    72  { "Server Enterprise (evaluation installation)"; break; }
    76  { "Windows MultiPoint Server Standard (full installation)"; break; }
    77  { "Windows MultiPoint Server Premium (full installation)"; break; }
    79  { "Server Standard (evaluation installation)"; break; }
    80  { "Server Datacenter (evaluation installation)"; break; }
    84  { "Enterprise N (evaluation installation)"; break; }
    95  { "Storage Server Workgroup (evaluation installation)"; break; }
    96  { "Storage Server Standard (evaluation installation)"; break; }
    98  { "Windows 8 N"; break; }
    99  { "Windows 8 China"; break; }
    100 { "Windows 8 Single Language"; break; }
    101 { "Windows 8"; break; }
    103 { "Professional with Media Center"; break; }

    default {"UNKNOWN: " + $SKU }
}

Aunque se utiliza como una forma de expresar declaraciones condicionales de gran tamaño de forma más limpia, una declaración switch funciona de forma muy similar a una gran secuencia de declaraciones if, en contraposición a una gran secuencia de declaraciones if... elseif... elseif... else. Dada la entrada que proporciones, PowerShell evalúa esa entrada con cada una de las comparaciones de la sentencia switch. Si el resultado de la comparación es true, PowerShell ejecuta el bloque de secuencia de comandos que le sigue. A menos que ese bloque de secuencia de comandos contenga una sentencia break,PowerShell continúa evaluando las siguientes comparaciones.

Para más información sobre la sentencia switch de PowerShell, consulta "Sentencias condicionales" o escribe Get-Help about_Switch.

4.4 Repetir operaciones con bucles

Problema

En querrás ejecutar el mismo bloque de código más de una vez.

Solución

Utiliza una de las sentencias en bucle de PowerShell (for, foreach, while, y do) o el cmdlet ForEach-Object de PowerShell para ejecutar un comando o bloque de script más de una vez. Para una descripción detallada de estas sentencias en bucle, consulta "Sentencias en bucle". Por ejemplo:

for bucle
for($counter = 1; $counter -le 10; $counter++)
{
  "Loop number $counter"
}
foreach bucle
foreach($file in dir)
{
  "File length: " + $file.Length
}
ForEach-Object cmdlet
Get-ChildItem | ForEach-Object { "File length: " + $_.Length }
while bucle
$response = ""
while($response -ne "QUIT")
{
  $response = Read-Host "Type something"
}
do..while bucle
$response = ""
do
{
  $response = Read-Host "Type something"
} while($response -ne "QUIT")
do..until bucle
$response = ""
do
{
  $response = Read-Host "Type something"
} until($response -eq "QUIT")

Debate

Aunque cualquiera de las sentencias de bucle puede escribirse para que sea funcionalmente equivalente a cualquiera de las otras, cada una se presta a ciertos problemas.

En sueles utilizar un bucle for cuando necesitas realizar una operación un número exacto de veces. Como utilizarlo de esta forma es tan habitual, a menudo se denomina bucle contado for.

Normalmente utilizas un bucle foreach cuando tienes una colección de objetos y quieres visitar cada elemento de esa colección. Si aún no tienes toda esa colección en memoria (como en la colección dir del ejemplo foreach mostrado anteriormente), el cmdlet ForEach-Object suele ser una alternativa más eficiente.

A diferencia del bucle foreach, el cmdlet ForEach-Object te permite procesar cada elemento de la colección a medida que PowerShell lo genera. Esta es una distinción importante; pedir a PowerShell que recoja toda la salida de un comando grande (como Get-Content hugefile.txt) en un bucle foreach puede arrastrar fácilmente tu sistema.

Al igual que las funciones orientadas al canal, el cmdlet ForEach-Object te permite definir comandos para ejecutar antes de que comience el bucle, durante el bucle y después de que éste finalice:

PS > "a","b","c" | ForEach-Object `
    -Begin { "Starting"; $counter = 0 } `
    -Process { "Processing $_"; $counter++ } `
    -End { "Finishing: $counter" }

Starting
Processing a
Processing b
Processing c
Finishing: 3
Consejo

Para invocar varias operaciones en tu bucle al mismo tiempo, utiliza el modificador -paralelo de ForEach-Object. Para más información, consulta la Receta 4.5.


Los bucles while y do..while son similares, en el sentido de que continúan ejecutando el bucle mientras su condición se evalúe como true. Un bucle while comprueba esto antes de ejecutar su bloque de secuencia de comandos, mientras que un bucle do..while comprueba la condición después de ejecutar su bloque de secuencia de comandos. Un bucle do..until es exactamente igual que un bucle do..while, salvo que sale cuando su condición devuelve $true, en lugar de cuando su condición devuelve $false.

Para una descripción detallada de estas sentencias en bucle, consulta "Sentencias en bucle" o escribe Get-Help about_For, Get-Help about_Foreach, Get-Help about_Whileo Get-Help about_Do.

4.5 Procesar en paralelo la acción que consume tiempo

Problema

En tienes un conjunto de datos o acciones que quieres ejecutar al mismo tiempo.

Solución

Utiliza el modificador -parallel del cmdlet ForEach-Object:

PS > Measure-Command { 1..5 | ForEach-Object { Start-Sleep -Seconds 5 } }

(...)
TotalSeconds      : 25.0247856
(...)

PS > Measure-Command { 1..5 | ForEach-Object -parallel { Start-Sleep -Seconds 5 } }

(...)
TotalSeconds      : 5.1354752
(...)

Debate

Hay ocasiones en PowerShell en las que puedes acelerar significativamente una operación de larga duración ejecutando partes de ella al mismo tiempo. Las oportunidades perfectas para ello son los escenarios en los que tu script pasa la mayor parte del tiempo esperando en recursos de red (como la descarga de archivos o páginas web) u operaciones lentas (como el reinicio de una serie de servicios lentos).

En estos escenarios, puedes utilizar el parámetro -parallel de ForEach-Object para realizar estas acciones al mismo tiempo. Bajo cuerda, PowerShell utiliza trabajos en segundo plano para ejecutar cada rama. Limita el número de ramas que se ejecutan al mismo tiempo a lo que especifiques en el parámetro -ThrottleLimit, con un valor por defecto de 5.

Nota

Si la razón por la que quieres varios comandos en paralelo es realizar alguna tarea rápidamente en un gran conjunto de máquinas, debes utilizar en su lugar Invoke-Command. Para más información, consulta la Receta 29.5.

Como PowerShell ejecuta estas ramas como trabajos en segundo plano, tienes que utilizar la sintaxis $USING para introducir variables externas en este trabajo en segundo plano (PowerShell introduce $_ por defecto) o proporcionar las variables en el parámetro -ArgumentList. Por ejemplo

PS > $greeting = "World"
PS > 1..5 | ForEach-Object -parallel { "Hello $greeting" }
Hello
Hello
Hello
Hello
Hello

PS > 1..5 | ForEach-Object -parallel { "Hello $USING:greeting" }
Hello World
Hello World
Hello World
Hello World
Hello World

PowerShell ejecuta estos trabajos en segundo plano en tu proceso PowerShell principal, para que puedas actuar sobre la entrada como instancias vivas:

$processes = 1..10 | ForEach-Object { Start-Process notepad -PassThru }
$processes | ForEach-Object -parallel { $_.Kill() }

Si necesitas que las ramas de tu bucle paralelo se comuniquen de nuevo con tu shell principal, el enfoque recomendado es conseguirlo mediante la salida del bloque de script y luego hacer que tu shell principal procese los resultados. Es tentador hacer esto con objetos vivos, pero ten cuidado porque el camino es traicionero y difícil. Tomemos un ejemplo sencillo: ejecutar una operación paralela para incrementar un contador.

Al principio puede parecer que debes utilizar

$counter = 0
1..10 | ForEach-Object -parallel {
    $myCounter = $USING:counter
    $myCounter = $myCounter + 1
}

Sin embargo, cuando escribes $counter = $counter + 1 en PowerShell, PowerShell actualiza la variable $counter en el ámbito actual. Si quieres cambiar un objeto desde un trabajo en segundo plano, tienes que hacerlo estableciendo una propiedad en un objeto activo en lugar de intentar sustituir el objeto. Afortunadamente, PowerShell tiene un tipo llamado [ref] para este tipo de escenario:

$counter = [ref] 0
1..10 | ForEach-Object -parallel {
    $myCounter = $USING:counter
    $myCounter.Value = $myCounter.Value + 1
}

Inicialmente, esto parece funcionar:

PS > $counter

Value
-----
   10

Ahora que estamos orgullosos de nosotros mismos, hagámoslo realmente en paralelo:

$counter = [ref] 0
1..10000 | ForEach-Object -throttlelimit 100 -parallel {
    $myCounter = $USING:counter
    $myCounter.Value = $myCounter.Value + 1
}
PS > $counter

Value
-----
 9992

¡Uy! Como hemos hecho esto con un paralelismo masivo, $myCounter.Value puede cambiar en cualquier momento durante las partes del pipeline en las que PowerShell se ejecuta $myCounter.Value = $myCounter.Value + 1. Esto se llama una condición de carrera, y es común a cualquier lenguaje que permita que el código de varios bloques de código simultáneos se ejecute al mismo tiempo. Para librarnos de los extraños estados intermedios, tenemos que utilizar la clase Incremento entrelazado del .Net Framework:

$counter = [ref] 0
1..10000 | ForEach-Object -throttlelimit 100 -parallel {
    $myCounter = $USING:counter
    $null = [Threading.Interlocked]::Increment($myCounter)
}

Lo que nos da correctamente:

PS > $counter

Value
-----
10000

Estos problemas son espinosos, y muerden con regularidad incluso a los programadores profesionales. La mejor práctica para tratar este tipo de problemas es evitar la zona por completo, no procesando ni operando sobre estado compartido.

4.6 Añadir una Pausa o Retraso

Problema

En querrás pausar o retrasar tu guión o comando.

Solución

Para hacer una pausa en hasta que el usuario pulse la tecla Intro, utiliza el comando pause:

PS > pause
Press Enter to continue...:

Para pausar hasta que el usuario pulse cualquier tecla, utiliza el método ReadKey() en el objeto $host:

PS > $host.UI.RawUI.ReadKey()

Para poner en pausa un script durante un tiempo determinado, utiliza el cmdlet Start-Sleep:

PS > Start-Sleep 5
PS > Start-Sleep -Milliseconds 300

Debate

Cuando quieras pausar tu script hasta que el usuario pulse una tecla o durante un tiempo determinado, pause y Start-Sleep son los dos cmdlets que probablemente utilizarás.

Nota

Si quieres recuperar la entrada del usuario en lugar de hacer una pausa, el cmdlet Read-Host te permite leer la entrada del usuario. Para más información, consulta la Receta 13.1.

En otras situaciones, a veces querrás escribir un bucle en tu script que se ejecute a una velocidad constante, como una vez por minuto o 30 veces por segundo. Esto suele ser una tarea difícil, ya que los comandos del bucle pueden ocupar una cantidad de tiempo considerable, o incluso incoherente.

En el pasado, muchos juegos de ordenador sufrían por resolver este problema de forma incorrecta. Para controlar la velocidad de su juego, los desarrolladores de juegos añadían comandos para ralentizarlo. Por ejemplo, después de muchos ajustes y jugueteos, los desarrolladores pueden darse cuenta de que el juego funciona correctamente en una máquina normal si hacen que el ordenador cuente hasta 1 millón cada vez que actualiza la pantalla. Por desgracia, la velocidad de estos comandos (como contar) depende en gran medida de la velocidad del ordenador. Como un ordenador rápido puede contar hasta 1 millón mucho más deprisa que un ordenador lento, ¡el juego acaba ejecutándose mucho más deprisa (a menudo hasta el punto de resultar incomprensible) en ordenadores más rápidos!

Para hacer que tu bucle se ejecute a una velocidad regular, puedes medir cuánto tardan en completarse los comandos de un bucle, y luego retrasarlo el tiempo que quede, como se muestra en el Ejemplo 4-1.

Ejemplo 4-1. Ejecutar un bucle a velocidad constante
$loopDelayMilliseconds = 650
while($true)
{
   $startTime = Get-Date

   ## Do commands here
   "Executing"

   $endTime = Get-Date
   $loopLength = ($endTime - $startTime).TotalMilliseconds
   $timeRemaining = $loopDelayMilliseconds - $loopLength

   if($timeRemaining -gt 0)
   {
      Start-Sleep -Milliseconds $timeRemaining
   }
}

Para más información sobre el cmdlet Start-Sleep, escribe Get-Help Start-Sleep.

Get Libro de cocina PowerShell, 4ª edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.