Windows PowerShell 2.0 代碼調試并排除bug
沒有任何腳本或程序能夠保證在任何情況下毫無錯誤地執行,在外界條件變化的情況下,需要預防可能出錯之處。本文將著重講解如何調試PowerShell代碼,即查找并排除bug,這是每個開發人員都應該熟練掌握的技術。在本文將介紹PowerShell提供的解決方法,以及診斷和預防錯誤的方法,以使程序更加健壯和穩定。
大多數編程語言和環境提供了完整調試特性的系統,可以逐步跟蹤程序的執行,保證所有的執行過程符合預定的算法。然而PowerShell并沒有提供腳本調試程序,這樣開發人員必須采取其他手段來調試錯誤。可以通過一些巧妙的手段貼近于對程序的逐步調試,臨時掛起執行流并檢查程序狀態。
對于一些復雜的任務,通常情況下會將程序細化分解為多個相對獨立的子模塊來檢查各自的功能。分解后的模塊之間要盡可能不包含過多耦合,這樣才不會在調試時造成太多的麻煩。
1、打印調試
通過打印來調試程序是最原始且很有效的調試方法,盡管這種方法相對比較繁瑣,但是這是開發人員編程所需的基本功。打印調試檢查程序的執行狀態,并在特定的執行流中添加注入代碼打印當時系統的相關信息,然后將這些信息和預期的信息進行比較。這是一種強大的調試方法,基本上適用于各種編程語言。在PowerShell中需要檢查的內容包括腳本變量、環境變量、文件內容和注冊表鍵值等,潛在的威脅是容易失去控制或者引入運行時的邏輯錯誤。有效控制的關鍵在于保持注入的 輸出語句邏輯簡單,并分模塊調試。
最方便的打印調試工具是Write-Host,它將輸出對象或字符串直接輸出到控制臺,而不會傳遞到對象管道中。該工具可以輸出各種類型的對象和集合,使用不同的顏色區分內容并通知用戶當前操作的危險系數。如用綠色表示診斷信息,紅色表示警告等。這是通過設定BackgroundColor和ForegroundColor屬性實現的,這兩個屬性的取值包括Black、DarkBlue、DarkGreen、DarkCyan、DarkRed、DarkMagenta、DarkYellow、Gray、DarkGray、Blue、Green、Cyan、Red、Magenta、Yellow和White。
創建名為“Prite-Debug.ps1”的腳本文件計算并分別用特殊的前景色和背景色顯示文本文件中的字符數量。在該腳本中定義名為“Count-Characters”的函數,代碼如下:
{
Write-Host "Opening $file" -Background White -Foreground DarkGreen
$content = ""
if (Test-Path $file)
{
$content = Get-Content $file
}
Write-Host "File contains $($content.Length) characters" `
-Background White -Foreground DarkGreen
}
Count-Characters Print-Debug.ps1
Count-Characters nosuchfile.txt
其中的函數默認定義Content字符串,首先使用Test-Path cmdlet檢查文件是否存在。如果存在,則使用Get-Content cmdlet讀取文件的內容。函數中用白色的背景色和深綠色的前景色分別在打開文件前及讀取文件后輸出兩行診斷信息,腳本執行結果如圖1所示。
(1)生成詳細輸出
很多時候需要輸出一些用戶并不需要看到的診斷信息為此。而且會使控制臺屏幕雜亂無章,為此需要有方法生成易于打開和關閉的診斷信息。PowerShell提供了適于診斷的cmdlet,包括Write-Verbose、Write-Warning和Write-Debug。Write-Verbose的用法如圖2所示。
因為默認情況下關閉Write-Verbose輸出,所以第1行中的Write-Verbose執行后并沒有將字符打印到控制臺。如果需要打印到控制臺,則如圖中第3行所示在cmdlet后增加-Verbose參數。由于每個輸出需要在調試時刪除或增加-Verbose參數,所以并沒有減少工作量。PowerShell提供了全局變量$VerbosePreference,默認值為SilentlyContinue。為了能夠看到輸出,需要將其改為Continue,如圖2所示的最后兩個命令。
$VerbosePreference的取值還包括Stop和Inquire,為Stop,Shell將會生成停止錯誤并會阻止剩余代碼的執行;為Inquire,Shell將會詢問用戶如何執行下一步操作,如圖3所示。
使用Write-Verbose的好處是可以在需要時執行一條命令打開所有的調試信息,而在不需要顯示時也可方便地關閉,常用的兩個值即打開和關閉輸出的Continue和SilentlyContinue。
修改Count-Characters函數,在調用之后立即生成詳細的調試信息。將腳本保存為“Print-DebugVerboseOn.ps1”文件,代碼如下:
{
$functionName = $MyInvocation.MyCommand.Name
Write-Verbose "Entering $functionName"
Write-Host "Opening $file" -Background White -Foreground DarkGreen
$content = ""
if (Test-Path $file)
{
$content = Get-Content $file
}
Write-Host "File contains $($content.Length) characters" `
-Background White -Foreground DarkGreen
Write-Verbose "Leaving $functionName"
}
Count-Characters Print-Debug.ps1
Count-Characters nosuchfile.txt
其中包含調試信息,在調試時只需要將變量$VerbosePreference的值設置為Continue。如果要關閉調試信息,則設置為SilentlyContinue,腳本執行結果如圖4所示。
上述代碼具有較強的診斷信息功能,但是在調試時需要將其放置在調試的代碼中,工作量較大。為解決這個問題,創建名為“Print-DebugWithInstrument.ps1”腳本。在其中定義一個名為“Instrument-Function”的函數,使用腳本塊封裝原始的函數體。該函數包含用于診斷的代碼,并在調用原始函數體前后分別生成詳細的輸出信息。這些調試信息需要繼續放在源代碼中,代碼如下:
{
$parentInvocation = Get-Variable MyInvocation -scope 1 -ValueOnly
$functionName = $parentInvocation.MyCommand.Name
Write-Verbose "Entering $functionName"
&$body
Write-Verbose "Leaving $functionName"
}
function Count-Characters($file)
{
Instrument-Function {
Write-Host "Opening $file" `
-Background White -Foreground DarkGreen
$content = ""
if (Test-Path $file)
{
$content = Get-Content $file
}
Write-Host "File contains $($content.Length) characters" `
-Background White -Foreground DarkGreen
}
}
Count-Characters Print-Debug.ps1
Count-Characters nosuchfile.txt
可以看到Count-Character函數的定義和之前的代碼基本相似,唯一不同是原始代碼在腳本塊中封裝,然后傳遞給Instrument-Function處理。Instrument-Function需要函數名生成診斷輸出信息,這是通過Get-Variable cmdlet在父作用域中查找調用信息來實現的。上述代碼的執行結果與上個實例相同,這樣巧妙地輸出了詳細信息。由于新的腳本塊中不包含原始自動變量$Args,所以無法作用于未命名函數參數。
(2)生成調試輸出
調試輸出是腳本調試信息同比較特殊的一種,不同于前面的詳細輸出信息,調試信息只是適用于腳本作者。即使是高級用戶也很難從這些調試信息中獲取到所需的有用信息,因為調試信息會包含內在的錯誤、中間變量的內容及其他有助于腳本作者找出bug和異常的信息。
可以通過Write-Debug cmdlet生成調試信息,如圖5所示。
默認關閉調試信息,如果要打開,需要在腳本最后添加-Debug參數。當Shell收到調試輸出的請求時,將會向用戶確認下一步的操作。與$VerbosePreference類似,Write-Debug也有一個用于控制的對象$DebugPreference,默認值也為SilentlyContinue。在上一個實例中通過添加-Debug參數得到確認信息,即將該對象的值設置為Inquire,并將其設置為Continue來獲取簡單的信息。
為了通過擴展Instrument-Function來捕獲所有可能由函數體拋出的錯誤,創建名為“Print-DebugOutput.ps1”腳本。在其中修改Count-Characters函數在文件不存在的情況下拋出異常,代碼如下:
{
$parentInvocation = Get-Variable MyInvocation -scope 1 -ValueOnly
$functionName = $parentInvocation.MyCommand.Name
trap
{
Write-Debug "$functionName raised an error"
}
Write-Verbose "Entering $functionName"
&$body
Write-Verbose "Leaving $functionName"
}
function Count-Characters($file)
{
Instrument-Function {
Write-Host "Opening $file" `
-Background White -Foreground DarkGreen
$content = ""
if (Test-Path $file)
{
$content = Get-Content $file
}
else
{
throw "No such file"
}
Write-Host "File contains $($content.Length) characters" `
-Background White -Foreground DarkGreen
}
}
Count-Characters Print-Debug.ps1
Count-Characters nosuchfile.txt
腳本執行結果如圖6所示。
可以看到第2個函數通過調用形式進入函數,但并沒有輸出其相關屬性,這是由于代碼觸發了異常。可以將$DebugPerference設置為Inquire掛起腳本的執行,并查看其中的運行環境。這對于獲取發生錯誤的附加信息非常有用,圖7所示為調試的過程。
可以看到在執行過程中通過逐個確認命令可以掛起程序的執行,以檢查運行的相關條件。在程序中可以檢查$file變量值,在后面的調用中可以看到打開nosuchfile.txt文件時出現錯誤。然后使用exit命令退出嵌套的命令提示符,返回到Write-Debug的命令提示符下。這是非常有用的一種機制,類似調試程序時使用的斷點。
(3)生成警告
警告是一種診斷輸出的形式,主要針對需要查看的非危機性信息。警告信息通常意味著腳本的運行的環境中存在問題,但是腳本可能知道如何處理這些問題。并且將會繼續執行,向用戶傳達信息。一旦腳本執行發生錯誤,用戶會知道如何處理。Write-Warning是輸出警告信息的cmdllet,如圖8所示。
默認情況下,警告通過控制臺輸出。也可以通過全局變量$WarningPreference來控制是否顯示警告信息,其默認值為Continue。可以通過將其設置為SilentlyContinue來忽略警告信息或者設置為Inquire來逐句調試腳本,檢查當時的執行狀況。
在Count-Characters函數中拋出的異常并不合適,因為不存在文件的大小為零字節是正常的。這里使用警告來提示用戶更為合適,下面改寫腳本并命名為“Print-Warning.ps1”,代碼如下:
{
$parentInvocation = Get-Variable MyInvocation -scope 1 -ValueOnly
$functionName = $parentInvocation.MyCommand.Name
trap
{
Write-Debug "$functionName raised an error"
}
Write-Verbose "Entering $functionName"
&$body
Write-Verbose "Leaving $functionName"
}
function Count-Characters($file)
{
Instrument-Function {
Write-Host "Opening $file" `
-Background White -Foreground DarkGreen
$content = ""
if (Test-Path $file)
{
$content = Get-Content $file
}
else
{
Write-Warning "$file does not exist."
}
Write-Host "File contains $($content.Length) characters" `
-Background White -Foreground DarkGreen
}
}
Count-Characters Print-Debug.ps1
Count-Characters nosuchfile.txt
運行腳本并先后設置$WarningPreference對象的內容,這樣即可看到調試信息,執行結果如圖9所示。
當要檢測的文件不存在時顯示相應的提示,使用戶明確知道問題所在。如果用戶對這些內容不感興趣,也可以選擇將其關閉。
(4)控制錯誤輸出
Write-Verbose、Write-Debug和Write-Warning由于對錯誤信息的覆蓋面不同,所以適用于不同的調試和使用場合;另外一個類似的cmdlet是Write-Error,也可以用其全局變量$ErrorActionPreference來控制輸出內容,如圖10所示。
如圖中所示,在發生未知錯誤時可以通過設置$ErrorActionPreference的值為Inquire掛起嵌套的執行代碼來查看當時的錯誤所在。
2、步進調試腳本和中斷執行
步進調試代碼是代碼執行的特殊狀態,會在每一行代碼執行之前詢問用戶下一步操作。表面上看起來這樣執行是很繁瑣的,但在遇到程序錯誤時卻非常有效。在每個語句前添加Set-PSDebug來實現步進調試,Count-Characters.ps1腳本中包含Count-Characters函數,代碼如下:
{
$content = ""
if (Test-Path $file)
{
$content = Get-Content $file
}
Write-Host "File contains $($content.Length) characters"
}
Count-Characters Print-Debug.ps1
Count-Characters nosuchfile.txt
為了逐個語句步進通過整個腳本,需要在執行腳本之前調用帶有Step參數的Set-PSDebug的cmdlet,執行結果如圖11所示。
從上圖中可以看到可在程序執行的任何一步掛起執行,并在嵌套的提示符下檢查環境的附加信息,檢查后可以用exit命令退出嵌套的命令提示符。為了返回正常的Shell,可能需要重啟Shell進程或者傳遞Off參數調用Set-PSDebug關閉調試模式,如圖12所示。
PowerShell本身不支持斷點,但是可以通過掛起腳本的執行并啟動嵌套的提示符查詢變量內容或修改環境的形式模擬設置斷點。下面創建新的腳本,并命名為“Count-CharactersDebug.ps1”,代碼如下:
{
Write-Host "Breakpoing hit!" -ForegroundColor Red
function prompt
{
"DEBUG> "
}
$host.EnterNestedPrompt()
}
function Count-Characters($file)
{
$content = ""
if (Test-Path $file)
{
$content = Get-Content $file
}
else
{
Start-Debug
}
Write-Host "File contains $($content.Length) characters"
}
Count-Characters Print-Debug.ps1
Count-Characters nosuchfile.txt
上述代碼執行時以紅色標識debug信息,以區別其他信息。可以在使得嵌套調試提示信息更加醒目,便于提示用戶注意,執行結果如圖13所示。
3、跟蹤腳本執行細節
PowerShell最強大的功能之一是通過跟蹤命令執行來診斷和解決其中存在的錯誤,所有調試的功能是為腳本開發人員、cmdlet開發人員或者高級用戶準備的。這些用戶能跟蹤詳細的操作日志,包括Shell本身及其cmdlet。在PowerShell中能夠觀察到兩類跟蹤日志,一是Shell的內部操作,如設置變量及調用函數的;二是其他能夠獲取到跟蹤的指定操作。
(1)跟蹤Shell內部操作
跟蹤腳本需要操作的相關信息及執行的順序,要調用Set-PSDebug cmdlet并傳遞Trace參數將Shell切換到跟蹤模式。PowerShell提供1和2兩種級別的跟蹤方式,后一種會生成更詳細的輸出。下面創建名為“Trace-Command.ps1”的腳本文件,其中包含兩個數相除的操作函數Calculate,代碼如下:
{
$result = $a / $b
return $result
}
Calculate 1 2
為了獲取腳本執行的所在行,將開關的跟蹤切換到等級1的調試狀態,執行結果如圖14所示。
可以看到在執行的過程中首先定義了Calculate函數,并在后面第7行調用這個函數,然后在第3行和第4行執行函數的計算操作。如果將跟蹤的等級改為2,則執行結果如圖15所示。除了行數和執行日志,還可以獲取被調用腳本和函數的相關信息,以及變量賦值的日志,這是輸出日志中包含的!起到的作用。需要強調的是可以通過表達式方式獲取復雜腳本的內容,并通過這些信息調試函數名沖突和不必要的函數重載。
級別2可以提供命令執行的詳細的內部信息,圖16所示為獲取的Get-Children執行的詳細信息。
在程序執行時能夠看到返回大段的日志,從中可以看到Get-ChildItem執行時在后臺觸發了一些復雜的操作,如計算文件訪問屬性相關的權限值以輸出文件的Mode字符串,以及獲取LastWriteTime并通過使用System.String.Format的.NET方法轉換為字符串。一旦設置了調試狀態,就會在任何cmdlet執行的過程中輸出相關的一些詳細信息,這在調試時很有用。為了重載普通的Shell操作狀態,可以重啟Shell進程或者調用Set-PSDebug –Off命令。
(2)跟蹤特定操作
PowerShell允許跟蹤其組件的特定操作,用戶可以獲取命令執行的相關信息或在提示符下鍵入特定函數、腳本塊、綁定參數的cmdlet,以及顯式或隱式類型轉換后Shell調用的命令。為了得到這些信息,需要使用Trace-Command這個cmdlet。
首先使用Trace-Command獲取在執行gal ii命令時需要調用命令的信息,為此需要跟蹤CommandDiscovery組件,執行結果如圖17所示。
其中所示的gal命令最后解析為Get-Allias cmdlet,在更深層次可看到Get-Alias被名為“Microsoft.PowerShell.Commands.GetAliasCommand”的.NET類執行,而PSHost參數通知Trace-Command將日志消息輸出到控制臺。下面演示當調用cmdlet時如何將變量綁定到cmdlet的參數,仍然使用相同的命令,只是將組件名換為ParameterBinding,執行結果如圖18所示。
ii的值被作為位置和Name參數來解析。檢查是否有參數缺失之后,Shell通過調用BeginProcessing、ProcessRecord和EndProcessing的順序為該cmdlet執行管道,這些步驟類似函數、腳本和腳本塊中操作管道的begin、process和end。最后使用Trac-Command獲取關于類型強制轉換的信息,需要跟蹤的組件變為TypeConversion。圖19所示為在計算表達式時獲取隱式類型轉換信息的方法。
從圖中可以看到返回的信息中包含PowerShell會將1從integer轉換為double類型,并分別嘗試隱式和顯式的類型轉換。
如果未小心使用命令跟蹤,將會得到大量無用的信息。通常情況下出現在其他方法無法解決的問題時,才會使用調試模式來處理。
4、總 結
錯誤處理和腳本調試的主題相互錯綜復雜地關聯在一起,加強腳本的錯誤處理邏輯后可以實現代碼的自診斷。這樣即可減少腳本的調試工作,快速而有效地達到目標需求。同樣也可以通過錯誤處理的相關技術,如錯誤陷阱輸出錯誤信息來快速調試程序。
為代碼添加好的錯誤處理處理機制是開發人員應該注意的問題,這樣能夠大大縮短調試的時間,而且任何使用代碼的人都可以從中受益。本文將著重講解了調試PowerShell代碼,即查找并排除bug,這是每個開發人員都應該熟練掌握的技術。在本文還介紹了PowerShell提供的解決方法,以及診斷和預防錯誤的方法,以使程序更加健壯和穩定。