Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native Command Error Handling #277

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 334 additions & 0 deletions 1-Draft/RFC00XX-Native-Command-Error-Handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
---
RFC: RFC00XX
Author: Jason Helmick
Status: Draft
Area: Core
Comments Due: 10/31/2020
---

# Native Command Error Handling

PowerShell scripts using native commands would benefit from being able to use error handling
features like those used by cmdlets.

## Motivation

In PowerShell by default, script processing continues when non-terminating errors occur. This is a
benefit when expecting non-terminating errors in normal execution such as non-responsive computers
from a list. This default behavior is controlled with the preference variable
`$ErrorActionPreference` default of `Continue`.

In production, often customers prefer that script execution stops when a non-terminating error
occurs. This is particularly true in CI where the preference is to fail fast. PowerShell currently
supports customers with this ability by setting the `$ErrorActionPreference` variable in the script
to `Stop`.

```Powershell
$ErrorActionPreference = 'Stop'
```

Native commands usually return an exit code to the calling application which will be zero for
success or non-zero for failure. However, native commands currently do not participate in the
PowerShell error stream. Redirected `stderr` output is not interpreted the same as the PowerShell
error stream as many native commands use `stderr` as information/verbose stream and thus only the
exit code matters. Users working with native commands in their scripts will need to check the
execution status after each call using a helper function similar to below:

```Powershell
if ($LASTEXITCODE -ne 0)
{
throw "Command failed. See above errors for details"
}
```

Simply relaying the errors through the error stream isn't the solution. The example itself doesn't
support all cases as `$?` can be false from a cmdlet or function error, making `$LASTEXITCODE`
stale.
Comment on lines +44 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example itself doesn't support all cases

What example is it referring to? The if ($LASTEXITCODE -ne 0) { ... } one right above? If so, it's not clear to me how $? will affect that example, and can you maybe elaborate a bit more on how $LASTEXITCODE would be stale?


In POSIX shells, this need to terminate on command error is addressed by the `set -e` configuration,
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
which causes the script to exit when a command fails. In addition, to ensure that an error is
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
returned if any command in a pipeline fails, POSIX shells address this need with `set -o pipefail`
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
configuration.
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved

This specification proposes a similar idea, but adapted to the PowerShell conventions of preference
variables and catchable, self-describing, terminating error objects. This proposal adds the
equivalent functionality of `set -eo pipefail` to stop execution and return an error if any command in a pipeline
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
fails.

