1. 程式人生 > >Windows PowerShell 2.0語言開發之指令碼塊

Windows PowerShell 2.0語言開發之指令碼塊

指令碼塊是重要的程式設計結構,是PowerShell重要的摘要和重用程式碼的機制,學習指令碼塊的最終目標是掌握各種重用程式碼的方法,如別名程式提供和指令碼檔案。這些技術都很重要,因為它們是逐步建立複雜指令碼的基礎。

定義指令碼塊

定義指令碼塊只需要把一些程式語句用花括號({})括起,它不會立即執行,取而代之的是建立和返回一個新的指令碼塊物件。下面是建立的第1個指令碼塊:

PS C:/> {Write-Host "This  sentence will not be print out."}
Write-Host "This  sentence will not be print out"
PS C:/>  Write-Host "This  sentence will be print out."
This  sentence will be print out

兩個語句的主要區別在於花括號,它使指令碼塊中的Write-Host語句沒有被執行,得到的是這些字串組成的指令碼塊物件。如果不為其儲存一個引用物件,這個語句沒有意義。下例將一個建立的指令碼塊賦值給一個變數:

PS C:/> $HelloWorldBlock={Write-Host "Hello World from a Script Block"}
PS C:/>

檢視指令碼塊變數的型別:

PS C:/> $HelloWorldBlock.GetType().FullName
System.Management.Automation.ScriptBlock

可以看到的指令碼塊物件是System.Manage.Automation的例項。作為標準的.NET類,指令碼塊也是PowerShell基礎結構,因此可以如同.NET物件那樣來處理它。下例獲取指令碼塊的屬性和方法:

PS C:/> $HelloWorldBlock | Get-Member

   TypeName: System.Management.Automation.ScriptBlock

