Colonel Panic
Colonel Panic

Reputation: 137722

Powershell analogue of Bash's `set -e`

How can I make Powershell behave like Bash with its flag set -e? set -o errexit makes a Bash script "Exit immediately if a simple command exits with a non-zero status".

I thought I could do this by setting $ErrorActionPreference="Stop" but this doesn't seem to work. Suppose I have a script a.ps1

$ErrorActionPreference="Stop"
& cmd /c "exit 1"
echo "cmd exited `$LastExitCode=$LastExitCode and `$?=$?"

If I run it

.\a. ; echo "a.ps1 exited `$LastExitCode=$LastExitCode `$?=$?"

To my surprises it prints

cmd exited $LastExitCode=1 and $?=False
a.ps1 exited $LastExitCode=1 $?=True

What's going on?! I expected a.ps1 to exit after the first line, throwing an error and setting $? to False.

Is there any official documentation explaining $ErrorActionPreference? All I found was this post on a blog about coffee.

Upvotes: 17

Views: 5293

Answers (2)

tmacam
tmacam

Reputation: 31

As @b-rad15 pointed out, there was a proposal to complement $ErrorActionPreference: $PSNativeCommandUseErrorActionPreference. It was implemented and landed with PowerShell 7.3.0.

From the docs:

When $PSNativeCommandUseErrorActionPreference is $true, native commands with non-zero exit codes issue errors according to $ErrorActionPreference.

Together, $ErrorActionPreference = 'Stop' and $PSNativeCommandUseErrorActionPreference = $true add the behavior analogous to Bash's set -e:

  • $ErrorActionPreference = 'Stop' displays the error message and stops executing whenever a cmdlet fails.
  • $PSNativeCommandUseErrorActionPreference = $true extends that functionality also to native commands.

You can see it in action in the snippet bellow:

# whoami will fail but get-date will still be executed
whoami -xyzzy
get-date
& {
    # our `set -e` equivalent combo
    $ErrorActionPreference = 'Stop'
    $PSNativeCommandUseErrorActionPreference = $true

    # whoami will fail and cause execution to stop
    # get-date will not be invoked
    whoami -xyzzy
    get-date
}

As @Raven have pointed out, "exit codes from native programs are not nearly as well-behaved as PowerShell cmdlets". Not all windows programs follow the Unix convention of using exit code 0 to report success. For instance, the native robocopy use non-zero exit codes to represent information other than errors.

Finally, just as set -e is not a silver bullet, there might be situations in which you might need to disable this functionality and do a more fine-grained error handling by inspecting $LastExitCode and/or a try-catch-finally block.

Upvotes: 1

Joey
Joey

Reputation: 354754

$ErrorActionPreference works as intended, it's just that exit codes from native programs are not nearly as well-behaved as PowerShell cmdlets.

For a cmdlet the error condition is fairly easy to determine: Did an exception happen or not? So the following would not reach the Write-Host statement with $ErrorActionPreference set to Stop:

Write-Error blah

or

Get-ChildItem somepathwhichsurelydoesntexisthere

For native programs an exit code of zero usually signals that no error occurred but that's merely a convention, as is a non-zero exit code signalling an error. Would you say that if choice exists with an exit code of 1 that it was an error?

The only robust way to handle this is to check the exit code after running a native command and handling it yourself, if needed. PowerShell doesn't try to guess what the result meant because the conventions aren't strong enough to warrant a default behaviour in this case.

You can use a function if you want to make your life easier:

function Invoke-NativeCommand {
  $command = $args[0]
  $arguments = $args[1..($args.Length)]
  & $command @arguments
  if ($LastExitCode -ne 0) {
    Write-Error "Exit code $LastExitCode while running $command $arguments"
  }
}

But in general many programs need different handling because they don't adhere to said convention.

Upvotes: 15

Related Questions