代码改变世界

Windows PowerShell 2.0 代码调试并排除bug

2010-12-01 00:12 by @天行健中国元素, ... 阅读, ... 评论, 收藏, 编辑

没有任何脚本或程序能够保证在任何情况下毫无错误地执行,在外界条件变化的情况下,需要预防可能出错之处。本文将着重讲解如何调试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”的函数,代码如下:

function Count-Characters($file)
{
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”文件,代码如下:

function Count-Characters($file)
{
$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”的函数,使用脚本块封装原始的函数体。该函数包含用于诊断的代码,并在调用原始函数体前后分别生成详细的输出信息。这些调试信息需要继续放在源代码中,代码如下:

function Instrument-Function($body)
{
$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函数在文件不存在的情况下抛出异常,代码如下:

function Instrument-Function($body)
{
$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”,代码如下:

function Instrument-Function($body)
{
		$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函数,代码如下:

function Count-Characters($file)
{
$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”,代码如下:

function Start-Debug
{
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,代码如下:

function Calculate($a, $b)
{
$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提供的解决方法,以及诊断和预防错误的方法,以使程序更加健壮和稳定。

 

赛迪网专稿地址:http://news.ccidnet.com/art/32859/20100708/2109237_1.html

 

作者: 付海军
出处:http://fuhj02.cnblogs.com
版权:本文版权归作者和博客园共有
转载:欢迎转载,为了保存作者的创作热情,请按要求【转载】,谢谢
要求:未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
个人网站: http://txj.shell.tor.hu/