Windows PowerShell 講座 (9)—模組化

發佈日期: 2008 年 6 月 12 日

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

身為 Windows 平台新一代的指令碼語言,Windows PowerShell 提供了許多用以達到模組化的功能;除了說明這些功能,本文也將討論變數、函式或篩選器的範圍。

本頁內容

指令碼區塊
函式
將引數傳入函式
函式的傳回值
處理管線資料的函式
函式的階段處理
篩選器
變數範圍
函式或篩選器範圍
結語

模組化能提高程式碼重複使用的能力,藉由這種設計理念,寫過的程式碼應該可以不用重頭再寫一次,而且應該要能與其他程式碼順利接合互用。函式是一般的程式語言常見的模組化機制,Windows PowerShell 除了提供函式,還有程式碼區塊和篩選器也可用於模組化。

指令碼區塊

指令碼區塊(script block)是以大括號括住的一群 Windows PowerShell 陳述式。通常我們可以將重要、經常需要修改的程式碼放進指令碼區塊,並將指令碼區塊指定給變數,而且也將指令碼區塊放在指令碼檔案的前面。利用執行運算子&,並搭配內容為指令碼區塊的變數,我們可以執行執行指令碼區塊。例如在 Windows PowerShell 提示符號輸入以下這個簡單的例子:

PS >  $a = {
>>  $x = 2
>>  $y = 3
>>  $x * $y
>>  }
>>

PS > &$a
PS > 6
           

由於我們是在 Windows PowerShell 提示字元輸入指令碼區塊內容,當輸入到指令碼區塊的左大括號之後,Windows PowerShell 知道我們正在輸入指令碼區塊,因此提示字元符號會變成>>,而完成指令碼區塊的輸入除了要先輸入右大括號,還要再按一次 Enter 按鍵。

我們是將指令碼區塊指定給 $a 變數,因此若要執行這個指令碼區塊,必須利用執行運算子&:&$a,而按下 Enter 按鍵之後所顯示的 6,就是這個指令碼區塊簡例的執行結果。

如上例將指令碼區塊指定給變數之後,在關閉 Windows PowerShell 執行環境之前,我們隨時可以利用執行運算子搭配變數來執行指令碼區塊。再舉一例如下:

PS > $lsName = {
>>  ForEach($item in $input) {
>>  $item.Name }
>>  }
>>

PS > dir | &$lsName
            

指令碼區塊也可以配合管線運算使用,這種情況指令碼區塊會透過管線接收到名為 $input 變數,這個變數的內容是管線之前的執行結果,而上例的指令碼區塊會以 ForEach 處理藉由管線傳入的 $input 變數;這個變數內容通常是物件集合,本例的指令碼區塊會只顯示每個物件的 Name 屬性。

如果將 $lsName 搭配 Get-ChildItem(dir):dir | &$lsName,結果會顯示目前工作磁碟裡的檔案名稱;如果將 $lsName 搭配 Get-Process(ps),結果則會顯示目前執行中的行程名稱:

PS > ps | &$lsName
            

我們也可以將指令碼區塊用在 Windows PowerShell 指令碼檔案,例如將以下程式碼存成 ls-FileAndProcessName.ps1 之後,每次執行 ls-FileAndProcessName.ps1 的時候,就會顯示目前工作磁碟裡的檔案名稱,以及目前執行中的行程名稱。

$lsName = {
	ForEach($item in $input) {
		$item.Name
	}
}

dir | &$lsName
ps | &$lsName
            

如上簡例,我們利用指令碼區塊重複使用顯示物件名稱的一小段程式碼,也就是將顯示物件名稱的程式碼放進指令碼區塊,並指定給變數。當我們需要這一段程式碼的時候,即可直接以執行運算子搭配變數來執行,而達到程式碼重複使用的效果。

函式

相較於指令碼區塊,函式擁有更多的功能,用起來也更彈性(當然也比較複雜)。函式定義時必須:

  • 使用 Function 關鍵字

  • 必須指定函式名稱

  • 必須以一對大括號括住函式程式碼

此外,函式可以(但非必要):

  • 傳入值

  • 傳回值

我們先從簡單的例子來說明函式:

Function MySimpleFun {
	Write-Host "簡單函式"
}
            

