PowerShell 異常處理

sparkdev發表於2018-01-30

在使用 PowerShell 的過程中,發現它的異常處理並不像想象中的那麼直觀,所以在這裡總結一下。

Terminating Errors

透過 ThrowTerminatingError 觸發的錯誤稱為 Terminating Errors。本質上它是建立了一個異常,所以我們可以使用 catch 語句來捕獲 Terminating Errors。因此 Terminating Errors 的另外一個名字叫 Exceptions。預設情況下,Terminating Errors 不影響後面命令的執行!

把下面的程式碼儲存到檔案 exception.ps1 中:

# 下面的命令不存在
Get-TerminatingError

Write-Host 'hello world'

然後執行指令碼 exception.ps1:

注意最後輸出的 "hello world",雖然執行過程中出現了錯誤,但是錯誤後面的程式碼依然被執行了。
Terminating Errors 的特點是預設情況下你可以透過 catch 語句捕獲它,從而控制程式碼的執行流。
把下面的程式碼儲存到檔案 exception.ps1 中:

Try{
    # 下面的命令不存在
    Get-TerminatingError
}
Catch{
    Write-Host 'got you'
    exit 1
}
Write-Host 'hello world'

然後執行指令碼 exception.ps1:

這樣就好多了,我們預測了程式碼中可能產生的問題,並且在發生了錯誤的情況下果斷的結束了指令碼的執行。

雖然 Terminating Errors 不能中斷指令碼的執行,但是卻可以中斷 pipeline 的執行。從 Terminating Errors 的文件中我們可以看到,其內部丟擲了 PipelineStoppedException 異常,並且 PowerShell 內部處理了這個異常。所以正在執行的 pipeline 會被中斷掉,然後繼續執行後面的其它程式碼。

Non-Terminating Errors

透過 Write-Error 觸發的錯誤稱為 Non-Terminating Errors。本質上它只是把錯誤資訊寫入了輸出流中而沒有產生異常,所以預設情況下 catch 語句無法捕獲 Non-Terminating Errors。預設情況下,Non-Terminating Errors 不影響後面命令的執行!

把下面的程式碼儲存到檔案 exception.ps1 中:

# C:\xxx 不存在
Copy-Item C:\xxx
Write-Host 'hello world'

然後執行指令碼 exception.ps1:

注意最後輸出的 "hello world",雖然執行過程中出現了錯誤,但是錯誤後面的程式碼依然被執行了。

Non-Terminating Errors 的特點是預設情況下 catch 語句無法捕獲它!
把下面的程式碼儲存到檔案 exception.ps1 中:

Try{
    # C:\xxx 不存在
    Copy-Item C:\xxx
}
Catch{
    Write-Host 'got you'
    exit 1
}
Write-Host 'hello world'

然後執行指令碼 exception.ps1,結果和上面是一樣的,錯誤後面的程式碼依然會被執行。

ErrorAction 選項的秘密

Non-Terminating Errors 預設只是透過 Write-Error 把錯誤資訊寫入了輸出流中而沒有產生異常,所以 catch 語句無法捕獲 Non-Terminating Errors。但是我們卻可以透過 -ErrorAction 選項改變 WriteError 的預設行為。
ErrorAction 選項主要用來改變命令的 non-terminating errors 的行為(它不會對 Terminating Errors 產生影響)!
ErrorAction 選項的工作原理為:用指定的引數覆蓋當前命令的 $ErrorActionPreference 變數。預設情況下  $ErrorActionPreference 變數的值為 Continue。

可以為 -ErrorAction 選項指定下面的引數:

-ErrorAction[:{Continue | Ignore | Inquire | SilentlyContinue | Stop | Suspend }]