Name                 MemberType Definition
----                 ---------- ----------
Equals               Method     System.Boolean Equals(Object obj)
GetHashCode          Method     System.Int32 GetHashCode()
GetNewClosure        Method     System.Management.Automation.
ScriptBlock GetNewClosure()
GetPowerShell        Method     System.Management.Automation.
PowerShell GetPowerShell(Params Object[] args)
GetSteppablePipeline Method     System.Management.Automation.
SteppablePipeline GetSteppablePipeline()
GetType              Method     System.Type GetType()
Invoke               Method     System.Collections.ObjectModel.
Collection`1[[System.Management.Automation.PSObject, ...
InvokeReturnAsIs  Method  System.
Object InvokeReturnAsIs(Params Object[] args)
ToString             Method     System.String ToString()
File                 Property   System.String File {get;}
IsFilter             Property   System.
Boolean IsFilter {get;set;}
Module      Property   System.Management.Automation.
PSModuleInfo Module {get;}
StartPosition  Property System.Management.Automation.
PSToken StartPosition {get;}

可以通過上面的屬性清單瞭解PowerShell使用指令碼塊的方法。在執行時PowerShell會解析指令碼程式碼建立指令碼塊物件,並呼叫物件的Invoke和InvokeReturnAsIs方法。

可以通過在指令碼塊名字首之前新增引用操作符(&)引用指令碼塊,下例通過指令碼塊變數來呼叫指令碼:

PS C:/> $HelloWorldBlock={Write-Host "Hello World from a Script Block"}
PS C:/> &$HelloWorldBlock
Hello World from a Script Block

引用操作符不僅可以和變數配合使用,也是一個表示式操作符,可以在指令碼塊定義時使用。如下面宣告定義一個匿名指令碼塊並且執行:

PS C:/> &{Write-Host "Hello World from a Anonymous Script Block"}
Hello World from a Anonymous Script Block

指令碼塊是編譯後的可以被傳遞和多次執行的物件,可以指定變數指向記憶體中的指令碼塊。下例說明如何多次執行指令碼塊,並通過不同的變數來訪問它:

PS C:/> $Block1={Write-Host "Block executed"}
PS C:/> $Block2=$Block1
PS C:/> for($i=0;$i-lt 3;$i++)
>> {&$Block1
>> &$Block2
>> }
>>
Block executed
Block executed
Block executed
Block executed
Block executed
Block executed

能夠看到6條指令碼塊執行的提示資訊,迴圈體被執行了3次,每次都執行了變數$Block1和$Block2指向的指令碼塊。兩個變數是指向同一個指令碼塊物件,所以指令碼塊執行了6遍。

返回值和引數

從指令碼塊中給出返回值需要輸出包含不被cmdlet和其他表示式接收的物件,下例是返回數字的指令碼塊:

PS C:/> $number = {5}
PS C:/> &$number 
5
PS C:/> 1+ (&$number)
6

可以從上例中看到最後一個命令,返回值可以在一定條件下用於其他表示式,為此需要使用圓括號括起返回值包含。在使用巨集的表示式中需要注意括號的使用,不然可能會因為構造的表示式有誤而造成錯誤。在上例中如果沒有括號,加號會把&作為其他右側的運算元,而丟擲異常。

PS C:/> 1 + &$number
You must provide a value expression on the right-hand side of the '+' operator.
At line:1 char:4
+ 1 + <<<<  &$number
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ExpectedValueExpression

需要注意的是輸出物件不會終止指令碼塊中後續語句的操作,餘下的語句將會繼續執行。如在返回數值後在控制檯視窗中輸出一個字串:

PS C:/> $numberPrint  = {5;Write-Host "generated a number" }
PS C:/> &$numberPrint
5
generated a number
PS C:/> $result = &$numberPrint
generated a number
PS C:/> $result
5

$numberPrint語句塊返回值並向控制檯列印字串,匿名語句塊表面上看起來同時輸出了賦值的5,還有字串。但是實際的返回值只是5,列印到控制檯的字串是在賦值時產生的伴生品。如果將$numberPrint呼叫賦值給其他變數,這個變數就擁有了$numberPrint的返回值,在賦值過程中可以看到已經列印字串,在賦值後使用$result列印的才是實際的返回值。

為了結束執行並終止指令碼塊,可以使用return語句,它將終止執行並返回值。下例中使用return語句終止Write-Host命令的輸出:

PS C:/> $numberPrint  = {return 5;Write-Host "generated a number" }
PS C:/> $a = &$number
PS C:/> $a
5

這樣可以使用return語句直接退出指令碼塊並終止執行,該語句並不嚴格要求使用者提供返回值。如果省略,指令碼塊將在不返回任何值的情況下退出。這種情況下,指令碼塊將會給呼叫者返回return語句之前的輸出內容,如下例所示:

PS C:/> $noReturn = {return; 4}
PS C:/> &$noReturn
PS C:/> $numberReturn = {4;return}
PS C:/> &$numberReturn
4
PS C:/> $writeStringReturn = {Write-Host "output string";return}
PS C:/> &$writeStringReturn
output string
PS C:/> $b = &$writeStringReturn
output string
PS C:/> $b
PS C:/> $stringReturn = {"output string";return}
PS C:/> &$stringReturn
output string
PS C:/> $b = &$stringReturn
PS C:/> $b
output string

呼叫$noReturn語句塊能看出return語句使後面的4未起作用;呼叫$numberReturn語句塊可以看出return語句在沒有返回值的情況下,其前面的輸出4成為返回值。$writeStringReturn語句塊驗證了return語句前的Write-Host輸出的字串並不會作為語句塊的返回值,Write-Host的作用只是向控制檯輸出要列印的內容,並不會以賦值的形式傳遞給其他變數。對比$stringReturn語句塊呼叫和上面的$writeStringReturn語句塊,可以看出return語句是將前面的字串作為返回值輸出。

上例中的return語句前面只有一個語句,事實上PowerShell中可以有多個語句作為返回值,這是和其他程式語言不同之一。PowerShell的返回值可以是多個值,如果在迴圈中多次執行,結果會是返回一個數組,下面是單個指令碼塊返回多個數字組成的陣列例項:

PS C:/> $numbers = {1;2;3;}
PS C:/> &$numbers
1
2
3
PS C:/> (&$numbers).GetType().FullName
System.Object[]

需要強調的是可以不顯式地返回一個數組,為此使用逗號作為數字分隔符,而不是使用分號。分號是作為語句的終結符,結果為3個數字組成的陣列。

如果指令碼塊能夠對外部有效,則其需要能夠從外部獲取引數,引數基於其傳遞並解析的順序和位置。指令碼塊擁有被稱為“$args”的預定義變數存在,這個變數由PowerShell的外殼自動定義,用來接收使用者傳遞到指令碼塊的引數的集合。下例演示$args的使用方法:

PS C:/> $greeter = {
>> $firstName = $args[0]
>> $lastName = $args[1]
>> Write-Host "Hello,$firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:/> &$greeter
Hello,  Welcome to the world of PowerShell
PS C:/> &$greeter "Jim" "Green"
Hello,Jim Green Welcome to the world of PowerShell
PS C:/> &$greeter "Liu" "Tao"
Hello,Liu Tao Welcome to the world of PowerShell

通過索引訪問引數是個不錯的功能,很多語言中均提供該功能。它適合在很簡單的應用環境下使用。也可用在定義指令碼塊時,並不清楚引數數目的應用場景下。但如果傳遞的引數比較多,這個功能會使得程式碼塊有更多出錯的可能性。在絕大多數情況下可以在指令碼塊內部使用param關鍵字宣告變數,來使用命名引數。下例是前一個例子用命名引數重寫後的形式:

PS C:/> $greeter = {
>> param ($firstName,$lastName)
>> Write-Host "Hello,$firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:/> &$greeter "Liu" "Tao"
Hello,Liu Tao Welcome to the world of PowerShell

這樣的寫法更為清晰,但還是有些不方便,因為是需要記憶引數的順序。為了解決這個問題,可以使用引數名開關來關聯引數名和引數值,如:

PS C:/> &$greeter "Liu" "Tao"
Hello,Liu Tao Welcome to the world of PowerShell
PS C:/> &$greeter -firstName "Liu" -lastName "Tao"
Hello,Liu Tao Welcome to the world of PowerShell
PS C:/> &$greeter -lastName "Tao" -firstName "Liu"
Hello,Liu Tao Welcome to the world of PowerShell

如果定義的引數名很長,這樣的方法也不太方便。為此,可以縮寫引數名,PowerShell外殼將會自動識別這些引數,如:

PS C:/> &$greeter -last "Tao" -first "Liu"
Hello,Liu Tao Welcome to the world of PowerShell
PS C:/> &$greeter -l "Tao" -f "Liu"
Hello,Liu Tao Welcome to the world of PowerShell

需要強調的是這種省略至少需要引數名的首字母就能夠滿足要求。如果引數名開頭的幾個字母相同,則需要提供足夠多的引數名字母,直到能讓PowerShell的外殼區分體提供的是哪個引數。

可以使用param語句強制將引數轉換為需要的型別。如果指令碼塊呼叫者提供了可以轉換到目標型別的引數,PowerShell外殼將會自動轉換。與此同時,還可以在程式碼塊中顯式指明當引數型別轉換失敗時丟擲錯誤。這個特性可以看做是簡單的錯誤處理。下例在兩個數相加時使用引數型別定義轉換值型別:

PS C:/> $sum = {
>> param([int]$a,[int]$b)
>> $a + $b
>> }
>>
PS C:/> &$sum 4 5
9
PS C:/> &$sum "4" 5
9
PS C:/> &$sum "not a number" 5
param([int]$a,[int]$b)
$a + $b
 : Cannot process argument transformation 
on parameter 'a'. Cannot convert value 
"not a number" to type "System.Int32".
 Error: "Input string was not in a correct format."
At line:1 char:2
+ & <<<< $sum "not a number" 5
    + CategoryInfo          : 
InvalidData: (:) [], ParameterBindin...mationException
    + FullyQualifiedErrorId : 
ParameterArgumentTransformationError

$sum指令碼塊求兩個整型數字的和。傳遞字串會觸發自動的型別轉換。如果不成功,將得到型別不符的錯誤提示。

當指令碼塊的呼叫者沒有提供所有的引數時丟失的引數會被初始化為$null,可以用這個特性檢查是否已經被傳遞某個引數。下例在呼叫者沒有提供給$lastName引數值時使用“Unknown”預設值:

PS C:/> $greeter = {
>> param ([string] $firstName,[string]$lastName)
>> if(!$lastName)
>> {
>>    $lastName = "Unknown"
>> }
>> Write-Host "Hello, $firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:/> &$greeter "Liu" "Tao"
Hello, Liu Tao Welcome to the world of PowerShell
PS C:/> &$greeter "Liu"
Hello, Liu Unknown Welcome to the world of PowerShell

上面的方法可以向缺失引數的指令碼塊呼叫提供預設值,但這並不是最好的方法。因為如果在指令碼塊中存在很多引數需要判斷引數值是否缺失,則程式碼中會有大量的重複程式碼用於判斷傳遞的引數值是否為空。PowerShell提供了引數預設值的簡化符號,為引數塊賦值意味著如果呼叫者沒有提供引數值,引數會被初始化為預設值。下例會為$greeter指令碼塊的$lastName賦予預設值:

PS C:/> $greeter = {
>> param ([string] $firstName,[string] $lastName = "Unknown")
>> Write-Host "Hello,$firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:/> &$greeter "Liu" "Tao"
Hello,Liu Tao Welcome to the world of PowerShell
PS C:/> &$greeter "Liu"
Hello,Liu Unknown Welcome to the world of PowerShell

引數預設值能包含任何表示式,可以使用這一特性實現強制引數,強制引數指呼叫者必須提供給指令碼塊的引數。為此需要新增丟擲異常的表示式,一旦呼叫者不能正常提供強制引數,就會丟擲異常。下例在$greeter中將$firstName引數設定為強制引數:

PS C:/> $greeter = {
>> param ($firstName =$(throw "firstName required!"),$lastName)
>> Write-Host "Hello,$firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:/> &$greeter -lastName "Tao"
firstName required!
At line:2 char:27
+ param ($firstName =$(throw <<<<  "firstName required!"),$lastName)
    + CategoryInfo          : OperationStopped: (firstName required!:String) [], RuntimeExceptio
    + FullyQualifiedErrorId : firstName required!

在這裡只需要知道$(throw “Error message”)的格式用來實現強制引數,將會在第11章中詳細討論異常和錯誤處理。

處理管道輸入

在前面的章節中曾經涉及向cmdlet傳遞指令碼塊並在管道中的每個元素呼叫它們。下例演示傳遞指令碼塊給ForEach-Object並獲取為所有文字檔案最後的寫入時間:

PS C:/> dir *.txt | ForEach-Object {$_.LastWriteTime.Date}
2009-1-3 10:09:25
2009-1-3 22:52:23
2009-1-6 14:52:45
2009-1-8 08:42:31

每個指令碼塊包含3個段來做用於管道輸入,即begin, process和end。每個段看起來像是巢狀的塊,包含零到多個語句。最重要的段是process,它會被管道中的每個物件呼叫。為了方便,當前的物件被標記為$_變數。下例使用process段過濾數字集合並返回比5大的數字:

PS C:/> $greaterThanFive = {
>> process {
>>          if ($_ -gt 5)
>>             {
>>               return $_
>>             }
>>          }
>> }
>>
PS C:/> 2,3,4,5,6,7 | &$greaterThanFive
6
7

與process類似,可以嵌入begin和end段。它們分別在管道程序開始之前及結束之後執行,主要用於計算集合統計資訊或保持用於建立返回值的狀態。下例將演示如何建立用於對管道中傳入的所有數字求和指令碼塊:

PS C:/> $sum = {
>> begin {
>>        $total = 0
>>       }
>> process {
>>       $total +=$_
>>       }
>> end {
>>      return $total
>>     }
>> }
>>
PS C:/> 1,3,5,7,8 | &$sum
24

其中的process段將當前值疊加到求和值$total上,返回值的操作在end段中實現。在process段中使用了+=操作符,用於獲取右側的變數值相加到左側的變數上。計算$total值通過反覆疊加當前值,並把$_變數傳遞給$total變數。

指令碼塊既能接收引數,也能接收管道輸入。在當前作用域和$args集合中命名引數和位置引數有效,而管道物件是被傳遞到$_變數中。為了示範如何建立引數和管道輸入,下例累加管道中的所有元素:

PS C:/> $adder = {
>> param($number)
>> process{
>>         $_ +$number
>>        }
>> }
>>
PS C:/> 1,3,5  | &$adder 10
11
13
15

上例將操作的方法和具體的應用剝離開,便於讀者理解。下例計算特定日期和當前目錄下文字檔案最後修改時間之間的天數:

PS C:/> $dateDiff = {
>> param([datetime]$referenceDate)
>> process {
>>         ($referenceDate - $_.LastWriteTime.Date).Days
>>         }
>> }
>>
PS C:/> dir *.txt | &$dateDiff([datetime]::Today)
5
7
13
22
84

需要強調的是對於[datetime]:Today使用了圓括號。如果沒有括號,DateTime物件被轉換為字串後比較時會被再次轉換為DateTime物件。這兩次轉換使得程式碼效率降低並可能因為datetime字串在不同語言作業系統中表示方法的不同而使得轉換出現異常。

將字串作為表示式呼叫

幾乎所有的指令碼語言都有特殊的函式或操作來將輸入的字串作為程式碼塊編譯並執行,PowerShell中的Invoke-Expression的cmdlet實現該功能。字串可以作為引數傳遞或者從其他命令中用管道傳遞,下例演示如何使用Invoke-Expression:

PS C:/> Invoke-Expression "Write-Host 'invoke expression'"
invoke expression

需要強調的是在語句中需要對引號進行轉義或者在字串中使用單引號的,這樣可以在執行時操作字串並且建立高度動態而優雅的指令碼。下例生成多個命令並用管道傳遞給Invoke-Expression執行:

PS C:/> 1..5 | foreach {"Write-Host 'Got $_'"}| Invoke-Expression
Got 1
Got 2
Got 3
Got 4
Got 5

呼叫表示式與建立並執行指令碼塊相似,Invoke-Expression的作用形式也是類似的。它會建立並執行新的指令碼塊,傳遞引數並通過管道將返回值給下一個命令。需要謹記的是,與指令碼塊不同的是Invoke-Expression不會建立子變數作用域。它總會在當前的作用域中執行,如下:

PS C:/> $name = "LiuTao"
PS C:/> Invoke-Expression "`$name = 'WangLei'"
PS C:/> $name
WangLei

