Windows PowerShell 講座 (8)—迴圈與流程控制

發佈日期: 2008 年 5 月 28 日

作者: 賴榮樞
    www.goodman-lai.idv.tw

任何程式語言都有迴圈及流程控制的概念或功能,Windows PowerShell 亦然。本文將以實例說明 Windows PowerShell 所提供的迴圈及流程控制。

本頁內容

If
Switch
ForEach(Foreach-Object)
For
While
巢狀迴圈
Break
Continue
Break、Continue 及迴圈標籤
結語

Windows PowerShell 提供豐富的流程控制及迴圈功能,包括 If、Switch、ForEach、For、While,以及終止或繼續迴圈的 Break 和 Continue;此外,Windows PowerShell 還提供了迴圈標籤的功能,能讓我們明確指出要終止或繼續的迴圈。

我們經常需要在程式裡持續執行某一段程式碼,直到某種情況成立(或不成立)才停止重複執行,像這種情形就需要用到迴圈。迴圈用在某種情況之下需要重複執行的某一段程式碼,如果某種情況不存在,迴圈就停止。因此迴圈的使用必須注意終止條件是否存在,如果終止條件始終不存在而讓迴圈陷入「無窮迴圈」,可能導致程式無法繼續執行而形同當掉。雖然有時候我們會以無窮迴圈的形式來使用迴圈,但這種情況都會加入流程控制來作為迴圈的終止條件。

流程控制能用來控制程式進行的岔路方向。程式可能會設計成「如果某種情況成立就跳到某一個程式碼區塊繼續執行」,這就需要利用流程控制所提供的功能:判斷條件式,並且根據判斷結果引導程式執行的路徑。本文就從 Windows PowerShell 提供的流程控制功能開始。

If

If 是經常用到的流程控制指令,任何程式語言都有 If,而且用法也都幾乎相同。If 的語法如下所示:

If (<條件式1>) {
	<程式碼區塊1>
}
Elseif (<條件式2>) {
	<程式碼區塊2>
	…
}
Else {
	<程式碼區塊n>
}
           

您可以依照程式分岔的情況設定 If 的條件式和程式碼區塊數量,但是通常遇到太多的條件式,我們就會改用稍後即將說明的 Switch,因為過多的條件式會讓 If 不易編寫、維護。如下僅有一個條件式是最單純的 If:

$wmi = Get-WmiObject win32_processor
if ($wmi.Architecture -eq 0) {
	"您的電腦是 x86 CPU"
}
            

上述第一行利用了 WMI 取得電腦的處理器資訊,並將處理器資訊置入 $wmi 變數。所取得資訊裡的 Architecture 包含了處理器架構資訊,如果該值為 0,其處理器架構為 x86,而其他的值所代表的架構如下表:

處理器架構

0

x86

1

MIPS

2

Alpha

3

PowerPC

6

Itanium

9

x64

因此這個例子接著再以 If 且輔以條件式來檢查所取得的 Architecture,而判斷電腦是哪一種處理器。不過上述的例子雖然可以執行,但並不完整,如果電腦不是 x86,就不會有任何結果。這並不理想,因此我們加上 Else 來處理「不是 x86」的情況。

$wmi = Get-WmiObject win32_processor
if ($wmi.Architecture -eq 0) {
	# 處理器屬於 x86
	"您的電腦是 x86 CPU"
}
Else {
	# 處理器非屬 x86
	"不清楚 WMI Architecture 值為 " + $wmi.Architecture + " 是哪一種處理器"
}
            

上述程式雖然能處理 x86 和「非 x86」兩種情況,但還是不完整,因此我們要根據上述表格,將 6 種情況的檢查判斷都加入。

$wmi = Get-WmiObject win32_processor
If ($wmi.Architecture -eq 0) {
	"您的電腦是 x86 CPU"
}
Elseif ($wmi.architecture -eq 1) {
	"您的電腦是 MIPS CPU"
}
Elseif ($wmi.architecture -eq 2) {
	"您的電腦是 Alapha CPU"
}
Elseif ($wmi.architecture -eq 3) {
	"您的電腦是 PowerPC CPU"
}
Elseif ($wmi.architecture -eq 6) {
	"您的電腦是 Itanium CPU"
}
Elseif ($wmi.architecture -eq 9) {
	"您的電腦是 x64 CPU"
}
Else {
	# 以上皆非
	"不清楚 WMI Architecture 值為 " + $wmi.Architecture + " 是哪一種處理器"
}

Write-Host "以上資訊係根據 WMI 所得"
            

