PowerShell – Cicli
Ora che i nostri script sanno prendere decisioni, è il momento di insegnargli a ripetere. Rinominare cento file, leggere ogni riga di un log, controllare una per una tutta le cartelle di un disco: nessuno ha voglia di scrivere lo stesso comando cento volte, ed è qui che entrano in gioco i cicli.
In PowerShell i cicli si dividono in due famiglie. Quelli di tipo Loop (for, while, do) ripetono un blocco di codice finché una condizione resta vera, oppure per un numero prestabilito di volte. I cicli ForEach, invece, ripetono il blocco per ogni elemento di una collezione: file, processi, righe di testo, numeri.
Avete già dato un’occhiata ai miei articoli PowerShell – Introduzione, PowerShell – Gestione del File System e PowerShell – Operatori e condizioni?
Ripassiamo la cartella di prova
Useremo la stessa cartella di prova del capitolo precedente. Se non l’avete più, ricreatela con questo blocco (vedi il capitolo su operatori e condizioni per i dettagli):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
New-Item -ItemType Directory -Path "Test" Set-Location "Test" $file = [ordered]@{ "documento.txt" = 1KB "note.txt" = 0 "log.log" = 200 "report.pdf" = 250KB "foto.jpg" = 3MB "archivio.zip" = 12MB } foreach ($nome in $file.Keys) { $f = [System.IO.File]::Create((Join-Path $PWD $nome)) $f.SetLength($file[$nome]) $f.Close() } (Get-Item "log.log").LastWriteTime = (Get-Date).AddDays(-45) (Get-Item "report.pdf").LastWriteTime = (Get-Date).AddDays(-3) (Get-Item "foto.jpg").LastWriteTime = (Get-Date).AddDays(-10) (Get-Item "archivio.zip").LastWriteTime = (Get-Date).AddDays(-120) |
Nel blocco c’è già un foreach, l’avete notato? Lo spiegheremo tra poco. Per ora verifichiamo cosa contiene la cartella:
|
1 |
Get-ChildItem | Select-Object Name, Length |