上述簡單函式執行後只是顯示「簡單函式」字串,整個函式程式碼只有一行 Write-Host "簡單函式",定義函式必須先以 Function 關鍵字啟始,空一字元後接著我們指定的函式名稱(此例為 MySimpleFun,要留意的是函式名稱不應該使用 Windows PowerShell 的關鍵字或 cmdlet 名稱、別名)。最後,函式程式碼要放在一對大括號裡。

函式可以定義在指令碼檔案,也可以在提示字元輸入。如果是後者,會像前述的指令碼區塊,提示字元符號會變成>>,輸入完函式程式碼及終止的右大括號之後,再按一次 Enter 按鍵即可恢復成正常的提示字元符號,接著直接輸入函式名稱並按 Enter 按鍵,即可執行函式;例如:

PS > Function MySimpleFun {
>>  Write-Host "簡單函式"
>>  }
>>

PS > MySimpleFun
簡單函式
            

如果函式是在指令碼檔案裡,也是直接以函式名稱來執行函式,但是函式定義必須在函式執行之前,例如:

# 定義 MySimpleFun函式
Function MySimpleFun {
	Write-Host "簡單函式"
}

# 執行函式
MySimpleFun
            

將引數傳入函式

能傳入引數並傳回值,是函式優於指令碼區塊的兩項特點。傳入引數讓函式的功能更為彈性,例如我們可以將不同情況的參數當成引數傳入函式,讓一個函式就能處理不同情況,而不需根據不同情況編寫不同函式。

Windows PowerShell 的函式有三種接受引數傳入的方式,例如利用 Windows PowerShell 的自動變數 $Args 陣列:

PS > Function Show-Str {
>> Write-Host $args[0]
>> }
>>

PS > Show-Str Hello
Hello
            

$Args 陣列的內容會是所有傳入函式的引數,上述例子在執行 Show-Str 函式時,指定了 Hello 作為參數,因此 $args[0] 的內容會是 Hello;而我們在 Show-Str 函式裡利用 Write-Host $args[0],則可顯示傳入的第一個引數。我們其實可以如下執行 Show-Str 函式,但因為這個函式只處理第一個引數,因此不會顯示後續的引數:

Show-Str Hello Windows PowerShell
            

既然 $args 是陣列,我們便可利用迴圈 一一 顯示每個陣列元素的內容,例如:

PS > Function Show-Para {
>>  For($i = 0; $i -le $args.length-1; $i++) {
>>  Write-Host $args[$i]
>>  }
>>  }
>>

PS > Show-Para Hello Windows PowerShell
Hello
Windows
PowerShell
            

上例的 For 迴圈能處理 $args 陣列裡的每一個元素,並且以一行一個的方式顯示出來。

我們也可以指定引數的資料型別例如以下這個簡單的例子:

PS > Function AplusB {
>>  [int]$args[0] + [int]$args[1]
>>  }
>>

AplusB 1 99
100
            

由於我們在上例指定了引數的資料型別為整數,因此如果執行時給予的不是整數,就會導致「無法將值轉換為 System.Int32 型別的錯誤」。

除了 $args 陣列,Windows PowerShell 的函式也能以一般常見的方式接收傳入的引數,例如:

Function AplusB([int]$x, [int]$y) {
	$x + $y
}
            

這種方式最大的優點,是除了能以原本循序的方式指定參數,還能以命名參數的形式指定,例如以下前兩行都會將 x 指定為 7、y 指定為 8,如果用以下第三行的方式反而無法得到正確的答案(執行結果為 0,因為只指定 a 為 7、b 為 8,並未指定 x、y 的值):

AplusB 7 8
AplusB -y 8 -x 7
AplusB -a 7 -b 8
            

此外,這種引數傳入方式還可指定預設值,例如以下的例子指定 $x、$y 的預設值為 10、90,如果執行函式時未指定引數,計算結果會是 100:

Function AplusB([int]$x=10, [int]$y=90) {
	$x + $y
}
            

最後一種,也就是第三種方式類似第二種,差別是將函式名稱後的引數列搬進大括號內的 Param,例如:

Function AplusB {
	Param([int]$x, [int]$y)
	$x + $y
}            
			

要注意的是第三種方式的 Param 必須是函式的第一行程式碼。此外,這三種引數傳入方式,如果不想指定引數資料型別,亦可刪除 [int](但就要留意型別自動轉換),或者也可以改成其他的資料型別。