最單純的 If 陳述式只有一個 If,如果還有其他的情況要判斷,就要如上述範例再加入其他的 Elseif。但不論單純或複雜,當 If 的任一個條件式成立時,就會執行條件式所屬的程式碼區塊,之後就會跳離整個 If。若以此例來說,如果 wmi.Architecture 的值為 0,在第一個條件式判斷即成立,因此會顯示 x86 CPU,並在顯示之後隨即跳離整個 If;如果 wmi.Architecture 的值為 9,就會歷經 6 次的條件式判斷,且在第 6 次判斷才讓條件式成立執行;如果 6 次的條件式判斷都不成立,就是執行 Else 所屬的程式碼區塊。

此外要提醒您的是,此例的程式碼區塊都只有簡單的一行程式,但實際上程式碼區塊允許多行程式,而如果有多行程式,每一行程式都必須放在大括號裡面,例如:

…
Else {
	程式行 1
	程式行 2
	程式行 3
	程式行 4
}
…
            

Switch

如果要判斷的條件式較多,可以改用 Switch,其語法如下所示:

Switch [-regex|-wildcard|-exact][-casesensitive] -file <檔名> (<欲判斷的變數>) {
	<比對條件1> {
		程式碼區塊 1
	}
	<比對條件2> {
		程式碼區塊 2
	}
	<比對條件3> {
		程式碼區塊 3
	}
…
	Default {
		程式碼區塊 n
	}
}
            

若將之前的 CPU 範例改寫成 Switch 則如下所示:

$wmi = Get-WmiObject win32_processor
Switch ($wmi.Architecture) {
	0 {
		"您的電腦是 x86 CPU"
	}
	1 {
		"您的電腦是 MIPS CPU"
	}
	2 {
		"您的電腦是 Alapha CPU"
	}
	3 {
		"您的電腦是 PowerPC CPU"
	}
	6 {
		"您的電腦是 Itanium CPU"
	}
	9 {
		"您的電腦是 x64 CPU"
	}
	Default {
		"WMI Architecture = " + $wmi.Architecture + ",不清楚是哪一種處理器"
	}
}

Write-Host "以上資訊係根據 WMI (switch) 所得"
            

Switch 的比對條件也可以是字串,而且 Switch 在比對時也支援正規運算式、萬用字元、精確比對、字母大小寫有別,或者從檔案輸入。例如以下是字母大小寫有別的比對實例:

$var = "Windows PowerShell"
Switch -casesensitive ($var) {
	"windows" {
		"windows"
	}
	"windows powershell" {
		"windows powershell"
	}
	"Windows PowerShell" {
		"Windows PowerShell (case sensitive)"
	}
}
            

要提醒您的是,Switch 和 If 最大的不同,是 If 只要有條件式成立,並執行了條件式所屬的程式區塊之後,就會跳離整個 If,但是 Switch 若有條件式成立,並執行過條件式所屬的程式區塊之後,仍會往下繼續比對。因此在設計 Switch 的比對時,應先考量會不會發生多重結果的情況、多重結果是不是您要的;如果不想得到多重結果,可以在每個程式碼區塊的最後加入稍後隨即說明的 Break 或 Continue。

ForEach(Foreach-Object)

由於 Windows PowerShell 隨處可見物件,因此也就經常需要處理物件,尤其當我們需要依序處理集合物件裡的每一個物件時,ForEach 更是最方便的處理方式。VBScript 也有功能相同的 For…Each…Next,但是 Windows PowerShell 的方式更為簡便:Windows PowerShell 的作法簡化成一個 ForEach,因此再也不用擔心會忘了結尾的 Next。ForEach 的用法如下所示:

ForEach ($<個別的項目或物件> in $<集合物件>) {
	<指令區塊>
}
            

舉凡迴圈都應該要有終止條件,不然若變成無窮迴圈,會讓程式卡在迴圈而無法繼續執行,形同程式當掉。ForEach 執行時會依序並一一取得集合物件裡的物件,而且每次取得個別物件時,就執行一次 <指令區塊> 裡的程式碼。這意味著 ForEach 的終止條件就是處理完集合物件裡的每一個物件,如果集合物件裡有十個物件,ForEach 就會執行十次。

我們先以簡單的例子來說明 ForEach,以下的例子會先建立陣列,然後利用 ForEach 一一取得陣列元素,最後再以指令區塊裡的 $item 顯示元素值。

PS > $array=@(1..5)
PS > ForEach ($item in $array) {$item}
1
2
3
4
5
            

稍後提及 If 時,我們將舉出綜合 ForEach 和 If 的實例,但現在我們要強調 ForEach 的另一種用法。實際上,Windows PowerShell 的 ForEach 其實是 Foreach-Object 的別名,而 Foreach-Object 是 Windows PowerShell 內建的 cmdlet。因此 ForEach 的另一種用法就是以管線符合結合其他的 cmdlet,其語法如下所示:

<命令> | ForEach -process {指令區塊} [-begin {指令區塊}] [-end {指令區塊}]
            

例如以下兩個例子,第一個先以 dir 列出目前磁碟目錄底下所有的檔案和目錄,然後將結果透過管線送到 ForEach,而 ForEach 會以 {指令區塊} 處理每一筆檔案和目錄;也就是將每個檔案或目錄的長度除以 1024,並加以顯示(以 KB 顯示檔案長度)。要注意的是,其中的 $_ 變數代表的是傳入 ForEach 的物件:

PS > dir | ForEach -process { $_.length / 1024}
            

第二個例子先以 Get-Eventlog 取得最新的一千個「系統」事件,並置入 $Events 變數。接著透過管線將 $Events 傳給 ForEach,而 ForEach 首先以 -begin 顯示目前的日期和時間,再以 -process 裡的 Out-File 建立名為 event.txt 的文字檔,並且將每個事件的訊息屬性存入這個檔案,最後再以 -end 在所有的處理都完成之後,再次顯示目前的日期和時間。

PS > $Events = Get-Eventlog -logname system -newest 1000
PS > $Events | ForEach -begin {get-date} -process {out-file -filepath events.txt -append -inputobject $_.message} -end {get-date}
            

For

For 類似 ForEach,但不同的是 ForEach 便於處理集合物件裡的每個物件,For 比較適合用在以固定次數作為終止條件的迴圈。以下是 For 迴圈的語法:

For (<計數器啟始>; <條件式>; <計數器增量>) {
	<程式碼區塊>
}
            

For 迴圈之所以適合用在固定次數的迴圈,是因為在其語法就配備了迴圈執行次數的計數器啟始及增量,而其執行的基本邏輯步驟是:1. 計數器啟始 2. 判斷條件式 3. 若條件式成立則執行程式碼區塊(若條件式不成立就結束迴圈,後續步驟 4 也不會執行)4. 計數器增量,接著跳到步驟 2 判斷條件式。以下的例子是以 WMI 搭配 For 迴圈來 ping 127.0.0.1 到 127.0.0.10 這一段循序範圍的 IP 位址。

[int]$intIPRange = 10
[string]$intIP = "127.0.0."

# 此 For 迴圈的計數器啟始值為1,條件式為$i < $intIPRange
# 也就是會 ping 127.0.0.1 到 127.0.0.10 等 10 個 IP 位址
For ($i=1; $i -le $intIPRange; $i++)
{
	# 要 ping 的 IP 位址是由 $intIP 和 $i 所結合
	$strQuery = "select * from win32_pingstatus where address = '" + $intIP + $i + "'"
	# 利用 Get-WmiObject 送出 ping 的查詢
	$wmi = Get-WmiObject -query $strQuery
	"Pinging $intIP$i ... "
	If ($wmi.statuscode -eq 0) {
		"成功"
	}
	Else {
		"錯誤,狀態碼: " + $wmi.statuscode
	}
}
           

For 的寫法還有一些變化,例如故意省略條件式,讓 For 迴圈變成無窮迴圈,但另在 For 迴圈的程式碼區塊利用 If 達到條件式跳出迴圈的目的。例如以下兩種變化,可以讓 For 適用於迴圈次數不固定的情況。

# 變化一:無窮迴圈
For (;;) {
	<程式碼>
	If (<條件式>) {
		Break
	}
}

# 變化二:無窮迴圈,但保持計數器啟始和增量
For ($i = 1; ;$i++) {
	<程式碼>
	If (<條件式>) {
		Break
	}
}
           

While

For 適合用在次數固定的迴圈,如果迴圈執行次數不固定或未知,除了如上將 For 寫成外加條件式的無窮迴圈,也可以改用 While 迴圈。While 迴圈可重複執行若干次數,並且可在重複執行之前依照其所附屬的條件式來決定繼續重複與否。根據先執行或先判斷條件式,以及條件式的判斷方式,While 迴圈有三種變化,以下分別討論。

‧While

While 迴圈的使用語法如下:

While (<條件式>) {
	<程式碼區塊>
}
			

While 迴圈會先檢查 <條件式>,如果條件式成立才會執行附屬的程式碼區塊,因此如果條件式一開始就不成立,程式碼區塊就一次都不會執行。例如以下顯示 1 到 10 的簡單例子,如果將 -le 改成 -ge,就完全不會顯示任何數值:

$i = 1
While ($i -le 10) {
	"While loop: " + $i
	$i ++
}
"程式結束"
			