Figura 1: la cartella di prova
I cicli Loop: for, while e do
Contare con for
Il ciclo for serve quando sappiamo quante volte ripetere un blocco. Dentro le parentesi tonde ci sono tre parti separate da punto e virgola: l’inizializzazione (eseguita una volta sola all’inizio), la condizione (controllata prima di ogni giro) e l’incremento (eseguito alla fine di ogni giro).
Sintassi:
|
1 2 3 |
for (inizializzazione; condizione; incremento) { # blocco ripetuto finché la condizione è vera } |
|
1 2 3 |
for ($i = 1; $i -le 5; $i++) { "Giro numero $i" } for ($i = 10; $i -gt 0; $i -= 3) { $i } for ($i = 1; $i -le 5; $i++) { "7 x $i = $(7 * $i)" } |
$i++ aumenta la variabile di 1, $i -= 3 la diminuisce di 3: possiamo contare come ci pare, anche all’indietro. Se ci serve solo una sequenza di numeri, PowerShell offre l’operatore di intervallo . . :
|
1 2 |
1..5 5..1 |

Figura 2: il ciclo for e l’operatore di intervallo
Ripetere finché serve: while
Il ciclo while ripete il blocco finché la condizione è vera e la controlla prima di ogni giro: se è falsa fin dall’inizio il blocco non viene eseguito nemmeno una volta. Lo usiamo quando non sappiamo in anticipo quanti giri serviranno.
|
1 2 3 4 5 |
$n = 1 while ($n -le 100) { $n $n *= 3 } |

Figura 3: il ciclo while: le potenze di 3 fino a 100
Attenzione: dentro un while qualcosa deve prima o poi rendere falsa la condizione. Se dimentichiamo di modificare $n il ciclo non finisce mai e la console resta bloccata: per interromperlo si preme CTRL+C.
Almeno una volta: do-while e do-until
Nei cicli do la condizione viene controllata dopo il blocco, che quindi viene eseguito sempre almeno una volta. do … while ripete finché la condizione è vera; do … until ripete finché la condizione è falsa, cioè si ferma quando diventa vera.
|
1 2 3 4 5 6 |
$n = 10 while ($n -lt 5) { "while: eseguito" } do { "do-while: eseguito" } while ($n -lt 5) $i = 1 do { "Tentativo $i"; $i++ } until ($i -gt 3) |

Figura 4: while contro do-while, e do-until
Il primo while non stampa nulla, perché 10 -lt 5 è falso fin dall’inizio; il do-while invece esegue il blocco una volta prima di accorgersene. È utile, per esempio, quando dobbiamo chiedere un dato all’utente e ripetere la domanda finché la risposta non è valida.
Saltare o uscire: continue e break
Dentro qualunque ciclo possiamo usare due parole chiave: continue salta il resto del giro corrente e passa a quello successivo, mentre break esce dal ciclo immediatamente. Sono gli stessi break visti nello switch.
|
1 2 3 4 5 |
for ($i = 1; $i -le 10; $i++) { if ($i % 2 -eq 0) { continue } if ($i -gt 7) { break } "dispari: $i" } |
Combinato con while ($true), che per definizione non finisce mai, break permette di scrivere cicli che si fermano quando succede qualcosa all’interno del blocco:
|
1 2 3 4 5 6 |
$tentativi = 0 while ($true) { $tentativi++ if ($tentativi -ge 3) { break } } "Usciti dopo $tentativi tentativi" |

Figura 5: continue, break e il ciclo “infinito” con uscita
Riepilogo dei cicli Loop
| Ciclo | Quando si usa | Controlla la condizione |
| for | Si sa quante volte ripetere | Prima di ogni giro |
| while | Si ripete finché la condizione è vera | Prima di ogni giro |
| do-while | Almeno un giro, poi finché vera | Dopo ogni giro |
| do-until | Almeno un giro, poi finché falsa | Dopo ogni giro |
I cicli ForEach
Molto spesso non ci interessa contare, ma fare qualcosa per ogni elemento di un elenco. PowerShell lavora ad oggetti e quasi ogni cmdlet restituisce una collezione, quindi foreach è probabilmente il ciclo che userete di più. Esistono due forme che si chiamano allo stesso modo ma si comportano in modo diverso: l’istruzione foreach e il cmdlet ForEach-Object.
L’istruzione foreach
Si scrive foreach ($elemento in $collezione) { … }: a ogni giro la variabile $elemento contiene l’elemento successivo della collezione. Il nome della variabile lo scegliamo noi, meglio se parlante ($frutto in $frutti).
Sintassi:
|
1 2 3 |
foreach ($elemento in $collezione) { # blocco eseguito per ogni elemento } |
|
1 2 3 4 5 6 7 8 9 10 |
$frutti = "mela","pera","kiwi" foreach ($frutto in $frutti) { "Mi piace: $frutto" } foreach ($f in Get-ChildItem -File) { "{0} ({1} byte)" -f $f.Name, $f.Length } foreach ($f in Get-ChildItem -File) { if ($f.Length -gt 100KB) { "$($f.Name) è grande" } } |

Figura 6: foreach su un array e sui file della cartella
L’ultimo esempio unisce due capitoli: dentro il ciclo usiamo un if per agire solo sui file che superano 100 KB. È la combinazione che userete più spesso negli script.
Il cmdlet ForEach-Object (alias: foreach, %)
ForEach-Object fa lo stesso lavoro ma si inserisce in una pipeline: riceve gli oggetti uno alla volta e, per ciascuno, esegue il blocco tra graffe, dove l’oggetto corrente si chiama $_. Ha due alias: foreach e %.
|
1 2 3 4 5 6 7 8 |
1..5 | ForEach-Object { $_ * 2 } Get-ChildItem -File | ForEach-Object { $_.Name.ToUpper() } Get-ChildItem -File | ForEach-Object { "$($_.Name): $($_.Length / 1KB) KB" } 1..3 | % { "Giro $_" } |

Figura 7: ForEach-Object nella pipeline
NOTA: la parola foreach ha due significati. Se si trova all’inizio di un’istruzione è il ciclo foreach ($x in $y); se si trova dopo una pipe è l’alias di ForEach-Object. PowerShell capisce dal contesto, ma per chi legge il codice può confondere: negli script meglio scrivere ForEach-Object per esteso.
Blocchi Begin, Process ed End
Foreach-Object può eseguire anche codice prima del primo elemento (-Begin) e dopo l’ultimo (-End), mentre -Process è il blocco ripetuto per ogni elemento. Ideale per calcolare totali: inizializziamo in -Begin, sommiamo in -Process e mostriamo il risultato in -End.
|
1 2 3 4 |
Get-ChildItem -File | ForEach-Object ` -Begin { $tot = 0 } ` -Process { $tot += $_.Length } ` -End { "Totale: $tot byte" } |

Figura 8: somma delle dimensioni con -Begin, -Process e -End
Il carattere backtick (apice rovesciato) a fine riga dice a PowerShell che il comando continua sulla riga successiva: ci permette di non scrivere una riga lunghissima. Attenzione però a non lasciare spazi dopo il backtick.
I metodi .ForEach() e .Where()
Da PowerShell 4.0 ogni collezione ha anche i metodi .ForEach() e .Where(), che applicano un blocco di codice a ogni elemento senza usare la pipeline. Sono compatti e, su grandi quantità di dati, più veloci di ForEach-Object.
|
1 2 3 |
(1..5).ForEach({ $_ * 2 }) (Get-ChildItem -File).ForEach({ $_.Name }) (1..10).Where({ $_ % 3 -eq 0 }) |

Figura 9: i metodi .ForEach() e .Where()
foreach o ForEach-Object?
| foreach (istruzione) | ForEach-Object (cmdlet) | |
| Sintassi | foreach ($x in $lista) { } | $lista | ForEach-Object { } |
| Elemento corrente | La variabile scelta ($x) | $_ |
| Elaborazione | Prima carica tutta la collezione | Un elemento alla volta |
| Memoria | Occupa più memoria | Occupa meno memoria |
| Velocità | In genere più veloce | In genere più lento |
| Si usa | Negli script | Nelle pipeline “al volo” |
Il fatto che foreach carichi prima tutta la collezione ha una conseguenza importante: possiamo modificare gli elementi mentre ci cicliamo sopra, perché l’elenco di partenza è già stato letto. Proviamo a creare tre file e a rinominarli uno per uno con un contatore:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
New-Item -ItemType Directory -Name "Prova" 1..3 | ForEach-Object { $p = Join-Path "Prova" "report_$_.txt" New-Item $p } Get-ChildItem "Prova" | Select-Object Name $i = 1 foreach ($f in Get-ChildItem "Prova") { Rename-Item $f.FullName ("copia_{0:D2}.txt" -f $i) $i++ } Get-ChildItem "Prova" | Select-Object Name Remove-Item "Prova" -Recurse |

Figura 10: creare e rinominare file con i cicli
Nel nostro esempio abbiamo aggiunto | Out-Null ai comandi New-Item per nascondere l’output di creazione (potete ometterlo per vedere cosa fanno). Il formato {0:D2} produce un numero a due cifre con lo zero davanti: 01, 02, 03. L’ultima riga ripulisce la cartella di test: ricordate che Remove-Item non usa il cestino.
Esempio pratico completo: totale per estensione
Mettiamo insieme foreach, if e una hashtable (vista nel capitolo sulle variabili) per calcolare quanti byte occupano i file di ciascun tipo. Per ogni file controlliamo se la sua estensione è già nella tabella: se sì sommiamo la dimensione, altrimenti la inseriamo. Un secondo foreach stampa il risultato in ordine alfabetico.
|
1 2 3 4 5 6 7 8 9 10 11 |
$totali = @{} foreach ($f in Get-ChildItem -File) { if ($totali.ContainsKey($f.Extension)) { $totali[$f.Extension] += $f.Length } else { $totali[$f.Extension] = $f.Length } } foreach ($ext in $totali.Keys | Sort-Object) { "{0,-6} {1,10} byte" -f $ext, $totali[$ext] } |

Figura 11: totale dei byte per estensione
Provate a modificarlo: cosa cambia se aggiungete -Recurse a Get-ChildItem? E se volete contare anche quanti file ci sono per ogni estensione?
Consigli Utili per Principianti
- Controllate sempre che il ciclo possa finire: in un while qualcosa deve rendere falsa la condizione; se resta bloccato premete CTRL+C.
- Preferite foreach a for quando dovete scorrere una collezione: è più leggibile e non rischia errori di indice.
- Date nomi parlanti alle variabili: foreach ($file in $files) si legge da solo, foreach ($x in $y) no.
- Provate prima con -WhatIf o su una cartella di test: un ciclo che cancella o rinomina agisce su tutti gli elementi, errori compresi.
- Usate ForEach-Object per esteso negli script: % è comodo in console, ma rende il codice meno leggibile.
- Ricordate continue e break: il primo salta un giro, il secondo esce dal ciclo.
Riepilogo Cicli
| Operazione | Costrutto | Esempio |
| Ripetere N volte | for | for ($i=1; $i -le 5; $i++) { } |
| Ripetere finché vero | while | while ($n -lt 10) { } |
| Almeno una volta | do-while / do-until | do { } while ($n -lt 10) |
| Saltare un giro | continue | if ($i -eq 3) { continue } |
| Uscire dal ciclo | break | if ($trovato) { break } |
| Sequenza di numeri | .. | 1..10 |
| Per ogni elemento | foreach | foreach ($f in $files) { } |
| Per ogni oggetto in pipeline | ForEach-Object (%) | $files | ForEach-Object { $_.Name } |
| Senza pipeline | .ForEach() | (1..5).ForEach({ $_ * 2 }) |
Conclusione
Con i cicli i nostri script smettono di fare una cosa alla volta: possono contare, ripetere, scorrere file e oggetti, uscire quando serve. Insieme a operatori e condizioni abbiamo ora i mattoni fondamentali di qualunque programma.
Ricordate: il modo migliore per imparare è fare pratica! Provate a riscrivere gli esempi dei capitoli precedenti usando un ciclo al posto dei comandi ripetuti.