Búsqueda de sitios web

PowerShell ForEach-Object y ForEach Loop explicados


Los bucles PowerShell ForEach y ForEach-Objects son una de las funciones más utilizadas. Le permiten iterar fácilmente a través de colecciones. Pero, ¿cuál es la diferencia entre los cmdlets ForEach-Object y ForEach()?

Ambos cmdlets tienen un aspecto similar y, básicamente, le permiten realizar la misma acción. Pero conocer la diferencia entre los dos le permite mejorar su script de PowerShell.

En este artículo, veremos cómo usar PowerShell ForEach y ForEach-Object y explicaremos la diferencia entre los dos.

ForEach() vs ForEach-Objeto

Los cmdlets ForEach() y ForEach-Object de PowerShell le permiten iterar a través de una colección y realizar una acción en cada elemento de ella. Sin embargo, hay una diferencia en cómo manejan los datos y en la usabilidad.

El cmdlet ForEach() carga la colección completa en la memoria antes de comenzar a procesar los elementos. Al cargar la colección en la memoria de su computadora, es más rápido que el cmdlet ForEach-Object.

foreach ($fruit in $fruitsAvailable) {
    Write-Host $fruit
}

Por otro lado, el cmdlet ForEach-Object de PowerShell también le permite ubicarlo detrás de otro cmdlet. De esta manera puede procesar cada elemento a medida que avanza en el proceso. Esto también significa que puede canalizar otros cmdlets detrás de él.

$fruitsAvailable | ForEach-Object {
    Write-Host $_
}

Además de estos dos cmdlets, también tenemos los dos alias ForEach (nota, sin el paréntesis () ) y %. Ambos son un alias para el cmdlet ForEach-Object, pero a veces esto puede resultar un poco confuso. Los siguientes comandos son todos iguales:

$fruitsAvailable | ForEach-Object {
    Write-Host $_
}

$fruitsAvailable | ForEach {
    Write-Host $_
}

$fruitsAvailable | % {
    Write-Host $_
}

Ahora bien, para una colección pequeña, la diferencia de rendimiento y el uso de memoria son mínimos. En el ejemplo anterior, la instrucción ForEach() es solo 4 ms más rápida y el bloque de script es más fácil de leer. Pero cuando trabaje con colecciones grandes, es posible que desee tener en cuenta el uso de la memoria.

Cuando carga un archivo CSV grande, por ejemplo, es más eficiente canalizar el cmdlet ForEach-Object detrás de él, en lugar de almacenar los resultados primero en una variable:

Import-Csv -Delimiter ";" -Path c:\temp\EmailAddresses.csv | Foreach-Object {Get-ADUser -Identity $_.Name}

PowerShell para cada uno

La instrucción ForEach en PowerShell le permite iterar a través de una matriz o una lista de elementos. La sintaxis de la declaración ForEach es bastante sencilla:

foreach ($item in $collection) {
   # Script to execute with $item
}

Como puede ver, podemos especificar la colección que queremos recorrer, puede ser una matriz o una lista, y especificamos una variable para el iterador. Este contendrá el valor de cada elemento que se encuentra dentro de la colección.

Por ejemplo, para imprimir cada fruta en la consola podemos hacer lo siguiente:

$fruitsAvailable = @("Apple", "Banana", "Orange", "Grapes", "Mango", "Strawberry", "Pineapple")

foreach ($fruit in $fruitsAvailable) { 
   Write-Host $fruit 
}

Usando colecciones

También podemos usar la instrucción ForEach de PowerShell con un cmdlet que devuelve una colección. La mayoría canalizará el cmdlet ForEach-Object detrás de un cmdlet, pero eso no siempre es necesario. La ventaja de utilizar la instrucción ForEach es que es más fácil de leer en comparación con el cmdlet ForEach-Object.

Por ejemplo, el cmdlet Get-ChildItem devuelve todos los archivos y carpetas de una ruta determinada. Para iterar a través de los archivos podemos hacer lo siguiente:

foreach ($item in Get-ChildItem -path "c:\temp") {
    Write-Host $item.Name
}

Como puede ver, Get-ChildItem devuelve el objeto de archivo o carpeta. Dentro del bloque de script podemos acceder a todas las propiedades del elemento, en este caso, el nombre del elemento.

Romper y continuar

Cuando trabaja con bucles ForEach en PowerShell, a veces desea detener las iteraciones cuando se encuentra un valor determinado u omitir un elemento según su valor. Para hacer esto podemos usar las palabras clave Break y Continue.

Por ejemplo, si queremos comprobar si un valor es divisible por 7. Si no, pasamos al siguiente elemento:

foreach ($i in 1..100) {
    if ($i % 7 -ne 0 ) { continue }
    Write-Host "$i is a multiple of 7"
}

La palabra clave Break le permite detener el bucle foreach de PowerShell cuando se cumple una determinada condición. Ahora bien, no los uso con frecuencia, la mayoría de las veces puedes escribir mejor código usando cmdlets where-object o usando los filtros apropiados. Pero a continuación se muestra un ejemplo para darle una idea de cómo podría utilizar la palabra clave Break:

foreach ($number in 1..10) {
    if ($number -gt 5) {
        Write-Host "Exiting the loop because $number is greater than 5"
        break
    }

    Write-Host "Processing number: $number"
}

Bucles anidados para cada uno

Dentro de un bloque de script, también puedes usar otras declaraciones foreach. Intente mantenerlo al mínimo, porque cuando utiliza demasiados bucles foreach anidados, su código será más difícil de leer y depurar.

Tome el siguiente ejemplo, tenemos una variedad de equipos y dentro de cada equipo, tenemos una variedad de jugadores. El bucle foreach externo itera sobre cada equipo en la matriz $teams. El bucle foreach interno luego itera sobre los jugadores dentro de cada equipo.

# Define an array of teams, where each team has an array of players
$teams = @(
    @{
        TeamName = "Team A"
        Players  = @("Player1A", "Player2A", "Player3A")
    },
    @{
        TeamName = "Team B"
        Players  = @("Player1B", "Player2B", "Player3B")
    },
    @{
        TeamName = "Team C"
        Players  = @("Player1C", "Player2C", "Player3C")
    }
)

# Nested foreach loop to iterate over teams and players
foreach ($team in $teams) {
    Write-Host "Team: $($team.TeamName)"

    foreach ($player in $team.Players) {
        Write-Host "  Player: $player"
    }
}

PowerShell para cada objeto

El cmdlet PowerShell ForEach-Object se usa cuando desea iterar a través de elementos que se transmiten desde una canalización. La ventaja de ForEach-Object es que comienza a procesar los elementos a medida que avanzan por el proceso y también puede canalizar otros cmdlets detrás de él.

Desea utilizar este cmdlet cuando trabaje con colecciones o conjuntos de datos grandes porque, aunque es más lento que la instrucción foreach, no consume tanta memoria.

Otra ventaja de ForEach-Object es que puede acelerarlo utilizando el conmutador paralelo en PowerShell 7. Esto le permite procesar varios elementos simultáneamente, pero hablaremos de eso más adelante.

Como se mencionó, el cmdlet ForEach-Object se canaliza detrás de otro cmdlet. Puede utilizar uno (o más) bloques de script para especificar la operación que desea ejecutar en el elemento actual. Por ejemplo, para obtener todos los nombres de los procesos en ejecución, puede hacer:

Get-Process | ForEach-Object {$_.ProcessName}

Como puede ver en el ejemplo anterior, hacemos referencia al elemento actual utilizando la variable $_.

Puede canalizar otros cmdlets detrás del bloque de secuencias de comandos de PowerShell ForEach-Object. Entonces, si desea almacenar cada nombre de proceso en un archivo de texto, puede hacer lo siguiente:

Get-Process | ForEach-Object {$_.ProcessName} | Out-File "c:\temp\runningprocs.txt"

En los ejemplos anteriores, ejecutamos solo un pequeño script en el bloque de script, lo que nos permite escribir todo en una sola línea. Pero también puedes agregar scripts más grandes dentro del bloque de script:

$files = Get-ChildItem -Path "C:\temp\" -File

$files | ForEach-Object {
    $fileName = $_.Name
    $fileSizeKB = [math]::Round($_.Length / 1KB, 2)

    Write-Host "Processing file: $fileName"
    Write-Host "File size: $fileSizeKB KB"

    Write-Host "Processing complete for $fileName"
}

Bloques de inicio, fin y múltiples scripts

Al utilizar el cmdlet ForEach-Object, puede utilizar un bloque de script begin y end. Estos bloques de script solo se ejecutan una vez, mientras que el bloque de script normal (el bloque de script de proceso) se ejecuta para cada elemento de la colección.

Los bloques de script de inicio y fin son excelentes cuando necesita registrar la hora de inicio y finalización de un script, o cuando desea registrar el inicio o el final de un proceso:

1..5 | ForEach-Object -Begin {
    Write-Host "Starting the processing..."
} -Process {
    $squared = $_ * $_
    Write-Host "Number: $_, Squared: $squared"
} -End {
    Write-Host "Processing complete."
}

En el ejemplo anterior, hemos etiquetado cada bloque de script con lo que es, el bloque de inicio, proceso y final. Pero no es necesario que especifiques esto (recomiendo hacerlo porque hace que tu código sea más legible).

Si especifica varios bloques de script, el primer bloque siempre se trata como un bloque inicial.

1..2 | ForEach-Object { 'Begin Block' } { 'Process Block' }

# Result
Begin Block
Process Block
Process Block

Si tiene más de dos bloques, el último siempre se trata como un bloque final. Cada bloque intermedio es un bloque de proceso.

1..2 | ForEach-Object { 'Begin' } { 'Process A Block' } { 'Process B Block' } { 'End' }

# Result
Begin
Process A Block
Process B Block
Process A Block
Process B Block
End

Si desea ejecutar varios bloques de proceso pero no desea utilizar un bloque de inicio o fin, tendrá que especificar los parámetros de inicio, proceso y fin, y asignar un valor $null a el primero y el último:

1..2 | ForEach-Object -Begin $null -Process { 'Process A Block' } { 'Process B Block' } -End $null

# Result
Process A Block
Process B Block
Process A Block
Process B Block

Usando procesamiento paralelo

Si tiene un bloque de script realmente lento o cuando necesita procesar muchos elementos (10.000+), puede usar el parámetro -parallel en PowerShell 7.x y superior. De esta manera, el bloque de secuencia de comandos se ejecutará en paralelo para cada elemento, lo que le permitirá procesar varios elementos simultáneamente.

De forma predeterminada, el parámetro paralelo procesará los elementos en lotes de 5. Pero con el parámetro -Trottelimit podemos definir el número de espacios de ejecución que queremos usar. El siguiente ejemplo procesará los 8 artículos en lotes de 4:

1..8 | ForEach-Object -Parallel {
    Write-Host "Slow script to process $_"
    Start-Sleep 1
} -ThrottleLimit 4

Con el parámetro paralelo, se crea un nuevo espacio de ejecución para cada lote. Esto significa que si necesita hacer referencia a una variable fuera del bloque de script, necesitará usar la variable $using: para hacer referencia a ella:

$Message = "Slow script to process:"

1..8 | ForEach-Object -Parallel {
    Write-Host $using:Message $_
    Start-Sleep 1
} -ThrottleLimit 4

Ahora bien, es bueno saber que ejecutar scripts en paralelo no siempre es más rápido. Para ser honesto, el 98% de las veces no es más rápido. El problema con la ejecución de su bloque de script en paralelo es que lleva tiempo crear el espacio de ejecución para ejecutar el script.

Sin la función paralela, su bloque de script se ejecuta en su hilo actual de PowerShell. De esta manera tiene acceso a todas sus variables, canalización y memoria cargada. Pero para ejecutar el bloque de script en paralelo, será necesario iniciar un nuevo subproceso de PowerShell (crear un nuevo espacio de ejecución), y esto lleva tiempo (hasta 1 segundo aproximadamente).

Entonces, esto solo tiene sentido cuando su bloque de script es muy lento, consume mucha CPU o necesita esperar a que una API responda, por ejemplo.

Por ejemplo, escribir "Hola mundo" sólo lleva un par de milisegundos:

Measure-Command { Write-Host "Hello World" } | Select TotalMilliseconds

# Result
TotalMilliseconds
-----------------
             4,65

Pero cuando se ejecuta en paralelo, la ejecución tardará casi más de 30 milisegundos:

Measure-Command { 1 | ForEach-Object -Parallel { Write-Host "Hello World" } } | Select TotalMilliseconds

# Result
TotalMilliseconds
-----------------
             31,36

Sin embargo, si tenemos un script que tarda más de 1 segundo en ejecutarse (aquí simulado con un inicio-suspensión), entonces el método paralelo tendrá una ventaja:

Measure-Command { 1..5 | ForEach-Object { Start-Sleep -Seconds 1 } } | Select TotalMilliseconds

# Result
TotalMilliseconds
-----------------
          5054,55

# Execute in parallel, with the 5 default threads:
Measure-Command { 1..5 | ForEach-Object -Parallel { Start-Sleep -Seconds 1 } } | Select TotalMilliseconds

# Result
TotalMilliseconds
-----------------
          1082,24

Romper, continuar y regresar

Al igual que con la declaración foreach, podemos usar Break para detener un bucle ForEach-Object de PowerShell, pero no podemos usar Continue. Una pausa detendrá completamente la iteración en el bucle ForEach-Object y saldrá del bucle:

1..10 | ForEach-Object {
    if ($_ -gt 5) {
        Write-Host "Exiting the loop because $_ is greater than 5"
        break
    }

    Write-Host "Processing number: $_"
}

Sin embargo, si intentamos usar Continue dentro de un bucle ForEach-Object, verá que también se detiene y existe en el bucle. Entonces el siguiente ejemplo no funcionará:

# This won't work
1..100 | ForEach-Object {
    if ($_ % 7 -ne 0 ) { continue }
    Write-Host "$_ is a multiple of 7"
}

En su lugar, necesitaremos utilizar la palabra clave return para continuar con el siguiente elemento de la colección.

1..100 | ForEach-Object {
    if ($_ % 7 -ne 0 ) { return}
    Write-Host "$_ is a multiple of 7"
}

Concluyendo

En la mayoría de los casos, usar la instrucción foreach() es el método más rápido y legible para usar un bucle ForEach. Sin embargo, si necesita procesar una gran cantidad de datos o necesita canalizar otros cmdlets detrás de ellos, entonces es mejor usar el cmdlet ForEach-Object.

Tenga en cuenta que la función paralela puede parecer agradable de usar, pero en la mayoría de los casos es más lenta debido al tiempo necesario para iniciar un nuevo espacio de ejecución. Así que asegúrese de medir las diferencias.

Espero que te haya gustado este artículo, si tienes alguna pregunta, deja un comentario a continuación. ¡Suscríbete al boletín si quieres leer más de estos artículos!

Artículos relacionados