正如上例中所示,$name變數被修改,這個字串在當前作用域中執行。在建立命令字串時使用了兩個引號,所以需要對美元符轉義以傳遞到Invoke-Expression中的字串包含變數名,而不是被展開的變數值。

使用Invoke-Expression的最大好處是很容易將字串轉換為可執行的命令,下例演示如何使用cmdlet建立允許使用者輸入表示式的計算程式:

:PS C:/> $expression = Read-Host "Enter expression"
Enter expression: 5*6-4
PS C:/> $result = Invoke-Expression $expression
PS C:/> Write-Host $result
26

由於尚未對輸入進行校驗,所以任何輸入的程式碼將會被執行,如果使用者輸入一下表達式4*6;del C:/-recurse –force,則將會丟失系統盤所有的資料。正確的做法是需要驗證使用者輸入,避免Invoke-Expression執行任何尚未過濾危險操作的表示式。

另外一種執行字串的方法是通過使用全域性變數$ExecutionContent,它是System.Management.Automation.EngineIntrinsics型別,其中包含一個屬性InvokeCommand,是System.Management.Automation.CommandInvocationIntrinsics型別。可以使用Invoke-Command的InvokeScript方法來實現Invoke-Expression所做的一切:

PS C:/> $ExecutionContext.InvokeCommand.InvokeScript("Write-Host 'invoke'")
invoke

