¿Por qué 'continuar' se comporta como 'romper' en un Foreach-Object?

123

Si hago lo siguiente en un script de PowerShell:

$range = 1..100
ForEach ($_ in $range) {
    if ($_ % 7 -ne 0 ) { continue; }
    Write-Host "$($_) is a multiple of 7"
}

Obtengo el resultado esperado de:

7 is a multiple of 7
14 is a multiple of 7
21 is a multiple of 7
28 is a multiple of 7
35 is a multiple of 7
42 is a multiple of 7
49 is a multiple of 7
56 is a multiple of 7
63 is a multiple of 7
70 is a multiple of 7
77 is a multiple of 7
84 is a multiple of 7
91 is a multiple of 7
98 is a multiple of 7

Sin embargo, si uso una tubería y ForEach-Object, continueparece romper el bucle de tubería.

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

¿Puedo tener un continuecomportamiento similar mientras sigo haciendo ForEach-Object, para no tener que romper mi canalización?

Justin Dearing
fuente
Aquí hay una página con muchos comandos para usar foreach: techotopia.com/index.php/…
bgmCoder
Encontré una explicación y una muestra decentes aquí ... powershell.com/cs/blogs/tips/archive/2015/04/27/…
Nathan Hartley

Respuestas:

164

Simplemente use el en returnlugar del continue. Esto returnregresa del bloque de script que es invocado por ForEach-Objecten una iteración particular, por lo tanto, simula el continueen un bucle.

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

Hay un problema que debe tenerse en cuenta al refactorizar. A veces, uno quiere convertir un foreachbloque de declaración en una canalización con un ForEach-Objectcmdlet (incluso tiene el alias foreachque ayuda a facilitar esta conversión y a cometer errores también). Todos los continues deben reemplazarse por return.

PD: Por desgracia, no es tan fácil de simular breaken ForEach-Object.

Roman Kuzmin
fuente
2
Por lo OP está diciendo al parecer continuepuede ser utilizado para simular una breaken ForEach-Object:)
Richard Hauer
6
@ Richard Hauer Tal continueromperá todo el guión, no solo ForEach-Objectdonde se usa.
Roman Kuzmin
22

Porque el For-Eachobjeto es un cmdlet y no un bucle continuey breakno se aplica a él.

Por ejemplo, si tiene:

$b = 1,2,3

foreach($a in $b) {

    $a | foreach { if ($_ -eq 2) {continue;} else {Write-Host $_} }

    Write-Host  "after"
}

Obtendrá la salida como:

1
after
3
after

Se debe a que continuese aplica al bucle foreach externo y no al cmdlet foreach-object. En ausencia de un bucle, el nivel más externo, lo que le da una impresión de que actúa como break.

Entonces, ¿cómo se obtiene un continuecomportamiento similar? Una forma es Where-Object, por supuesto:

1..100 | ?{ $_ % 7  -eq 0} | %{Write-Host $_ is a multiple of 7}
manojlds
fuente
Usar el cmdlet Where-Object es una buena sugerencia. En mi caso real, no creo que tenga sentido convertir las múltiples líneas de código que preceden a mi declaración if en una sola línea larga de código difícil de leer. Sin embargo, eso funcionaría para mí en otras situaciones.
Justin Dearing
@JustinDearing - In my actual case, I don't think it makes sense to make the multiple lines of code preceding my if statement into a single long line of hard to read code.¿Qué quieres decir?
manojlds
3
@manojlds tal vez piense que su solución de una línea es "difícil de leer", al menos para mí es completamente lo contrario. La forma de hacer las cosas en canalización es realmente poderosa y clara, y es el enfoque correcto para cosas simples como esa. Escribir código en el shell sin aprovechar eso no tiene sentido.
mjsr
En mi caso, esta fue la respuesta correcta, agregue una condición where para filtrar los objetos en los que estaría haciendo una continuación o un retorno para que no necesite procesarlos en primer lugar. +1
Chris Magnuson
3

Otra alternativa es una especie de truco, pero puedes envolver tu bloque en un bucle que se ejecutará una vez. De esa forma, continuetendrá el efecto deseado:

1..100 | ForEach-Object {
    for ($cont=$true; $cont; $cont=$false) {
        if ($_ % 7 -ne 0 ) { continue; }
        Write-Host "$($_) is a multiple of 7"
    }
}
zdan
fuente
4
Francamente, eso es feo :) Y no solo un truco porque en lugar del foreach-object, también podrías haber usado un bucle foreach.
manojlds
1
@manojlds: 1..100 es solo para ilustración. do {} while ($ False) funciona tan bien como for loop y es un poco más intuitivo.
Harry Martyrossian
2

Una elsedeclaración simple hace que funcione como en:

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

O en una sola tubería:

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

Pero una solución más elegante es invertir su prueba y generar resultados solo para sus éxitos

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