它們表示的含義如下:
Continue   顯示錯誤資訊並繼續執行後面的命令,這是預設值。
Ignore       這個值是在 PowerShell 3.0 引入的。它不顯示錯誤資訊並繼續執行後面的命令。與 SilentlyContinue 不同的是,它也不會把錯誤資訊新增到 $Error 變數中。
Inquire      顯示錯誤資訊並彈框與使用者互動。
SilentlyContinue   不顯示錯誤資訊並繼續執行後面的命令。
Stop          顯示錯誤資訊並且退出指令碼的執行。
Suspend    這個值只適用於 workflow。當 terminating error 發生時執行會暫停下來,然後決定是否恢復執行。

這裡我們重點關注使用比較多的 stop, 它會讓 Write-Error 等原本產生 non-terminating error 的命令產生 terminating error!所以我們就可以用 catch 語句來捕獲異常了。把下面的程式碼儲存到檔案 exception.ps1 中:

Try{
    # C:\xxx 不存在
    Copy-Item C:\xxx -ErrorAction Stop
}
Catch{
    Write-Host 'got you'
    exit 1
}
Write-Host 'hello world'

注意我們為 Copy-Item 命令新增了 -ErrorAction Stop 選項,然後執行指令碼 exception.ps1:

哈哈,這次捕獲到異常了,並且最後也沒有輸出 "hello world"。

如果需要給每個命令都新增 -ErrorAction 選項可不是什麼好玩的事情,好在我們可以在指令碼中設定 $ErrorActionPreference 變數來完成同樣的功能:

$ErrorActionPreference = 'Stop'

這樣在當前的指令碼中,所有原本的 non-terminating error 都會變成 terminating error。

Try/Catch/Finally

在異常處理中不介紹 Try/Catch/Finally 語句是不完整的。Catch 語句用來捕獲 Try 塊中產生的異常,當然我們可以指定只捕獲那些我們感興趣的異常。
Finally 塊也是非常重要的,它能保證一些必要的邏輯被執行,比如釋放資料庫連線。下面的 demo 演示瞭如何在指令碼中設定 $ErrorActionPreference 變數:

$eap = $ErrorActionPreference
Try{
    $ErrorActionPreference = 'Stop'
    # do something
}
Catch{
    Write-Host "error !"
    Exit 1
}
Finally{
    $ErrorActionPreference = $eap
}

在 Finally 塊中把 $ErrorActionPreference 變數還原,保證我們自己設定的 $ErrorActionPreference 變數隻影響 Try/Catch 塊中的語句。

$PSItem 變數

$PSItem 是一個 ErrorRecord 型別的變數,它會儲存異常的詳細資訊。在捕獲到異常時,我們可以把異常相關的資訊輸出或儲存到日誌中:

$eap = $ErrorActionPreference
Try{
    $ErrorActionPreference = 'Stop'
    # 下面的命令不存在
    Get-TerminatingError
}
Catch{
    # 比較簡潔的資訊
    Write-Output $PSItem.ToString()    
}
Finally{
    $ErrorActionPreference = $eap
}

$PSItem.ToString() 中只有錯誤的描述,看起來會比較簡潔。執行上面的指令碼:

紅框中的資訊就是 $PSItem.ToString() 提供的。
僅有錯誤資訊並不總是能夠很好的幫助調查問題的根源,如果有出錯的行號和異常堆疊會好很多:

$eap = $ErrorActionPreference
Try{
    $ErrorActionPreference = 'Stop'
    # 下面的命令不存在
    Get-TerminatingError
}
Catch{    
    # 包含堆疊的資訊
    Write-Output $PSItem
}
Finally{
    $ErrorActionPreference = $eap
}

這次我們直接輸出了 $PSItem,執行上面的程式碼會得到下面的輸出:

這下好多了,有了出錯的行號和呼叫堆疊就可以很容易的看到出錯的原因。

結論

作為一門指令碼語言,PowerShell 對異常的支援還是非常強大的。不過像 Terminating Errors 和 Non-Terminating Errors 這樣的特性也會給我們帶來不少的困擾。希望本文介紹的內容可以幫助大家更好的理解 PowerShell 中異常相關的概念。