函式的傳回值

Windows PowerShell 函式的傳回值與一般程式語言不太相同,例如請回想上述的 AplusB 函式,我們並未有任何傳回值的程式碼,但執行後還是會顯示相加結果:

PS > $result = AplusB 1 9
            

這是寫法的問題,因為函式裡直接寫著:$x + $y,對 Windows PowerShell 來說,這不僅意味著兩個變數的相加,也代表要在 console 顯示相加的結果,因此相加結果就成了傳回值。這種寫法可以讓 Windows PowerShell 函式同時有數個傳回值,例如以下這個簡單例子的3行運算結果,都是函式的傳回值(如果將以下 AandB 函式的結果指定給變數,這個變數會變成內含 3 個元素的陣列):

PS > Function AandB([int]$x=10, [int]$y=90) {
>>  $x + $y
>>  $x - $y
>>  $x * $y
>>  }
>>

PS > AandB 8 2
10
6
16

PS > $rst = AandB 8 2
PS > $rst.Length
3
           

Windows PowerShell 也提供了 Return 關鍵字,也可用來讓函式傳回值,但 Return 的重點應該是會立即結束函式、跳回呼叫函式之原處後繼續執行。例如我們將上述例子加上 Return,其結果與不加相同:

PS > Function AandB([int]$x=10, [int]$y=90) {
>>  $x + $y
>>  $x - $y
>>  Return $x * $y
>>  }
>>

PS > AandB 8 2
10
6
16
           

處理管線資料的函式

管線是 Windows PowerShell 相當實用的功能,而只要處理 Windows PowerShell 的 $input 自動變數,我們自訂的函式也能加入管線的行列。$input 自動變數是管線之間的指令傳遞資料的橋樑,只要處理這個自動變數,我們的函式就能處理上一個指令透過管線傳來的資料。例如以下這個簡單的例子,等於只是接收上一個指令透過管線傳來的資料,但什麼也沒做就直接顯示出來:

PS > Function foo {
>>  $input
>>  }
>>

PS > dir | foo
			

$input 的內容端視管線的上一個指令而定,但根據 Windows PowerShell 的特性,$input 的內容通常會是集合物件,因此我們可以利用 ForEach 之類的迴圈來處理 $input 的內容,例如:

…
ForEach($item in $input) {
	$item.Name
	…
}
…
			

上述簡例可處理管線上一指令的結果,如果要讓函式執行結果透過管線傳入下一個指令,其實並不需要自己處理 $input 變數,因為 Windows PowerShell 的管線功能會自行將管線上一個指令的傳回值置入 $input 變數,例如以下這個簡單的例子,最後的執行結果是顯示 101 到 1001 等 10 個整數值:

Function foo1 {
	# 以 For 迴圈產生 1~9 等整數,並直接傳回
	For($i=1; $i -le 10; $i++) {
		# 直接傳回所產生的整數值
		$i
	}
}

Function foo2 {
	# 將$input的內容都 * 100 + 1
	foreach($item in $input) {
		$item * 100 + 1
	}
}

foo1 | foo2
			

函式的階段處理

Windows PowerShell 的函式還可分成以下 3 個處理階段的程式碼區塊:

  • Begin:只會在函式一開始時執行一次,適合放置初始動作的程式碼,例如初始變數。

  • Process:至少執行一次,但有可能重複執行數次。

  • End:只會在函式結束前執行一次。

這 3 階段並非必要,而且也不是全部都要,例如可以只取 Begin 和 Process 兩階段使用。再者,若使用函式階段處理功能,在階段區塊以外的地方,不能有任何程式碼,而且每個階段只能出現一次,否則都會造成執行錯誤。

以下這個例子說明了含階段處理的用法,例中 foo 函式有 3 階段,Begin 和 End 只會在函式執行之初及結束前執行一次,但 Process 可能會重複執行數次;此例因以管線將 dir 結果傳入 foo 函式,每傳入一次,就會讓 Process 區塊執行一次:

Function foo {
	Begin {
		"Begin…"
		# 造成空一行的效果
		" "
		$i = 1
	}

	Process {
		"Process " + $i
		$_.Name
		$i ++
		# 造成空一行的效果
		" "
	}

	End {
		"The End."
	}
}

