O'Reilly logo

Monad (AKA PowerShell) by Andy Oakley

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

When Things Go Wrong

For anything but the simplest of tasks, there's always a chance that something can go wrong. Aware that processes and scripts don't always run as planned and can sometimes hit unexpected problems, MSH offers an error-handling system that gives the script author the ability to control what happens next in times of trouble.

Before we begin, it's important to call out the two types of errors that can occur when processing a command. The first type, a non-terminating error, indicates that some problem has occurred but that execution can still continue. An example of a non-terminating error is an access problem that occurs when trying to read a protected resource or write to a read-only file. In contrast, a terminating error signifies a condition in which execution cannot possibly continue and the command is terminated.

The core of the error-handling system is exposed by the trap keyword. The keyword is always followed by a script block that contains instructions for what to do when an error occurs. To make use of this, we'll also come across some additional ubiquitous parameters that are used to specify what a cmdlet should do in the case of error.

How Do I Do That?

Let's start with a simple example that is guaranteed to cause a problem: division by zero. Dividing any number by zero generates an error message and causes MSH to complain:

    MSH D:\MshScripts> 100/0
    Attempted to divide by zero.
    At line:1 char:5
    + 100/0 <<<<

Whenever a runtime error occurs, MSH automatically updates the special $error array with information about the problem. The most recent error is in the first slot ([0]), the second most recent at [1], and so on:

    MSH D:\MshScripts> $error[0]
    Attempted to divide by zero.

The $error variable is useful for diagnosing errors after execution has finished, but suppose we'd like to take action as the problems arise. For this simple example, instead of just writing out the message to the screen, we want to write out a special message when a problem occurs. Let's create a script, shown in Example 4-3, that contains a very simple error handler.

Example 4-3. SimpleTrap.msh

trap
{
    "In error handler"
}

100/0

Now, when we run the script, we'll see that our own trap statement is run. This is just the beginning:

    MSH D:\MshScripts> SimpleTrap.msh
    In error handler
    : Attempted to divide by zero.
    At D:\MshScripts\SimpleTrap.msh:6 char:5
    + 100/0 <<<<

When inside the trap block, MSH automatically populates the special variable $_ with details of the problem that landed execution there. Now we're in business. Example 4-4 contains the improved trap handler.

Example 4-4. ImprovedTrap.msh

trap
{
    "In error handler"
    "Problem:"+$_.Message
}

100/0

Dealing with division by zero cases probably isn't typical of day-to-day problems. Let's instead look at the task of copying a set of files where we know one will fail. For this scenario, let's assume we have one folder, source, that contains files a.txt, b.txt, and c.txt, and we're planning to copy them into the dest folder that already contains a write-protected copy of a.txt. We can set up this little structure from either an MSH or CMD prompt with the following commands:

    mkdir source
    "content" > source\a.txt
    "content" > source\b.txt
    "content" > source\c.txt

    mkdir dest
    copy source\a.txt dest\a.txt
    attrib +r dest\a.txt

Now that we're set up, let's try copying the contents of source to dest:

    MSH D:\MshScripts> copy-item source\* dest
    copy-item : Access to the path 'D:\MshScripts\dest\a.txt' is denied.

As expected, we see that the a.txt file could not be overwritten because it is write-protected. However, on closer inspection, look what made it into dest:

    MSH D:\MshScripts> get-childitem dest


        Directory: FileSystem::D:\MshScripts\dest


    Mode    LastWriteTime     Length Name
    ----    -------------     ------ ----
    -ar--   Apr 05 16:16          9  a.txt
    -a---   Apr 05 16:16          9  b.txt
    -a---   Apr 05 16:16          9  c.txt

Sure enough, the b.txt and c.txt files made it over. Although the copy-item cmdlet hit a problem, it kept on trying to copy the other files that matched the wildcard.

The cmdlet's behavior in the face of a non-terminating error is controlled by the -ErrorAction option. By default, this takes a value of Continue, which, in case you hadn't guessed, instructs the cmdlet to notify the user that a problem occurred (by generating the "Access to the path ... " message in this case) and continue processing any additional cases. By using another ErrorAction setting, we can change how the cmdlet deals with problems.

First, let's reset the scenario by deleting the b.txt and c.txt files with a del dest\[bc].txt command. This time, we'll tell MSH to ask us what to do if any problems arise by using the -ErrorAction Inquire setting:

    MSH D:\MshScripts> copy-item -ErrorAction Inquire source\* dest

    Confirm
    Access to the path 'D:\MshScripts\dest\a.txt' is denied.
    [Y] Yes  [A] Yes to All  [H] Halt Command  [S] Suspend  [?] Help
    (default is "Y"):

MSH will now wait for some user input about what to do next before it moves ahead.

Finally, let's take a look at Stop, one of the other ErrorAction settings that effectively transforms any non-terminating errors into terminating errors and instructs the cmdlet to give up immediately and execute the trap handler if present. In Example 4-5, we bring together a handful of the techniques we've learned so far to create a simple script for ROBOCOPY-like behavior that will retry a file copy 10 times before giving up. For the sake of consistency, we'll continue to try overwriting the write-protected file so, it's fairly unlikely that any of the 10 attempts will succeed.

Example 4-5. RetryCopy.msh

$retryCount=10

while ($retryCount -gt 0)
{
        $success = $true

        trap {
                $script:retryCount--
                $script:success = 0
                "Retrying.."
                continue
        }

        copy-item -ErrorAction Stop source\* dest

        if ($success) { $retryCount = 0 }
}

"Done"

When it comes time to run this script, we'll see the script iterating through its loop before finally giving up:

    MSH D:\MshScripts> .\retryCopy.msh
    Retrying..
    Retrying..
    Retrying..
    Retrying..
    Retrying..
    Retrying..
    Retrying..
    Retrying..
    Retrying..
    Retrying..
    Done

What Just Happened?

The trap keyword is a basic part of the MSH script language and is equal in standing to many other keywords such as while, if, and for. The trap keyword can be followed by an error type in square brackets to indicate that its handler should only be run if the error is of the specified type. For example, to catch only problems with division by zero, we would write the following:

    trap [DivideByZeroException]
    {
        "Divide by zero trapped"
        break
    }

A trap must always include a script block, which defines the instructions to run when a problem arises. The $_ special variable is always available within the script block to enable the script to figure out what went wrong (which is often useful for deciding what to do next).

trap blocks are subject to scoping rules just as variables are. A trap block is entered only when an error occurs at that level. A parent scope will never invoke the trap handlers of any children, but an error inside a child (such as a function, filter, or loop) will cause execution to jump to the nearest trap block. Each scope can contain several trap blocks; when more than one is present, each is executed in turn when a problem arises.

After execution has finished inside the trap block, the error has usually become evident to the user. By placing a continue statement at the end of the trap block (as the last instruction before the closing brace), MSH understands that it is to continue at the end of the trap handler instead of terminating execution.

The ErrorAction option has a number of different settings that control how cmdlets behave when problems arise. In a pipeline, it's valid to use different ErrorAction settings for different stages; indeed, it's this fine-grain control that gives MSH its flexibility in handling different types of errors at each stage of processing. Table 4-6 describes the valid ErrorAction settings and the effects of each one.

Table 4-6. ErrorAction values and their effects

ErrorAction value

Effect

Stop

Abort on failure and treat all non-terminating errors as terminating

Continue

Generate an error and continue

Inquire

Ask the user how to proceed (see below)

SilentlyContinue

Proceed to the next item without generating an error

The Inquire prompt is worthy of a short discussion. It is shown when a problem arises and user input is needed to determine the next steps. The "Yes" option allows execution to start up again but will result in a similar prompt for every failure case that follows. Meanwhile, "Yes to All" assumes that "Yes" will be the answer for any future failures, so no further questions will be asked. "No" has the effect of stopping the cmdlet in its tracks so that no further processing will be attempted, whereas "No to All" is assumed to stop all future cmdlets. The "Suspend" option is useful because it starts a little sub-shell with all of the current settings, state, and an MSH> prompt, and it allows for browsing and troubleshooting. In the previous example, we could have used a sub-shell to run a quick attrib -r a.txt to resolve the issue.

What About...

... Changing the default ErrorAction value? Yes, you can do it. Instead of having to supply an -ErrorAction option for every cmdlet, MSH actually picks up the default value from a global variable called $ErrorActionPreference. If your preference is to have MSH ask how to proceed in every instance of a problem, add a $ErrorActionPreference="Inquire" line to your profile.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required