$ExecutionContext.InvokeCommand物件很有用,它允許使用者把字串編譯為指令碼塊。可以用來建立稍後執行的動態命令,如:

PS C:/> $cmd = "Write-Host `"`$(`$args[0])`""
PS C:/> $cmdBlock = $ExecutionContext.InvokeCommand.NewScriptBlock($cmd)
PS C:/> &$cmdBlock "test"
test

雖然能夠儲存字串並使用Invoke-Expression呼叫它來執行,但是指令碼塊的程式碼只在建立時編譯一次,而Invoke-Expression會在每次執行行編譯。這樣如果指令碼內容很多,編譯時間會很長;另外,指令碼塊會建立子變數作用域,如果要在當前作用域中保護變數值,子變數作用即可實現。通常處理巢狀作用域可以將父作用域變數宣告為script或global作用域,或在指令碼塊需要修改變數時用Get-Variable和Set-Variable改變內容。

指令碼塊作為委託

委託是.NET事件處理機制的重要組成部分。簡而言之,如果方法不是靜態的,委託即用於儲存.NET類、方法引用和物件引用的物件。當呼叫委託時,它指向物件的方法會被執行。NET中的事件工具指向事件接收物件的委託和方法,傳遞這些委託到事件觸發者。當事件發生時,事件觸發者呼叫事件委託。

指令碼塊和委託有很多相似點,二者都是指向一些程式碼並呼叫執行。PowerShell的型別系統允許將指令碼塊轉換為委託。只有System.EventHandler型別的委託是支援的,即只有帶物件和System.EventArgs例項為引數的指令碼塊可以被轉換為委託。下例演示如何宣告指令碼塊並轉換為委託:

PS C:/> $handler = {
>> param($sender,[EventArgs] $eventArgs)
>> Write-Host "Event raised!"
>> }
>>
PS C:/> $delegate = [EventHandler] $handler
PS C:/> $delegate.GetType().FullName
System.EventHandler

可以通過使用Invoke()方法呼叫委託:

PS C:/> $delegate.Invoke(3,[EventArgs]::Empty)
Event raised!

可以建立Windows窗體程式來模仿圖形輸入框,允許使用者輸入。為此,需要建立Form物件並在其中增加TextBox和Button控制元件繫結Button的Click事件處理。Click事件處理將會關閉視窗,並把TextBox中的值儲存到全域性變數,以下是程式碼:

PS C:/> $null = [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
PS C:/> $form = New-Object Windows.Forms.Form
PS C:/> $form.Size = New-Object Drawing.Size -arg 300,85
PS C:/> $textBox = New-Object Windows.Forms.textBox
PS C:/> $textBox.Dock = "Fill"
PS C:/> $form.Controls.Add($textBox)
PS C:/> $button = New-Object Windows.Forms.Button
PS C:/> $button.Text = "Done"
PS C:/> $button.Dock = "Bottom"
PS C:/> $button.add_Click(
>> {$global:resultText = $textBox.Text;$form.Close()})
>> $form.Controls.Add($button)
>> [Void] $form.ShowDialog()
>> Write-Host $global:resultText
>>
Hello ,Windows Form in PowerShell!

在第1行中PowerShell沒有預設載入System.Windows.Forms的彙編物件。建立Form物件,並設定視窗大小為300畫素寬,85畫素高。然後新增充滿整個視窗寬度的TextBox控制元件,即水平方向使用所有可用空間。最後新增Button控制元件,其中包含呼叫add_Click方法,這個方法把委託作為引數並繫結Click事件。需要強調的是在呼叫方法時,不需要顯式丟擲,PowerShell自動完成。程式碼執行後顯示如圖1所示的輸入框視窗。

2031831

在其框中輸入“Hello ,Windows Form in PowerShell!”,單擊“Done”按鈕,能夠看到控制檯輸出“Hello ,Windows Form in PowerShell!”。說明PowerShell已經接受了Form物件的事件委託,即將Form接收的引數傳遞給PowerShell處理。

作者: 付海軍
出處:http://fuhj02.blog.csdn.net
版權:本文版權歸作者和csdn共有
轉載:歡迎轉載,為了儲存作者的創作熱情,請按要求【轉載】,謝謝
要求:未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
個人網站: http://txj.shell.tor.hu/