The specification and alternative proposals are based on the
[Equivalent of bash `set -e` #3415](https://github.com/PowerShell/PowerShell/issues/3415)
committee review of the associated
[pull request](https://github.com/PowerShell/PowerShell/pull/3523), and
[implementation plan](https://github.com/PowerShell/PowerShell-RFC/pull/88#issuecomment-613653678)

## Specification

This RFC proposes a preference variable to configure the elevation of errors produced by native
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
commands to first-class PowerShell errors, so that native command failures will produce error
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
objects that are added to the error stream and may terminate execution of the script without added
boilerplate.

The specification proposes similar functionality to the common POSIX shell configuration `set -eo pipefail`.

- `set -u` - returns an error if any variable has not been previously defined.
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
- `set -e` - instructs to immediately exit if any command has a non-zero exit status.
- `set -o pipefail` - prevents errors in the pipeline from being masked. The return code for the
non-zero error is returned for the entire pipeline.

### set -u/ Set-StrictMode

In the example below, `set -u` is equivalent to `Set-StrictMode -Version 2.0`.
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved

theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
```bash
#!/bin/bash

/bin/echo "Without set -u : No output - No error is produced"
/bin/echo "$firstname"

/bin/echo ""

/bin/echo "With set -u : Equivalent to Set-StrictMode -Version 2.0"
set -u
/bin/echo "$firstname"
```

```output
Without set -u : No output - No error is produced


With set -u : Equivalent to Set-StrictMode -Version 2.0
/Users/jasonhelmick/natcmdbash/strict: line 10: firstname: unbound variable
```

```powershell
# In PowerShell

/bin/echo "Without Set-StrictMode -version 2.0 : No output - No error is produced"
/bin/echo "$firstname"

/bin/echo ""

/bin/echo "With Set-StrictMode -version 2.0 : Equivalent to set -u"
Set-StrictMode -version 2.0
/bin/echo "$firstname"
```

```output
Without set -u : No output - No error is produced


With Set-StrictMode -version 2.0 : Equivalent to set -u
InvalidOperation: /Users/jasonhelmick/natcmdbash/psstrict.ps1:9
Line |
9 | /bin/echo "$firstname"
| ~~~~~~~~~~
| The variable '$firstname' cannot be retrieved because it has not been set.
```

### set -e/ $ErrorActionPreference

In the example below, `set -e` is not equivalent to `$ErrorActionPReference` for native commands.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In the example below, `set -e` is not equivalent to `$ErrorActionPReference` for native commands.
In the example below, `set -e` is not equivalent to `$ErrorActionPreference` for native commands.


```bash
#!/bin/bash

/bin/echo "Without set -e : Will receive message after failure"
/bin/cat ./nofile
/bin/echo "Message After failure"

/bin/echo ""

/bin/echo "With set -e : Will NOT receive message after failure"
Copy link
Member

@SteveL-MSFT SteveL-MSFT Mar 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should say "Will NOT continue script execution after failure", apply globally

set -e
/bin/cat ./nofile
/bin/echo "Message After failure"
```

```output
Without set -e : Will receive message after failure
cat: ./nofile: No such file or directory
Message After failure

With set -e : Will NOT receive message after failure
cat: ./nofile: No such file or directory
```

```powershell
# In Powershell

/bin/echo "Without $ErrorActionPreference : Will receive message after failure"
/bin/cat ./nofile
/bin/echo "Message After failure"

/bin/echo ""

/bin/echo "With `$ErrorActionPreference = 'Stop' : SHOULD NOT receive message after failure - but does"
$ErrorActionPreference = "Stop"
/bin/cat ./nofile
/bin/echo "Message After failure"

/bin/echo ""

Write-Host "With cmdlet's - `$ErrorActionPreference = 'Stop' : Will NOT receive message after failure"
$ErrorActionPreference = 'Stop'
Get-Content ./nofile
Write-Host "Message After failure"
```

```output
Without $ErrorActionPreference : Will receive message after failure
cat: ./nofile: No such file or directory
Message After failure

With $ErrorActionPreference = 'Stop' : SHOULD NOT receive message after failure - but does
cat: ./nofile: No such file or directory
Message After failure

With cmdlet's - $ErrorActionPreference = 'Stop' : Will NOT receive message after failure
Get-Content: /Users/jasonhelmick/natcmdbash/psstop.ps1:17
Line |
17 | Get-Content ./nofile
| ~~~~~~~~~~~~~~~~~~~~
| Cannot find path '/Users/jasonhelmick/natcmdbash/nofile' because it does not exist.
```

### set -o pipefail/ PSNativeCommandErrorAction

In the example below, PowerShell has no equivalent to `set -o pipefail` for native commands.

```bash
#!/bin/bash

/bin/echo "Without set -o pipefail : returns 0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to clarify what 0 and 1 means for those not familiar

/bin/cat ./nofile | /bin/echo "pipe statement after failure"
/bin/echo "returns $?"

/bin/echo ""

/bin/echo "With set -o pipefail : returns non-zero"
set -o pipefail
/bin/cat ./nofile | /bin/echo "pipe statement after failure"
/bin/echo "returns $?"
```

```output
Without set -o pipefail : returns 0
pipe statement after failure
cat: ./nofile: No such file or directory
returns 0

With set -o pipefail : returns non-zero
cat: ./nofile: No such file or directory
pipe statement after failure
returns 1
```

```powershell
# In PowerShell

/bin/echo "Without `$PSNativeCommandErrorAction = 'Stop' : should return 0 (true)"
/bin/cat ./nofile | /bin/echo "pipe statement after failure"
/bin/echo "returns $?"

/bin/echo ""

/bin/echo "With set -o equiv. `$PSNativeCommandErrorAction = 'Stop' : should return non-zero (false)"
$PSNativeCommandErrorAction = 'Stop'
/bin/cat ./nofile | /bin/echo "pipe statement after failure"
/bin/echo "returns $?"
```

```output
Without `$PSNativeCommandErrorAction = 'Stop' : should return 0 (true)
cat: ./nofile: No such file or directory
pipe statement after failure
returns False

With set -o equiv. = 'Stop' : should return non-zero (false)
cat: ./nofile: No such file or directory
pipe statement after failure
returns False
```

> [!NOTE] A common configuration command for POSIX shells `set -euo pipefail` includes the `set -u`
> configuration which returns an error if any variable has not been previously defined. This is
> equivalent to the existing PowerShell `Set-StrictMode` and does not need to be addressed in this
> RFC.

### $PSNativeCommandErrorAction

The `$PSNativeCommandErrorAction` preference variable will implement a version of the
`$ErrorActionPreference` variable for native commands.

- The value will default to `Continue` for compatibility with existing behavior.
- For non-zero exit codes and except for the value `Ignore`, an `ErrorRecord` will be added to
`$Error` that wraps the exit code and the command executed that returned the exit code.
- Initially, only the existing values of `$ErrorActionPreference` will be supported in
`$PSNativeCommandErrorAction` as described in the table below.

### Valid values for `$PSNativeCommandErrorAction`

| Value | Definition
---------------- | -------------------
| Break | Enter the debugger when an error occurs or when an exception is raised.
| Continue | Displays the error message and continues executing.
| Ignore | Suppresses the error message and continues to execute the command.
| Inquire | Displays the error message and asks you whether you want to continue.
| SilentlyContinue| No effect. The error message isn't displayed and execution continues without interruption.
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
| Stop | Displays the error message and stops executing. In addition to the error generated, the Stop value generates an ActionPreferenceStopException object to the error stream. stream
| Suspend | Automatically suspends a workflow job to allow for further investigation.
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved

### Preference variable resolves error handling conflicts

In cases where an existing script already handles non-zero native command errors, the preference
variable `$PSNativeCommandErrorAction` may be set to `Continue`. Error handling behavior of the
PowerShell cmdlets is handled separately with `$ErrorActionPreference`.

### Error object

The reported error record object will be the new type: `NativeCommandException` with the following details:

| Property | Definition
---------------- | -------------------
| ExitCode: | The exit code of the failed command.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some alignment issues with vertical bars

| ErrorID: | `"Program {0} ended with non-zero exit code {1}"`, with the command name and the exit code, from resource string `ProgramFailedToComplete`.
| ErrorCategory: | `ErrorCategory.NotSpecified`.
| object: | exit code
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
| Source: | The full path to the application
| ProcessInfo | details of failed command including path, exit code, and PID

## Alternative Approaches and Considerations

### Native commands should respect $ErrorActionPreference

Native commands should respect $ErrorActionPreference and not need $PSNativeCommandErrorAction. This
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
could be released as an experimental feature.

### Extending $PSNativeCommandErrorAction

For users that prefer to set a single preference variable that affects both PowerShell and native
commands, The values of `$PSNativeCommandErrorAction` may be extended to include
`MatchErrorActionPreference`, which should apply the `$ErrorActionPreference` setting to native
commands.

- A conversion will occur between `$PSNativeCommandErrorAction` and `$ErrorActionPreference` values,
theJasonHelmick marked this conversation as resolved.
Show resolved Hide resolved
where `MatchErrorActionPreference` is converted to the current value of `$ErrorActionPreference`

### Explicit invocation logic

One way of checking for a single native command and handling its exit
status explicitly would be to put this logic into a script block and call it with the invocation
operator (`&`).

```Powershell
if ($LASTEXITCODE -ne 0)
{
throw "Command failed. See above errors for details"
}
```

### Set-StrictMode

A common configuration command for POSIX scripts `set -euo pipefail` includes the `set -u`
configuration which returns an error if any variable has not been previously defined. This is
equivalent to the existing PowerShell `Set-StrictMode` and is not needed to be addressed in this RFC.