dir | foo
			

篩選器

Windows PowerShell 的篩選器與函式類似,差別在於:

  • 篩選器的關鍵字為 Filter(函式為 Function)。

  • 若管線傳入資料,篩選器是一次收到傳入資料的一個物件,傳入篩選器的資料(物件)是放在 $_ 變數,而且每傳入一個物件,篩選器就會執行一次(相對的,函式是一次收到傳入資料的一整個集合物件,這個集合物件是放在 $input 變數)。

也就是說:管線資料是一次、而且是以集合物件($input)傳入函式,因此函式要以迴圈來處理集合物件裡的每個物件;而管線資料每次傳入一個物件($_)到篩選器,每傳入物件一次,篩選器就執行一次,直到所有的資料傳完。例如以下兩個簡單的例子:

PS > Function foo-fun {
>>  $_.Name
>>  }
>>

PS > Filter foo-fil {
>>  $_.Name
>>  }
>>
			

兩個例子都以 $_ 變數處理管線傳入的物件(顯示物件的 Name 屬性),第一個例子是函式,第二個例子是篩選器。若分別執行 dir | foo-fun和dir | fii-fil,會發現前者未能顯示任何東西,而後者顯示目前工作磁碟裡的檔案名稱。如果要 foo-fun 得到相同於 foo-fil 的結果,要改寫成以 ForEach 處理 $input。

但函式還是可以使用 $_,並且達到與篩選器相同的效果;前述函式階段處理的例子就是:管線一次將資料放在 $input,並且執行該例函式一次,但因為函式裡有 Process 程式碼區塊,因此會自動一次次的從 $input 取出 $_,並且重複執行 Process 程式碼區塊,直到 $input 裡的所有物件都處理過,而達到如下的效果:

ForEach($_ in $input)
			

也就是說,內含 Process 程式碼區塊的函式,其實就有等同於篩選器的效果。

變數範圍

本文的最後一個主題是「範圍」。與範圍關係密切的是變數、函式和篩選器,討論範圍的目的,是為了瞭解變數、函式和篩選器的「可及領域」。Windows PowerShell 變數、函式或篩選器的範圍有三種層級,如下圖:

圖 1

全域指的是 Windows PowerShell 的執行環境,也就是有提示字元、可以輸入指令或指令碼檔案的視窗。範圍的基本規則是:除非明確指定,否則只能在建立變數的範圍內讀取、修改變數,而且建立變數所在範圍的的下一層範圍,只能夠讀取上層範圍所建立變數;在上層範圍不能讀也不能改下層範圍所建立的變數。

開啟 Windows PowerShell 執行環境之後,隨即進入全域範圍。此時若執行指令碼檔案、呼叫函式、甚至再從中開啟另一個 Windows PowerShell 執行環境,都會建立新的下層範圍(或稱子系範圍)。在下層範圍可以讀取上層範圍(或稱父系範圍)建立的變數,但除非明確的指出範圍,否則不能更改上層範圍的變數。

我們以下面這個簡單的指令碼例子來說明:

# 指令碼層級
$var = "A"

Function foo {
	# 函式層級
	"2- " + $var

	$var = "B"
	"3- " + $var
}

"1- " + $var
foo
"4- " + $var
			

請務必將以上範例儲存在指令碼檔案,再於 Windows PowerShell 環境執行。執行的結果是:

1- A
2- A
3- B
4- A
			

這個指令碼檔案一開始就將 $var 變數的值指定成 A(因此 $var 是指令碼層級的變數),接著是函式 foo,然後:

  1. 執行指令碼範圍的 "1- " + $var:由於一開始就將 $var 變數的值指定成 A,所以顯示 1- A。

  2. 呼叫 foo 函式,執行 foo 函式裡的 "2- " + $var:函式或篩選器有自己的範圍,但因為指令碼變數的範圍可及函式,Windows PowerShell 的下層能讀取上層的變數,而且 $var 變數的值依然是 A,所以顯示 2- A。

  3. 執行 foo 函式裡的 $var = "B":這裡請注意,Windows PowerShell 的下層只能讀取上層的變數,但不能修改,因此如果要將 $var 變數的值改成 B,結果是建立屬於 foo 函式範圍的 $var 變數(雖然同名,但這是獨立於上層 $var 的區域變數),並將這個 $var 的值指定成 B。所以並非上層的 $var 變數被改成 B,而是另建了屬於 foo 函式層級的同名變數,而且其值為 B。

  4. 執行 foo 函式裡的 "3- " + $var:此時在foo函式已經有自己的 $var 變數,所以顯示 3- B。

  5. 離開 foo 函式、回到函式呼叫原處繼續執行 "4- " + $var:此時已跳出函式,回到上層的指令碼層級,這個範圍的 $var 變數值為 A,因此顯示 4- A。

