Sponsored By Aspose - File Format APIs for .NET

Aspose are the market leader of .NET APIs for file business formats – natively work with DOCX, XLSX, PPT, PDF, MSG, MPP, images formats and many more!

The Exec Problem

I must admit that I don’t much care for PowerShell’s default behaviour with respect to errors, which is to continue on error. It feels very VB6 “On Error Resume Next”-ish. Given that it is a shell scripting language, I can understand why the PowerShell team chose this as a default. Fortunately you can change the default by setting $ErrorActionPreference = ‘Stop’, which terminates execution by throwing an exception. (The default value is Continue, which means the script prints the error and continues executing.) Unfortunately this only works for PowerShell commands and not external executables that return non-zero error codes. (In the shell world, a return code of zero (0) indicates success and anything else indicates failure.)

Take the following simple script:

'Starting script...'
$ErrorActionPreference = 'Stop'
ping -badoption
"Last Exit Code was: $LastExitCode"
rm nonexistent.txt
'Finished script'

image

Notice how execution continued after the ping command failed with an exit code of one (1) even though we have $ErrorActionPreference set to ‘Stop’. Also notice that the rm command, which is an alias for the PowerShell command, Remove-Item, did cause execution to abort as expected and ‘Finished script’ was never printed to the console. The discrepancy in error handling between PowerShell commands and executables is annoying and forces us to constantly think about what we’re calling – a PowerShell command or an executable. The obvious solution is:

'Starting script...'
$ErrorActionPreference = 'Stop'
ping -badoption
if ($LastExitCode -ne 0) { throw 'An error has occured...' }
rm nonexistent.txt
'Finished script'

image

The error handling code adds a lot of noise, IMHO, and feels like a throwback to COM and HRESULTs. Can we do better? Jorge Matos, one of the psake contributors came up with this elegant helper function:

function Exec([scriptblock]$cmd, [string]$errorMessage = "Error executing command: " + $cmd) { 
& $cmd
if ($LastExitCode -ne 0) {
throw $errorMessage
}
}

Note the “& $cmd” syntax. $cmd is a scriptblock and & is used to execute the scriptblock. We can now re-write our original script as follows. (N.B. Exec function is elided for brevity.)

'Starting script...'
$ErrorActionPreference = 'Stop'
exec { ping -badoption }
rm nonexistent.txt
'Finished script'

image

The script now terminates when the bad ping command is executed. We do have to remember to surround executables with exec {}, but this is less noise IMHO than having to check $LastExitCode and throwing an exception.

For those of you using psake for your builds, the Exec helper function is included in the latest versions of the psake module. So you can use it in your build tasks to ensure that you don’t try to run unit tests if msbuild fails horribly. smile_regular

Happy Scripting!

About James Kovacs

James Kovacs is a Technical Evangelist for JetBrains. He is passionate in sharing his knowledge about OO, SOLID, TDD/BDD, testing, object-relational mapping, dependency injection, refactoring, continuous integration, and related techniques. He blogs on CodeBetter.com as well as his own blog, is a technical contributor for Pluralsight, writes articles for MSDN Magazine and CoDe Magazine, and is a frequent speaker at conferences and user groups. He is the creator of psake, a PowerShell-based build automation tool, intended to save developers from XML Hell. James is the Ruby Track Chair for DevTeach, one of Canada’s largest independent developer conferences. He received his Bachelors degree from the University of Toronto and his Masters degree from Harvard University.
This entry was posted in PowerShell. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://www.dominic.cronin.nl/weblog Dominic Cronin

    I had to check twice when I tried this and found the behaviour on my system to be different.

    Why? – Well, it turned out that as I have the Power Shell Community Extensions loaded, “ping” is aliased to a cmdlet.