‧Do While

Do While 迴圈的使用語法如下:

Do {
	<程式碼區塊>
} While (<條件式>)
			

Do While 和 While 迴圈幾乎相同,差別只在 Do While 會先執行所屬的程式碼區塊,再檢查 <條件式>,若條件式成立會再次執行所屬的程式碼區塊,然後再檢查條件式,並以此重複。這意味著 Do While 所屬的程式碼區塊至少會執行一次。

‧Do Until

Do Until 迴圈的使用語法如下:

Do {
	<程式碼區塊>
} Until (<條件式>)
			

Do Until 和 Do While 也很類似,兩者都是先執行所屬的程式碼區塊,再檢查 <條件式>。但兩者的差別在於:Do While 是條件式成立就再次執行所屬的程式碼區塊,而 Do Until 則是條件式成立就結束迴圈(也就是若條件式不成立就再次執行所屬的程式碼區塊)。

巢狀迴圈

迴圈也可以巢狀,也就是迴圈裡還有迴圈;例如以下的雙層巢狀迴圈:

While (<條件式>) {
	…
	While (<條件式>) {
		…
	}
}
			

如果巢狀的是兩個 For 迴圈,那麼總執行次數將是兩個 For 迴圈執行次數的乘積;例如外層執行 3 次,內層執行 5 次,總共會執行 15 次。

Break

Break 除了可以用來終止所屬的迴圈(包括 ForEach、For、While、Do While、Do Until),而且也能用來結束整個 Switch 陳述式。例如以下兩個範例的第一個範例比對到 windows powershell 就會結束,因為此例不是字母大小寫相異的比對,更重要的是受到 Break 的作用。

$var = "Windows PowerShell"
Switch ($var) {
	"windows" {
		"windows"
		Break
	}
	"windows powershell" {
		"windows powershell (case insensitive)"
		Break
		# 由於不是字母大小寫相異的比對,而且還有 Break,
		# 所以此例執行至此便告終止。
	}
	"Windows PowerShell" {
		"Windows PowerShell (case sensitive)"
		Break
	}
}
			

另一個範例則是受到 If 的影響,當 $i 遞增到 5 的時候,使得 If 的條件式成立而執行了 Break,因而終止了 For 迴圈。

For ($i = 1; $i -le 10; $i++) {
	Write-Host $i
	If ($i -eq 5) {
		Write-Host "BREAK!!"
		Break
	}
}
			

Continue

和 Break 相同的是 Continue 也能作用在 ForEach、For、While、Do While、Do Until 等迴圈以及 Switch 流程控制,但更重要的差別是 Break 會終止這些陳述式,而 Continue 會跳回這些陳述式開頭的地方,「繼續」執行迴圈或 Switch 的下一個動作。例如以下的範例,只有 $i 是奇數時,條件式 $i % 2 才會成立,也才會執行If程式碼區塊裡的 Continue,而這會跳過下一行的 $i、跳回 ForEach 迴圈繼續下一個數值,因此這個範例只會顯示 1 到 10 裡的偶數。

ForEach ($i in 1..10) {
	If ($i % 2) {
		Continue
	}
	$i
}
			

Break、Continue 及迴圈標籤

Break 和 Continue 可以指定要作用的迴圈,通常在巢狀迴圈時,為了清楚 Break 或 Continue 所作用的迴圈為何,就會在 Break 或 Continue 指定之後迴圈名稱。但是在這之前,必須先替迴圈命名,也就是替迴圈加上標籤,位置是在迴圈關鍵字之前,並且還要在最前面加上冒號,例如以下的例子,在 While 之前的 :OutHere 即為 While 迴圈的標籤,而內層 While 迴圈裡的 Break OutHere 則明確指定了要結束的是外層 While 迴圈(這當然會連同內層的迴圈都一併結束)。

…
:OutHere While (1) {
	While (1) {
		If ($var -le 1024) {
			Break OutHere
		}
	}
}
…
			

我們甚至可以如下使用迴圈標籤:

:OutHere While (1) {
	While (1) {
		If ($var -le 1024) {
			Break $target
		}
	}
}
…
			

結語

迴圈和流程控制能讓程式變得靈活、彈性,讓程式不只是循序的執行一次,更能依照我們所設定的條件,重複執行程式裡的某些程式碼,或者跳過某些程式碼不執行。

使用迴圈或條件控制時,除了要瞭解語法,更重要的是條件式,包括要避免無窮迴圈,以及必須確定所設定的條件必須真的符合現況,而不只是語法正確,否則很可能會得到沒有錯誤訊息但錯誤的執行結果(如果還把錯誤結果當真,就更麻煩了)。