剛才曾提及一項變數範圍的規則:除非明確指定,否則只能在建立變數的範圍內讀取或修改變數;Windows PowerShell 提供了 3 種識別變數範圍的標籤:local、global、script,利用這些標籤,就能指定要讀取或修改變數的範圍。

local 範圍就是目前的範圍,只要執行函式或指令碼,或者開啟新的 Windows PowerShell 執行環境,就會建立新的 local 範圍。global 範圍是 Windows PowerShell 開啟所建立的範圍,在下層範圍(包括指令碼或函式、篩選器)只要指明 global 標籤,就能更改 global 範圍所建立的變數(但在下層範圍不需指明 global 標籤,就可以讀取 global 範圍所建立的變數)。

script 範圍是執行指令碼所建立的範圍,當指令碼結束,該範圍也就終止。指明變數的範圍標籤,是因為在下層範圍想要更改上層範圍的變數值,因此 script 標籤是用在函式或篩選器裡。要指明變數的範圍標籤,請在變數名稱之前、$符號之後,加入適當的標籤名稱及冒號,例如指明 script 範圍的 $var 變數是為:

$script:var
			

現在我們略微調整上一個範例,將 foo 函式裡的更改 $var 變數值的陳述式指明 script 範圍,如下:

# 指令碼層級
$var = "A"

Function foo {
	# 函式層級
	"2- " + $var

	$script:var = "B"
	"3- " + $var
}

"1- " + $var
foo
"4- " + $var
			

同樣的將以上範例儲存在指令碼檔案再執行,你認為結果會有怎麼樣的變化?以下是執行結果:

1- A
2- A
3- B
4- B
			

為什麼最後一項從之前的 A 變成現在的 B?因為指明了 script 範圍,因此 $script:var = "B" 所更改的變數,是上一層指令碼範圍裡的 $var。

避免範圍困擾的方法之一,是指明變數的範圍標籤,例如:

$script:var = "init"

Function chgvar {
	$script:var

	$local:var = "function"
	$local:var

	$script:var = "script"
	$script:var
}

chgvar
$local:var
			

此外,Windows PowerShell 也提供了僅在建立範圍有效的私有變數,例如:

$var = "a"

Function foo1 {
	$var
	$private:var = "b"
	$var
	foo2
}

Function foo2 {
	$var
}

$var
foo1
$var
			

執行結果如下:

a
a
b
a
a
			

函式或篩選器範圍

同樣的範圍概念也適用於函式或篩選器。函式或篩選器的範圍僅及建立之處及下層,因此定義在指令碼檔案裡的函式或篩選器,不能用在上層提示環境。而如果上、下層範圍皆定義了同名的函式或篩選器,下層範圍會優先使用該區域所定義的函式或篩選器,除非以範圍標籤指明函式或篩選器。前述的 local、script、global、private 關鍵字不只能用在變數,也能用在函式或篩選器。

巢狀的函式

Windows PowerShell 允許函式以巢狀的形式出現,例如:

# 外部函式 A
Function A {
	Function A1 {
		# 內部函式 A1 的程式碼
	}

	Function A2 {
		# 內部函式 A2 的程式碼
	}

	函式 A 的程式碼
}
			

根據上述規則,只有在函式 A 之內才能呼叫函式 A1、A2(函式 A1、A2 彼此也能呼叫)。

結語

Windows PowerShell 提供了許多模組化的機制,目的是要便於程式碼的重複使用。本文提及了使用 Windows PowerShell 所能應用的模組化功能,包括程式碼區塊、函式或篩選器。變數、函式或篩選器的範圍則是本文另一項重點,瞭解這些規則才能完全掌握變數、函式或篩選器的使用。

顯示: