What if it doesn't all go according to plan? This is the core question behind error management in any system and plays a large part in writing PowerShell scripts as well.
While it is a core concern in many systems, PowerShell's support for error management provides several unique features designed to make your job easier: the primary benefit being a distinction between terminating and nonterminating errors.
When running a complex script or scenario, the last thing you want is for your world to come crashing down because a script can't open one of the 1,000 files it is operating on. Although the system should make you aware of the failure, the script should still continue to the next file. That is an example of a nonterminating error. But what if the script runs out of disk space while running a backup? That should absolutely be an error that causes the script to exit—also known as a terminating error.
Given this helpful distinction, PowerShell provides several features that let you manage errors generated by scripts and programs, and also allows you to generate them yourself.
You want to get status information about the last command you executed, such as whether it succeeded.
Use one of the two variables PowerShell
provides to determine the status of the last command you executed: the
$lastExitCode variable and the
$? variable.
$? - change font of '(pronounced "dollar hook")'
$lastExitCodeA number that represents the exit code/error level of the last script or application that exited
$? (Pronounced
"Dollar Hook")A Boolean value that represents the success or failure of the last command
The $lastExitCode PowerShell variable is similar
to the %errorlevel% variable in DOS.
It holds the exit code of the last application to exit. This lets you
continue to interact with traditional executables (such as ping, findstr, and choice) that use exit codes as a primary
communication mechanism. PowerShell also extends the meaning of this
variable to include the exit codes of scripts, which can set their
status using the exit statement. Example 15.1, “Interacting with the $lastExitCode and $? variables” demonstrates this
interaction.
Example 15.1. Interacting with the $lastExitCode and $? variables
PS > ping localhost
Pinging MyComputer [127.0.0.1] with 32 bytes of data:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Ping statistics for 127.0.0.1:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milliseconds:
Minimum = 0ms, Maximum = 0ms, Average = 0ms
PS > $?
True
PS > $lastExitCode
0
PS > ping missing-host
Ping request could not find host missing-host. Please check the name and try again.
PS > $?
False
PS > $lastExitCode
1The $?
variable describes the exit status of the last application in a more
general manner. PowerShell sets this variable to False on error conditions such as when:
An application exits with a non-zero exit code.
A cmdlet or script writes anything to its error stream.
A cmdlet or script encounters a terminating error or exception.
For commands that do not indicate an error
condition, PowerShell sets the $?
variable to True.
To access the list of errors generated so far,
use the $error variable, as shown by
Example 15.2, “Viewing errors contained in the $error variable”.
Example 15.2. Viewing errors contained in the $error variable
PS > 1/0
Attempted to divide by zero.
At line:1 char:3
+ 1/ <<<< 0
+ CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRe
cordException
+ FullyQualifiedErrorId : RuntimeException
PS > $error[0] | Format-List -Force
ErrorRecord : Attempted to divide by zero.
StackTrace : at System.Management.Automation.Expressio
(...)
WasThrownFromThrowStatement : False
Message : Attempted to divide by zero.
Data : {}
InnerException : System.DivideByZeroException: Attempted to d
ivide by zero.
at System.Management.Automation.ParserOps
.PolyDiv(ExecutionContext context, Token opT
oken, Object lval, Object rval)
TargetSite : System.Collections.ObjectModel.Collection`1[
System.Management.Automation.PSObject] Invok
e(System.Collections.IEnumerable)
HelpLink :
Source : System.Management.AutomationThe PowerShell $error variable always holds the list of
errors generated so far in the current shell session. This list includes
both terminating and nonterminating errors.
PowerShell displays fairly detailed information when it encounters an error:
PS > Stop-Process -name IDoNotExist
Stop-Process : Cannot find a process with the name "IDoNotExist". Verify t
he process name and call the cmdlet again.
At line:1 char:13
+ Stop-Process <<<< -name IDoNotExist
+ CategoryInfo : ObjectNotFound: (IDoNotExist:String) [Stop-
Process], ProcessCommandException
+ FullyQualifiedErrorId : NoProcessFoundForGivenName,Microsoft.PowerS
hell.Commands.StopProcessCommandOne unique feature about
these errors is that they benefit a diverse and international community
of PowerShell users. Notice the FullyQualifiedErrorId
line: an error identifier that remains the same no matter which language
the error occurs in. When a user pastes this error message on an
Internet forum, newsgroup or blog, this fully qualified error ID never
changes. English users can then benefit from errors posted by
non-English PowerShell users, and vice versa.
If you want to view an error in a table or
list (through the Format-Table or
Format-List cmdlets), you must also
specify the -Force option to override
this customized view.
If you want to display errors in a more
compact manner, PowerShell supports an additional view called CategoryView that you set through the $errorView preference variable:
PS > Get-ChildItem IDoNotExist
Get-ChildItem : Cannot find path 'C:\IDoNotExist' because it does not exist.
At line:1 char:14
+ Get-ChildItem <<<< IDoNotExist
+ CategoryInfo : ObjectNotFound: (C:\IDoNotExist:String) [Ge
t-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.
GetChildItemCommand
PS > $errorView = "CategoryView"
PS > Get-ChildItem IDoNotExist
ObjectNotFound: (C:\IDoNotExist:String) [Get-ChildItem], ItemNotFoundExcep
tion$errorView = "CategoryView" Excellent ! and useful
To clear the list of errors, call the Clear() method on the $error list:
PS > $error.Count 2 PS > $error.Clear() PS > $error.Count 0
$error.Clear() Excellent ! and useful
For more information about PowerShell's
preference variables, type Get-Help
about_automatic_variables. If you want to determine only the
success or failure of the last command, see the section called “Determine the Status of the Last Command”.
the section called “Determine the Status of the Last Command”
Get-Help
about_automatic_variables
You want to display detailed information about errors that come from commands.
To list all errors (up to $MaximumErrorCount) that have occurred in this
session, access the $error
array:
$error
To list the last error that occurred in this
session, access the first element in the $error
array:
$error[0]
To list detailed information about an error,
pipe the error into the Format-List
cmdlet with the -Force
parameter:
$currentError = $error[0] $currentError | Format-List -Force
To list detailed information about the command
that caused an error, access its InvocationInfo property:
$currentError = $error[0] $currentError.InvocationInfo
To display errors in a more succinct
category-based view, change the $errorView variable to "CategoryView":
$errorView = "CategoryView"
To clear the list of errors collected by
PowerShell so far, call the Clear()
method on the $error variable:
$error.Clear()
Errors are a simple fact of life in the administrative world. Not all errors mean disaster, though. Because of this, PowerShell separates errors into two categories: nonterminating and terminating.
Nonterminating errors are the most common type
of error. They indicate that the cmdlet, script, function, or pipeline
encountered an error that it was able to recover from or was able to
continue past. An example of a nonterminating error comes from the
Copy-Item cmdlet. If it fails to copy
a file from one location to another, it can still proceed with the rest
of the files specified.
A terminating error, on the other hand,
indicates a deeper, more fundamental error in the operation. An example
of this can again come from the Copy-Item cmdlet when you specify invalid
command-line parameters.
Digging into an error (and its nested errors) can be cumbersome, so for a script that automates this task, see the section called “Program: Resolve an Error”.
Analyzing an error frequently requires several different investigative steps: displaying the error, exploring its context, and analyzing its inner exceptions.
Example 15.3, “Resolve-Error.ps1” automates these mundane tasks for you.
Example 15.3. Resolve-Error.ps1
#############################################################################
<#
.SYNOPSIS
Displays detailed information about an error and its context
#>
param($errorRecord = ($error[0]))
""
"If this is an error in a script you wrote, use the Set-PsBreakpoint cmdlet"
"to diagnose it."
""
'Error details ($error[0] | Format-List * -Force)'
"-"*80
$errorRecord | Format-List * -Force
'Information about the command that caused this error ' +
'($error[0].InvocationInfo | Format-List *)'
"-"*80
$errorRecord.InvocationInfo | Format-List *
'Information about the error''s target ' +
'($error[0].TargetObject | Format-List *)'
"-"*80
$errorRecord.TargetObject | Format-List *
'Exception details ($error[0].Exception | Format-List * -Force)'
"-"*80
$exception = $errorRecord.Exception
for ($i = 0; $exception; $i++, ($exception = $exception.InnerException))
{
"$i" * 80
$exception | Format-List * -Force
}
For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.
You want to manage the detailed debug, verbose, and progress output generated by cmdlets and scripts.
To enable debug output for scripts and cmdlets that generate it:
$debugPreference = "Continue" Start-DebugCommand
To enable verbose mode for a cmdlet that
checks for the -Verbose
parameter:
Copy-Item c:\temp\*.txt c:\temp\backup\ -Verbose
To disable progress output from a script or cmdlet that generates it:
$progressPreference = "SilentlyContinue" Get-Progress.ps1
In addition to error output (as described in the section called “Manage the Error Output of Commands”), many scripts and cmdlets generate several other types of output. This includes:
output (as described < no closing bracket
Helps you diagnose problems that may
arise and can provide a view into the inner workings of a command.
You can use the Write-Debug
cmdlet to produce this type of output in a script or the WriteDebug() method to produce this type
of output in a cmdlet. PowerShell displays this output in yellow,
unless you customize it through the $host.PrivateData.Debug* color
configuration variables.
Helps you monitor the actions of
commands at a finer level than the default. You can use the
Write-Verbose cmdlet to produce
this type of output in a script or the WriteVerbose() method to produce this
type of output in a cmdlet. PowerShell displays this output in
yellow, unless you customize it through the $host.PrivateData.Verbose* color
configuration variables.
Helps you monitor the status of
long-running commands. You can use the Write-Progress cmdlet to produce this
type of output in a script or the WriteProgress() method to produce this
type of output in a cmdlet. PowerShell displays this output in
yellow, unless you customize it through the $host.PrivateData.Progress* color
configuration variables.
Some cmdlets generate verbose and debug output
only if you specify the -Verbose and
-Debug parameters,
respectively.
To configure the debug, verbose, and progress
output of a script or cmdlet, modify the $debugPreference, $verbosePreference, and $progressPreference shell variables. These
variables can accept the following values:
Do not display this output.
Treat this output as an error.
Display this output.
Display a continuation prompt for this output.
You want to handle warnings, errors, and terminating errors generated by scripts or other tools that you call.
To control how your script responds to warning
messages, set the $warningPreference
variable. In this example, to ignore them:
$warningPreference = "SilentlyContinue"
To control how your script responds to
nonterminating errors, set the $errorActionPreference variable. In this
example, to ignore them:
$errorActionPreference = "SilentlyContinue"
To control how your script responds to
terminating errors, you can use either the try /
catch / finally statements, or the
trap statement. In this example, to
output a message and continue with the script:
try
{
1 / $null
}
catch [DivideByZeroException]
{
"Don't divide by zero!"
}
finally
{
"Script that will be executed even if errors occur in the try statement"
}Use the trap statement if you want its
error handling to apply to the entire scope:
trap [DivideByZeroException] { "Don't divide by zero!"; continue }
1 / $nullPowerShell defines several preference variables that help you control how your script reacts to warnings, errors, and terminating errors. As an example of these error management techniques, consider the following script:
############################################################################## Write-Warning "Warning: About to generate an error" Write-Error "Error: You are running this script" throw "Could not complete operation."
For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.
You can now see how a script might manage those separate types of errors:
PS > $warningPreference = "Continue"
PS > Get-WarningsAndErrors
WARNING: Warning: About to generate an error
Get-WarningsAndErrors : Error: You are running this script
At line:1 char:22
+ Get-WarningsAndErrors <<<<
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteError
Exception
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExc
eption,Get-WarningsAndErrors
Could not complete operation.
At line:15 char:6
+ throw <<<< "Could not complete operation."
+ CategoryInfo : OperationStopped: (Could not complete opera
tion.:String) [], RuntimeException
+ FullyQualifiedErrorId : Could not complete operation.Once you modify the warning preference, the
original warning message gets suppressed. A value of
SilentlyContinue is useful when you are expecting an
error of some sort.
PS > $warningPreference = "SilentlyContinue"
PS > Get-WarningsAndErrors
Get-WarningsAndErrors : Error: You are running this script
At line:1 char:22
+ Get-WarningsAndErrors <<<<
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteError
Exception
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExc
eption,Get-WarningsAndErrors
Could not complete operation.
At line:15 char:6
+ throw <<<< "Could not complete operation."
+ CategoryInfo : OperationStopped: (Could not complete opera
tion.:String) [], RuntimeException
+ FullyQualifiedErrorId : Could not complete operation.When you modify the error preference, you suppress errors and exceptions, as well:
PS > $errorActionPreference = "SilentlyContinue" PS > Get-WarningsAndErrors PS >
An addition to the $errorActionPreference variable, all cmdlets
let you specify your preference during an individual call:
PS > $errorActionPreference = "Continue" PS > Get-ChildItem IDoNotExist Get-ChildItem : Cannot find path '...\IDoNotExist' because it does not exist. At line:1 char:14 + Get-ChildItem <<<< IDoNotExist PS > Get-ChildItem IDoNotExist -ErrorAction SilentlyContinue PS >
If you reset the error preference back to
Continue, you can see the impact of a
try / catch /
finally statement. The message from the Write-Error call makes it through, but the
exception does not:
PS > $errorActionPreference = "Continue"
PS > try { Get-WarningsAndErrors } catch { "Caught an error" }
Get-WarningsAndErrors : Error: You are running this script
At line:1 char:28
+ try { Get-WarningsAndErrors <<<< } catch { "Caught an error" }
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteError
Exception
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExc
eption,Get-WarningsAndErrors
Caught an errorThe try /
catch / finally statement acts
like the similar statement in other programming languages. First, it
executes the code inside of its script block. If it encounters a
terminating error, it executes the code inside of the
catch script block. It executes the code in the
finally statement no matter what—an especially useful
feature for cleanup or error-recovery code.
A similar technique is the
trap statement:
PS > $errorActionPreference = "Continue"
PS > trap { "Caught an error"; continue }; Get-WarningsAndErrors
Get-WarningsAndErrors : Error: You are running this script
At line:1 char:60
+ trap { "Caught an error"; continue }; Get-WarningsAndErrors <<<<
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteError
Exception
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExc
eption,Get-WarningsAndErrors
Caught an errorUnlike the try statement,
the trap statement handles terminating errors for
anything in the scope that defines it. For more information about
scopes, see the section called “Control Access and Scope of Variables and Other Items”.
After handling an error, you can also
remove it from the system's error collection by typing
$error.RemoveAt(0).
For more information about error management in PowerShell, see the section called “Managing Errors”. For more detailed information about the valid settings of these preference variables, see Appendix A, PowerShell Language and Environment.
You want your script to notify its caller of a warning, error, or terminating error.
############################################################################## Write-Warning "Warning: About to generate an error" Write-Error "Error: You are running this script" throw "Could not complete operation."
To write warnings and errors, use the Write-Warning and Write-Error cmdlets, respectively. Use the
throw statement to generate a
terminating error.
When you need to notify the caller of your
script about an unusual condition, the Write-Warning, Write-Error, and throw statements are the way to do it. If your
user should consider the message as more of a warning, use the Write-Warning cmdlet. If your script
encounters an error (but can reasonably continue past that error), use
the Write-Error cmdlet. If the error
is fatal and your script simply cannot continue, use a throw statement.
For information on how to handle these errors when thrown by other scripts, see the section called “Handle Warnings, Errors, and Terminating Errors”. For more information about error management in PowerShell, see the section called “Managing Errors”. For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.
When you write scripts that heavily interact with the user, you may sometimes feel that your script could benefit from better performance.
When tackling performance problems, the first rule is to measure the problem. Unless you can guide your optimization efforts with hard performance data, you are almost certainly directing your efforts to the wrong spots. Random cute performance improvements will quickly turn your code into an unreadable mess, often with no appreciable performance gain! Low-level optimization has its place, but it should always be guided by hard data that supports it.
The way to obtain hard performance data is from a profiler. PowerShell doesn't ship with a script profiler, but Example 15.4, “Get-ScriptPerformanceProfile.ps1” uses PowerShell features to implement one.
Example 15.4. Get-ScriptPerformanceProfile.ps1
################################################################################
param($logFilePath = $(throw "Please specify a path to the transcript log file."))
function Main
{
$uniqueLines = @{}
$samples = GetSamples $uniqueLines
"Breakdown by line:"
"----------------------------"
$counts = @{}
$totalSamples = 0;
foreach($item in $samples.Keys)
{
$counts[$samples[$item]] = $item
$totalSamples += $samples[$item]
}
foreach($count in ($counts.Keys | Sort-Object -Descending))
{
$line = $counts[$count]
$percentage = "{0:#0}" -f ($count * 100 / $totalSamples)
"{0,3}%: Line {1,4} -{2}" -f $percentage,$line,
$uniqueLines[$line]
}
""
"Breakdown by marked regions:"
"----------------------------"
$functionMembers = GenerateFunctionMembers
foreach($key in $functionMembers.Keys)
{
$totalTime = 0
foreach($line in $functionMembers[$key])
{
$totalTime += ($samples[$line] * 100 / $totalSamples)
}
$percentage = "{0:#0}" -f $totalTime
"{0,3}%: {1}" -f $percentage,$key
}
}
function GetSamples($uniqueLines)
{
$logStream = [System.IO.File]::Open($logFilePath, "Open", "Read", "ReadWrite")
$logReader = New-Object System.IO.StreamReader $logStream
$random = New-Object Random
$samples = @{}
$lastCounted = $null
while(-not $host.UI.RawUI.KeyAvailable)
{
$sleepTime = [int] ($random.NextDouble() * 100.0)
Start-Sleep -Milliseconds $sleepTime
$rest = $logReader.ReadToEnd()
$lastEntryIndex = $rest.LastIndexOf("DEBUG: ")
if($lastEntryIndex -lt 0)
{
if($lastCounted) { $samples[$lastCounted] ++ }
continue;
}
$lastEntryFinish = $rest.IndexOf("\n", $lastEntryIndex)
if($lastEntryFinish -eq -1) { $lastEntryFinish = $rest.length }
$scriptLine = $rest.Substring(
$lastEntryIndex, ($lastEntryFinish - $lastEntryIndex)).Trim()
if($scriptLine -match 'DEBUG:[ \t]*([0-9]*)\+(.*)')
{
$last = $matches[1]
$lastCounted = $last
$samples[$last] ++
$uniqueLines[$last] = $matches[2]
}
$logReader.DiscardBufferedData()
}
$logStream.Close()
$logReader.Close()
$samples
}
function GenerateFunctionMembers
{
$callstack = New-Object System.Collections.Stack
$currentFunction = "Unmarked"
$callstack.Push($currentFunction)
$functionMembers = @{}
foreach($line in (Get-Content $logFilePath))
{
if($line -match 'write-debug "ENTER (.*)"')
{
$currentFunction = $matches[1]
$callstack.Push($currentFunction)
}
elseif($line -match 'write-debug "EXIT"')
{
[void] $callstack.Pop()
$currentFunction = $callstack.Peek()
}
else
{
if($line -match 'DEBUG:[ \t]*([0-9]*)\+')
{
if(-not $functionMembers[$currentFunction])
{
$functionMembers[$currentFunction] =
New-Object System.Collections.ArrayList
}
if(-not $functionMembers[$currentFunction].Contains($matches[1]))
{
[void] $functionMembers[$currentFunction].Add($matches[1])
}
}
}
}
$functionMembers
}
. Main
Get-ScriptPerformanceProfile.ps1 Looks really useful - some expanded explanation would be welcome.
I would also vote for description. I expect that in the final version all scripts will be with comments as first edition. For example this script is over five pages with very useful comments inside (in 1st ed.).
Sorry, there's a bug in the feedback system that's dropping comments from most of the book. It's in the manuscript and will be in print - hopefully we can get the online issues sorted out.
For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.
No comments yet
Add a comment